"""
GUI-enabled WolfArray with OpenGL rendering and wxPython integration.
This module defines :class:`WolfArray`, which inherits from
:class:`WolfArrayModel` (data operations) and :class:`Element_To_Draw`
(OpenGL / MapViewer integration). It adds:
- Multiple 2D rendering back-ends (display-list via Cython, per-cell
shader, shared-resource shader).
- LOD (Level-Of-Detail) tiling for large grids.
- Interactive wx dialogs for crop, band selection, volume estimation, etc.
- Palette / colormap management with automatic isopop.
- 3D preview support.
Author: HECE - University of Liege, Pierre Archambeau
Date: 2024
Copyright (c) 2024 University of Liege. All rights reserved.
This script and its content are protected by copyright law. Unauthorized
copying or distribution of this file, via any medium, is strictly prohibited.
"""
import os
import sys
import logging
import numpy as np
import numpy.ma as ma
import wx
from typing import TYPE_CHECKING
from datetime import datetime
import math
from pathlib import Path
try:
from OpenGL.GL import *
except:
[docs]
msg = 'Error importing OpenGL library'
raise Exception(msg)
try:
from ..PyTranslate import _
except ImportError as e:
raise Exception('Error importing modules')
try:
from ..drawing_obj import Element_To_Draw
except ImportError as e:
raise Exception('Error importing modules')
try:
from wolf_libs import wolfogl
except ImportError as e:
raise Exception('Error importing wolfogl.pyd')
try:
from ..opengl.py3d import WolfArray_plot3D, TypeOfView, WolfArrayPlotShader
from ..opengl.wolf_array_shader2d import WolfArrayShader2D
except ImportError as e:
raise Exception('Error importing modules')
try:
from ..PyPalette import wolfpalette
from ..PyVertexvectors import Zones, vector, wolfvertex, zone, Triangulation
from ..PyVertex import cloud_vertices
except ImportError as e:
raise Exception('Error importing modules')
from ..ui.wolf_array_ui import NewArray, CropDialog
from ._header_wolf import header_wolf
from ._clipping import WolfArrayClipZone, ClipSlider, ClipSliderEdge, ClipConfig, ClipConfigs
from ._selection_data import SelectionData, SelectionDataMB
from ._base import (
WolfArrayModel,
OGLRenderer,
DEFAULT_OGLRENDERER,
VERSION_RGB,
WOLF_ARRAY_HILLSHAPE,
WOLF_ARRAY_FULL_SINGLE,
WOLF_ARRAY_FULL_DOUBLE,
WOLF_ARRAY_FULL_INTEGER,
WOLF_ARRAY_FULL_INTEGER8,
WOLF_ARRAY_FULL_UINTEGER8,
WOLF_ARRAY_FULL_INTEGER16,
WOLF_ARRAY_FULL_INTEGER16_2,
WOLF_ARRAY_FULL_LOGICAL,
WOLF_ARRAY_MB,
)
if TYPE_CHECKING:
from ..ui.wolf_array_ops import Ops_Array
[docs]
class WolfArray(WolfArrayModel, Element_To_Draw):
"""GUI-enabled WolfArray with OpenGL rendering and wxPython integration.
Inherits all data operations from :class:`WolfArrayModel` and adds:
- OpenGL 2D rendering via display-lists (Cython ``wolfogl``) or GLSL
shaders (:class:`WolfArrayShader2D`).
- LOD tiling: the grid is divided into tiles whose resolution adapts
to the current zoom level.
- Palette / colormap management with automatic ``isopop`` adjustment.
- Interactive wx dialogs for crop, band selection, volume estimation,
file saving, and error reporting.
- ``Ops_Array`` integration for the GUI operations panel.
- 3D preview via :class:`WolfArrayPlotShader`.
"""
[docs]
myops: "Ops_Array" #: GUI operations panel (None when headless).
# ================================================================
# Construction / serialization
# ================================================================
def __init__(self,
fname:str = None,
mold:"WolfArrayModel" = None,
masknull:bool = True,
crop:list[list[float],list[float]]=None,
whichtype = WOLF_ARRAY_FULL_SINGLE,
preload:bool = True,
create:bool = False,
mapviewer = None,
nullvalue:float = 0.,
srcheader:header_wolf = None,
idx:str = '',
plotted:bool = False,
need_for_wx:bool = False,
mask_source:np.ndarray = None,
np_source:np.ndarray = None,
) -> None:
"""Create a GUI-enabled WolfArray.
Initialises :class:`Element_To_Draw` first (so that *wx_exists*,
*mapviewer*, *idx* and *plotted* are available), then delegates
data initialisation to :class:`WolfArrayModel`.
:param fname: Path to the data file (header is inferred).
:param mold: Existing model to copy geometry/data from.
:param masknull: If ``True``, mask cells equal to *nullvalue*.
:param crop: ``[[xmin, xmax], [ymin, ymax]]`` crop bounds.
:param whichtype: Array data type constant (e.g. ``WOLF_ARRAY_FULL_SINGLE``).
:param preload: If ``True``, read data from disk immediately.
:param create: If ``True``, open a *NewArray* dialog to define
the grid interactively (requires a running wx App).
:param mapviewer: :class:`WolfMapViewer` instance for GUI integration.
:param nullvalue: Value treated as "no data".
:param srcheader: Pre-built :class:`header_wolf` to use instead of
reading one from file.
:param idx: Identifier string shown in the viewer.
:param plotted: Initial visibility state.
:param need_for_wx: Force wx availability check.
:param mask_source: External boolean mask array.
:param np_source: External numpy array used as data source.
"""
# Initialize Element_To_Draw FIRST so wx_exists, mapviewer, idx, plotted are set
Element_To_Draw.__init__(self, idx, plotted, mapviewer, need_for_wx)
# Initialize data via WolfArrayModel (no create/mapviewer/need_for_wx)
WolfArrayModel.__init__(self, fname, mold, masknull, crop, whichtype,
preload, nullvalue, srcheader,
idx, mask_source, np_source)
# Handle interactive array creation (GUI only)
if create:
assert self.wx_exists, _('Array creation required a running wx App to display the UI')
new = NewArray(None, self.get_mapviewer())
ret = new.ShowModal()
if ret == wx.ID_CANCEL:
return
self.init_from_new(new)
# GUI-only attributes: shaded hillshade array
if self.wolftype != WOLF_ARRAY_HILLSHAPE and mapviewer is not None:
self.shaded = WolfArray(whichtype=WOLF_ARRAY_HILLSHAPE)
self.shaded.mypal.defaultgray()
self.shaded.mypal.automatic = False
# Clip zones for viewport-restricted rendering (glScissor)
[docs]
self._clip_zones: list[WolfArrayClipZone] = []
[docs]
self._clip_show_bars: bool = True # draw slider bars and borders
# GUI initialization
self.add_ops_sel()
self.rendering_machine = DEFAULT_OGLRENDERER
# ================================================================
# Clip zones
# ================================================================
@property
[docs]
def clip_zones(self) -> list[WolfArrayClipZone]:
"""Active clip zones restricting OpenGL rendering."""
return self._clip_zones
[docs]
def add_clip_zone(self, clip: WolfArrayClipZone) -> WolfArrayClipZone:
"""Add a clip zone to restrict rendering.
:param clip: The clip zone to add.
:return: The same clip zone (for chaining).
"""
if clip not in self._clip_zones:
self._clip_zones.append(clip)
return clip
[docs]
def remove_clip_zone(self, clip: WolfArrayClipZone):
"""Remove a previously added clip zone."""
try:
self._clip_zones.remove(clip)
except ValueError:
pass
[docs]
def clear_clip_zones(self):
"""Remove all clip zones (rendering becomes unrestricted)."""
self._clip_zones.clear()
[docs]
def add_clip_zone_from_bounds(self, xmin: float, ymin: float,
xmax: float, ymax: float,
active: bool = True) -> WolfArrayClipZone:
"""Create and add a clip zone from world-coordinate bounds.
:return: The newly created clip zone.
"""
cz = WolfArrayClipZone(xmin=xmin, ymin=ymin,
xmax=xmax, ymax=ymax, active=active)
return self.add_clip_zone(cz)
[docs]
def setup_curtain_clip(self, edge: ClipSliderEdge = ClipSliderEdge.RIGHT,
**slider_kwargs) -> tuple[WolfArrayClipZone, ClipSlider]:
"""Set up a "curtain" clip with a draggable slider.
Creates a clip zone covering the full array extent and attaches
a slider on the given *edge*. This is the recommended entry
point for the "swipe to compare" use case.
:param edge: Which edge the slider bar controls.
:param slider_kwargs: Extra keyword arguments forwarded to
:class:`ClipSlider` (e.g. *color*, *line_width*).
:return: ``(clip_zone, slider)`` tuple.
"""
cz = WolfArrayClipZone.from_header(self, owner_name=self.idx)
slider = cz.attach_slider(edge=edge, **slider_kwargs)
self.add_clip_zone(cz)
return cz, slider
[docs]
def setup_vertical_band(self, **slider_kwargs) -> tuple[WolfArrayClipZone, list[ClipSlider]]:
"""Set up a vertical band with left and right sliders.
The left slider hides everything to its left, the right slider
hides everything to its right.
:return: ``(clip_zone, [left_slider, right_slider])`` tuple.
"""
cz = WolfArrayClipZone.from_header(self, owner_name=self.idx)
sl = cz.attach_slider(edge=ClipSliderEdge.LEFT, **slider_kwargs)
sr = cz.attach_slider(edge=ClipSliderEdge.RIGHT, **slider_kwargs)
self.add_clip_zone(cz)
return cz, [sl, sr]
[docs]
def setup_horizontal_band(self, **slider_kwargs) -> tuple[WolfArrayClipZone, list[ClipSlider]]:
"""Set up a horizontal band with bottom and top sliders.
The bottom slider hides everything below, the top slider
hides everything above.
:return: ``(clip_zone, [bottom_slider, top_slider])`` tuple.
"""
cz = WolfArrayClipZone.from_header(self, owner_name=self.idx)
sb = cz.attach_slider(edge=ClipSliderEdge.BOTTOM, **slider_kwargs)
st = cz.attach_slider(edge=ClipSliderEdge.TOP, **slider_kwargs)
self.add_clip_zone(cz)
return cz, [sb, st]
[docs]
def setup_clip_from_view(self, xmin: float, ymin: float,
xmax: float, ymax: float,
invert: bool = False) -> WolfArrayClipZone:
"""Create a clip zone matching the given viewport bounds.
:param invert: If True, the *exterior* of the zone is drawn.
:return: The created clip zone.
"""
cz = WolfArrayClipZone(xmin=xmin, ymin=ymin,
xmax=xmax, ymax=ymax,
owner_name=self.idx,
invert=invert)
return self.add_clip_zone(cz)
[docs]
def snapshot_clip_config(self, name: str) -> ClipConfig:
"""Create a :class:`ClipConfig` snapshot of the current clip zones."""
return ClipConfig(name=name, owner_name=self.idx,
zones=list(self._clip_zones))
[docs]
def apply_clip_config(self, cfg: ClipConfig):
"""Apply a :class:`ClipConfig` to this array, replacing current clips."""
self._clip_zones.clear()
for z in cfg.zones:
z.owner_name = self.idx
for s in z.sliders:
s.owner_name = self.idx
self._clip_zones.append(z)
# ================================================================
# Properties
# ================================================================
@property
[docs]
def rendering_machine(self) -> OGLRenderer:
"""Current OpenGL rendering back-end (:class:`OGLRenderer`)."""
return self._rendering_machine
@rendering_machine.setter
def rendering_machine(self, which:OGLRenderer):
"""Set the rendering back-end and reset the display.
:param which: One of the :class:`OGLRenderer` enum members.
:raises AssertionError: If *which* is not a valid ``OGLRenderer``.
"""
assert which in OGLRenderer, _('Invalid rendering machine -- Must be one of %s') % list(OGLRenderer)
self._rendering_machine = which
self.reset_plot()
if self.myops is not None:
self.myops.update_rendering_machine(which)
@property
[docs]
def epsg_parent(self) -> int:
"""EPSG code of the parent map viewer.
Falls back to the array's own EPSG when no viewer is attached.
"""
if self.get_mapviewer() is not None:
return self.get_mapviewer().epsg
else:
logging.debug(_('No mapviewer linked to the array -- returning array EPSG'))
return self.epsg
[docs]
def _check_epsg_coherence(self):
"""Check whether the array and map viewer share the same EPSG code.
:return: ``True`` if both codes match, ``False`` otherwise.
:rtype: bool
"""
epsg_viewer = self.epsg_parent
if epsg_viewer is None or epsg_viewer != self.epsg:
logging.warning(_('EPSG code of the array (%s) is different from the mapviewer (%s)') % (self.epsg, epsg_viewer))
return False
return True
@property
[docs]
def usemask(self):
"""Whether the operations panel requests masked-value filtering.
Delegates to ``self.myops.usemask``; returns ``False`` if no
operations panel is attached.
"""
if self.myops is not None:
return self.myops.usemask
else:
return False
def __getstate__(self):
"""Return picklable state, stripping OpenGL and shaded references.
OpenGL list handles and the shaded hillshade array are not
serialisable; they are removed or reset here.
"""
state = self.__dict__.copy()
# Remove OpenGL references to avoid bad handles on unpickle
if 'mygrid' in state:
state['mygrid'] = {}
# If not empty, the OpenGL list is not properly serialized.
# During "delete_lists", OpenGL can produce an error.
if 'shaded' in state:
state['shaded'] = None # Avoid serializing the shaded WolfArray
return state
# ================================================================
# Appearance / opacity
# ================================================================
[docs]
def set_opacity(self, alpha:float):
"""Set the global transparency of this array.
*alpha* is clamped to [0, 1]. If an ``Ops_Array`` panel is
attached, the corresponding slider is synchronised.
:param alpha: Opacity value (0 = fully transparent, 1 = opaque).
:return: The clamped alpha value actually applied.
:rtype: float
"""
if alpha <0.:
alpha = 0.
if alpha > 1.:
alpha = 1.
self.alpha = alpha
if self.myops is not None:
self.myops.palalpha.SetValue(0)
self.myops.palalphaslider.SetValue(int(alpha*100))
self.reset_plot()
return self.alpha
# def find_minmax(self, update=False):
# if update:
# [self.xmin, self.xmax], [self.ymin, self.ymax] = self.get_bounds()
# ================================================================
# Selection / extraction
# ================================================================
# ================================================================
# 3D preview
# ================================================================
[docs]
def prepare_3D(self):
"""Build a :class:`WolfArrayPlotShader` for 3D rendering.
Converts the data to a GPU-ready z-texture (C-contiguous float32,
masked cells set to ``array.min()``) and creates a per-cell shader
object stored in ``self._array3d``.
"""
if self.array.ndim != 2:
logging.error(_('Array is not 2D'))
return
self._quads = self.get_centers()
ztext = np.require(self.array.data.copy(), dtype=np.float32, requirements=['C'])
assert ztext.flags.c_contiguous, _('ztext is not C-contiguous')
ztext[self.array.mask] = self.array.min()
self._array3d = WolfArrayPlotShader(self._quads,
self.dx, self.dy,
self.origx, self.origy,
zscale = 1.,
ztexture = ztext,
color_palette=self.mypal.get_colors_f32().flatten(),
color_values=self.mypal.values.astype(np.float32))
# ================================================================
# GUI operations panel (Ops_Array)
# ================================================================
[docs]
def show_properties(self):
"""Show the ``Ops_Array`` properties window (wxPython)."""
if self.wx_exists and self.myops is not None:
self.myops.SetTitle(_('Operations on array: ') + self.idx)
self.myops.Show()
self.myops.Center()
self.myops.Raise()
[docs]
def hide_properties(self):
"""Hide the ``Ops_Array`` properties window."""
if self.wx_exists and self.myops is not None:
self.myops.hide_properties()
@property
[docs]
def Operations(self) -> "Ops_Array":
"""The :class:`Ops_Array` instance managing GUI operations."""
return self.myops
[docs]
def add_ops_sel(self):
"""Attach an ``Ops_Array`` panel and a selection manager.
- Creates an ``Ops_Array`` if a map viewer is available.
- Creates a :class:`SelectionData` (or ``SelectionDataMB`` for
multi-block arrays) if none exists yet.
"""
if self.wx_exists and self.mapviewer is not None:
from ..ui.wolf_array_ops import Ops_Array as _Ops_Array
self.myops = _Ops_Array(self, self.mapviewer)
self.myops.Hide()
else:
self.myops = None
if self.mngselection is None:
if self.nb_blocks>0:
self.mngselection = SelectionDataMB(self)
else:
self.mngselection = SelectionData(self)
[docs]
def change_gui(self, newparentgui):
"""Re-parent this array to a different :class:`WolfMapViewer`.
If no viewer was previously attached, creates a new
``Ops_Array`` panel. Otherwise, migrates the existing one.
:param newparentgui: Target :class:`WolfMapViewer` instance.
:raises AssertionError: If *newparentgui* is not a
:class:`WolfMapViewer`.
"""
from ..PyDraw import WolfMapViewer
assert isinstance(newparentgui, WolfMapViewer), _('newparentgui must be a WolfMapViewer instance')
self.wx_exists = wx.App.Get() is not None
if self.mapviewer is None:
self.mapviewer = newparentgui
self.add_ops_sel()
else:
self.mapviewer = newparentgui
if self.myops is not None:
self.myops.mapviewer = newparentgui
else:
self.add_ops_sel()
[docs]
def init_from_new(self, dlg: NewArray):
"""Initialise grid geometry from a completed :class:`NewArray` dialog.
Reads *dx*, *dy*, *nbx*, *nby*, *origx*, *origy* from the dialog
widgets and creates a ones-filled masked array.
:param dlg: The :class:`NewArray` dialog that has been validated.
"""
self.dx = float(dlg.dx.Value)
self.dy = float(dlg.dy.Value)
self.nbx = int(dlg.nbx.Value)
self.nby = int(dlg.nby.Value)
self.origx = float(dlg.ox.Value)
self.origy = float(dlg.oy.Value)
self.array = ma.MaskedArray(np.ones((self.nbx, self.nby), order='F', dtype=np.float32))
self.mask_reset()
# ================================================================
# GUI hook overrides (wx dialogs)
# ================================================================
# These methods override headless stubs in WolfArrayModel so that
# interactive operations (crop, band choice, file save, errors) can
# prompt the user via wxPython dialogs.
[docs]
def _prompt_crop(self, dx:float, dy:float) -> list | None:
"""Show a :class:`CropDialog` and return crop bounds.
:param dx: Cell size in X (pre-filled, read-only).
:param dy: Cell size in Y (pre-filled, read-only).
:return: ``[[xmin, xmax], [ymin, ymax]]`` or ``None`` if cancelled.
"""
if not self.wx_exists:
logging.error(_('Crop bounds required but no GUI available'))
return None
newcrop = CropDialog(None, self.get_mapviewer())
if self.mapviewer is not None:
bounds = self.mapviewer.get_canvas_bounds()
newcrop.dx.Value = str(dx)
newcrop.dy.Value = str(dy)
newcrop.dx.Enable(False)
newcrop.dy.Enable(False)
newcrop.ox.Value = str(float((bounds[0] // 50.) * 50.))
newcrop.ex.Value = str(float((bounds[2] // 50.) * 50.))
newcrop.oy.Value = str(float((bounds[1] // 50.) * 50.))
newcrop.ey.Value = str(float((bounds[3] // 50.) * 50.))
badvalues = True
while badvalues:
badvalues = False
ret = newcrop.ShowModal()
if ret == wx.ID_CANCEL:
newcrop.Destroy()
return None
crop = [[float(newcrop.ox.Value), float(newcrop.ex.Value)],
[float(newcrop.oy.Value), float(newcrop.ey.Value)]]
tmpdx = float(newcrop.dx.Value)
tmpdy = float(newcrop.dy.Value)
if dx != tmpdx or dy != tmpdy:
if tmpdx / dx != tmpdy / dy:
badvalues = True
newcrop.Destroy()
return crop
[docs]
def _prompt_band_selection(self, band_names:list[str]) -> int | None:
"""Show a single-choice dialog for raster band selection.
:param band_names: List of human-readable band names.
:return: 1-based band index, or ``None`` if cancelled.
"""
if not self.wx_exists:
return 1
dlg = wx.SingleChoiceDialog(None,
_('Which band?'),
_('Band choice'),
band_names)
ret = dlg.ShowModal()
if ret == wx.ID_CANCEL:
dlg.Destroy()
return None
which = dlg.GetSelection() + 1
dlg.Destroy()
return which
[docs]
def _prompt_save_file(self, message:str, wildcard:str) -> str | None:
"""Show a file-save dialog.
:param message: Dialog title / prompt.
:param wildcard: File-type filter string (wx format).
:return: Selected file path, or ``None`` if cancelled.
"""
if not self.wx_exists:
return None
with wx.FileDialog(None, message, wildcard=wildcard,
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return None
return fileDialog.GetPath()
[docs]
def _show_error_dialog(self, message:str):
"""Log an error and optionally show a wx error dialog.
:param message: Error message text.
"""
logging.error(message)
if self.wx_exists:
dlg = wx.MessageDialog(None, message,
_('Wolf array'), wx.OK | wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
# ================================================================
# Volume estimation (interactive)
# ================================================================
[docs]
def volume_estimation(self, axs=None):
"""Interactive volume estimation driven by wx dialogs.
Prompts the user for Z-max, number of slices, and labelling
method, then delegates to
:meth:`WolfArrayModel.volume_estimation`.
:param axs: Optional matplotlib axes for plotting results.
:return: The result of ``WolfArrayModel.volume_estimation()``.
"""
import matplotlib.pyplot as plt
vect = self.array[np.logical_not(self.array.mask)].flatten()
zmin = np.amin(vect)
zmax_val = np.amax(vect)
dlg = wx.TextEntryDialog(None, _("Desired Z max ?\n Current Z min :") + str(zmin), _("Z max?"), str(zmax_val))
ret = dlg.ShowModal()
if ret == wx.ID_CANCEL:
dlg.Destroy()
return
zmax_val = float(dlg.GetValue())
dlg.Destroy()
dlg = wx.NumberEntryDialog(None, _("How many values?"), _("How many?"), _("How many ?"), 10, 0, 200)
ret = dlg.ShowModal()
if ret == wx.ID_CANCEL:
dlg.Destroy()
return
nb = dlg.GetValue()
dlg.Destroy()
methods = [_("All cells below the elevation (even if cells are disconnected)"),
_("Only the largest connected area below the elevation (not necessarily the same for each elevation)"),
_("Only the area below the elevation (if containing the cells stored in memory selection)")]
keys_select = list(self.SelectionData.selections.keys())
if len(keys_select) == 0:
methods = methods[:2]
dlg = wx.SingleChoiceDialog(None, _("Choose a method for selecting the area to integrate"), _("Labeling?"), methods)
ret = dlg.ShowModal()
if ret == wx.ID_CANCEL:
dlg.Destroy()
return
selection = dlg.GetSelection()
dlg.Destroy()
labeled = selection != 0
use_memory = False
memory_key = None
if selection == 2:
use_memory = True
if len(keys_select) > 1:
dlg = wx.SingleChoiceDialog(None, _("Choose a memory selection"), _("Memory selection?"), keys_select)
ret = dlg.ShowModal()
if ret == wx.ID_CANCEL:
dlg.Destroy()
return
memory_key = keys_select[dlg.GetSelection()]
dlg.Destroy()
elif len(keys_select) == 1:
memory_key = keys_select[0]
return WolfArrayModel.volume_estimation(self, zmax=zmax_val, nb=nb, labeled=labeled,
use_memory=use_memory, memory_key=memory_key,
output_fn=None, axs=axs)
# ================================================================
# Plot state management
# ================================================================
[docs]
def check_plot(self):
"""Enable plotting and lazy-load data if needed.
If the array has not been loaded yet (and a filename exists),
reads all data from disk and applies the null-value mask.
Updates the palette so that colour stops match the current
data range.
"""
self.plotted = True
if not self.loaded and self.filename != '':
# if not loaded, load it
self.read_all()
# self.read_data()
if self.masknull:
self.mask_data(self.nullvalue)
self.loaded = True
if VERSION_RGB==1 :
if self.rgb is None:
self.updatepalette(0)
else:
# For VERSION_RGB >= 2 (including shader path), palette values
# must be adapted to the data range so that colorValues[] sent
# to the GPU match the actual z-values in the array.
self.updatepalette(0)
[docs]
def uncheck_plot(self, unload:bool=True, forceresetOGL:bool=False, askquestion:bool=True):
"""Disable plotting and optionally unload data from memory.
:param unload: If ``True`` (default), free the array data and
OpenGL resources after prompting the user.
:param forceresetOGL: If ``True``, delete OpenGL lists without
asking.
:param askquestion: If ``True`` (default) and a wx App is
running, prompt the user before unloading or resetting.
"""
self.plotted = False
if unload and self.filename != '':
if Path(self.filename).exists():
# An array can exists with a filename but no written data on disk
# In this case, we don't want to delete the data in memory
if askquestion and self.wx_exists:
dlg = wx.MessageDialog(None,
_('Do you want to unload data? \n If YES, the data will be reloaded from file once checekd \n If not saved, modifications will be lost !!'),
style=wx.YES_NO)
ret = dlg.ShowModal()
if ret == wx.ID_YES:
unload=True
if unload:
self.delete_lists()
self.array = np.zeros([1])
if VERSION_RGB==1 : self.rgb = None
self.loaded = False
return
if not forceresetOGL:
if not OGLRenderer:
if askquestion and self.wx_exists:
dlg = wx.MessageDialog(None, _('Do you want to reset OpenGL lists?'), style=wx.YES_NO)
ret = dlg.ShowModal()
if ret == wx.ID_YES:
self.delete_lists()
if VERSION_RGB==1 : self.rgb = None
else:
self.delete_lists()
if VERSION_RGB==1 : self.rgb = None
[docs]
def reset_plot(self, whichpal=0, mimic=True):
"""Reset OpenGL resources and rebuild the palette.
Deletes cached display lists / shader objects, clears any 2D/3D
array caches, recomputes the palette, and propagates the reset
to linked arrays when *mimic* is ``True``.
:param whichpal: Palette index passed to :meth:`updatepalette`.
:param mimic: If ``True``, also reset linked arrays sharing the
same palette.
"""
self.count()
if self.plotted:
self.delete_lists()
if self._array3d is not None:
del self._array3d
self._array3d = None
if self._array2d is not None:
del self._array2d
self._array2d = None
if mimic:
for cur in self.linkedarrays:
if id(cur.mypal) == id(self.mypal) and id(self) !=id(cur):
cur.reset_plot(whichpal=whichpal, mimic=False)
self.updatepalette(whichpal)
if self.mapviewer is not None:
self.mapviewer.Refresh()
[docs]
def updatepalette(self, which:int=0, onzoom=[]):
"""Recompute palette colours for the current data range.
When ``which == 0`` and ``VERSION_RGB >= 2``, only the float32
temporary buffer is prepared (palette look-up is done on the
GPU). When ``which == 1`` or ``VERSION_RGB == 1``, a full RGBA
buffer (``self.rgb``) is computed on the CPU.
:param which: ``0`` for normal update, ``1`` to force CPU-side
RGBA computation (used by hillshade overlay).
:param onzoom: Optional ``[xmin, xmax, ymin, ymax]`` window;
if given, the iso-population palette is computed over this
sub-extent only.
"""
if self.array is None:
logging.warning(_('No data to update palette'))
return
if self.mypal.automatic:
if onzoom != []:
self.mypal.isopop(self.get_working_array(onzoom), self.nbnotnullzoom)
else:
self.mypal.isopop(self.get_working_array(), self.nbnotnull)
if VERSION_RGB==1 or which == 1:
if self.nbx * self.nby > 1_000_000 : logging.info(_('Computing colors'))
if self.wolftype not in [WOLF_ARRAY_FULL_SINGLE, WOLF_ARRAY_FULL_INTEGER8, WOLF_ARRAY_FULL_UINTEGER8]:
# FIXME: Currently, only some types are supported in Cython/OpenGL library
self._tmp_float32 = self.array.astype(dtype=np.float32)
self.rgb = self.mypal.get_rgba(self._tmp_float32)
else:
self._tmp_float32 = None
self.rgb = self.mypal.get_rgba(self.array)
if self.nbx * self.nby > 1_000_000 : logging.info(_('Colors computed'))
elif VERSION_RGB in [2 ,3]:
if self.wolftype not in [WOLF_ARRAY_FULL_SINGLE,
WOLF_ARRAY_FULL_INTEGER8,
WOLF_ARRAY_FULL_UINTEGER8,
WOLF_ARRAY_FULL_INTEGER16,
WOLF_ARRAY_FULL_INTEGER16_2,
WOLF_ARRAY_FULL_INTEGER,
WOLF_ARRAY_FULL_DOUBLE,
WOLF_ARRAY_HILLSHAPE]:
# FIXME: Currently, only some types are supported in Cython/OpenGL library
self._tmp_float32 = self.array.astype(dtype=np.float32)
else:
self._tmp_float32 = None
if VERSION_RGB==1 or which == 1:
if self.shading:
pond = (self.shaded.array-.5)*2.
pmin = (1. - self.shaded.alpha) * self.rgb
pmax = self.shaded.alpha * np.ones(self.rgb.shape) + (1. - self.shaded.alpha) * self.rgb
for i in range(4):
self.rgb[pond<0,i] = self.rgb[pond<0,i] * (1.+pond[pond<0]) - pmin[pond<0,i] * pond[pond<0]
self.rgb[pond>0,i] = self.rgb[pond>0,i] * (1.-pond[pond>0]) + pmax[pond>0,i] * pond[pond>0]
if VERSION_RGB==1 or which == 1: self.rgb[self.array.mask] = [1., 1., 1., 0.]
if self.myops is not None:
# update the wx
self.myops.update_palette()
if len(self.viewers3d) > 0:
for cur in self.viewers3d:
cur.update_palette(self.idx, self.mypal.get_colors_f32().flatten(), self.mypal.values.astype(np.float32))
[docs]
def find_minmax(self, update=False):
"""Refresh the bounding-box cache.
This override of :class:`Element_To_Draw` updates
``xmin``/``xmax``/``ymin``/``ymax`` from the grid header.
:param update: If ``True``, recompute bounds from the header;
otherwise leave the existing values untouched.
"""
if update:
[self.xmin, self.xmax], [self.ymin, self.ymax] = self.get_bounds()
# ================================================================
# Level of Detail (LOD)
# ================================================================
[docs]
def _get_LOD(self, sx:float, sy:float, xmin:float, ymin:float, xmax:float, ymax:float):
"""Determine the level-of-detail factor for the current viewport.
The LOD is the smallest power of two such that each cell
occupies at least 0.5 screen pixels. A return value of 1 means
every cell is drawn; higher values skip cells.
:param sx: Horizontal scale (pixels per world-unit).
:param sy: Vertical scale (pixels per world-unit).
:param xmin: Viewport lower-left X.
:param ymin: Viewport lower-left Y.
:param xmax: Viewport upper-right X.
:param ymax: Viewport upper-right Y.
:return: LOD factor (>= 1), or ``-1`` if nothing is visible.
"""
if self.plotted and sx is None:
sx = self.sx
sy = self.sy
xmin = self._xmin_plot
xmax = self._xmax_plot
ymin = self._ymin_plot
ymax = self._ymax_plot
else:
self.sx = sx
self.sy = sy
self._xmin_plot = xmin
self._xmax_plot = xmax
self._ymin_plot = ymin
self._ymax_plot = ymax
# Number of pixels per cell
nbpix = min(sx * self.dx, sy * self.dy)
if nbpix == 0.:
logging.warning(_('WolfArray.plot - No pixels to plot'))
return -1
elif nbpix >= 0.5:
# Cell covers at least 0.5 screen pixels
lod = 1
else:
# lod is the nearest power of 2 greater than or equal to 1 / nbpix
lod = int(math.ceil(math.log2(1. / nbpix)))
lod = max(lod, 1)
nbx_lod, nby_lod = self._get_grid_shape_LOD(lod)
if (nbx_lod == 1 and nby_lod == 1):
# One single tile for the whole array, no need to compute more LOD
if (self._lod_max_whole_array == -1):
# retain the LOD for the whole array
self._lod_max_whole_array = lod
elif lod > self._lod_max_whole_array:
# Using the coarser LOD for the whole array
lod = self._lod_max_whole_array
nbx_lod, nby_lod = self._get_grid_shape_LOD(lod)
return lod
[docs]
def _get_dxdy_LOD(self, lod:int):
"""Return the effective cell sizes ``(dx, dy)`` at a given LOD.
:param lod: Level-of-detail factor.
:return: ``(dx * lod, dy * lod)``.
"""
return float(self.dx * lod), float(self.dy * lod)
[docs]
def _get_scale_step_LOD(self, lod:int):
"""Return the tile step ``_gridsize * lod``.
:param lod: Level-of-detail factor.
"""
return int(self._gridsize * lod)
[docs]
def _get_grid_shape_LOD(self, lod:int):
"""Return ``(nbx, nby)`` of LOD tiles covering the full grid.
:param lod: Level-of-detail factor.
:return: Tuple ``(ceil(nbx / step), ceil(nby / step))``.
"""
nbx = max(math.ceil(float(self.nbx) / (self._get_scale_step_LOD(lod))), 1)
nby = max(math.ceil(float(self.nby) / (self._get_scale_step_LOD(lod))), 1)
return nbx, nby
[docs]
def _get_grid_LOD(self, lod:int, shader:bool=False):
"""Return (or create) the tile cache for a given LOD.
On first call for a given *lod*, allocates OpenGL display-list
IDs (legacy path) or a ``WolfArrayPlotShader`` grid (shader
path).
:param lod: Level-of-detail factor.
:param shader: If ``True``, allocate shader objects instead of
GL display-list IDs.
:return: ``dict`` with keys ``'nbx'``, ``'nby'``, ``'cache'``,
``'done'``.
"""
nbx_lod, nby_lod = self._get_grid_shape_LOD(lod)
if not lod in self._cache_grid.keys():
cache_grid_lod = self._cache_grid[lod] = {}
cache_grid_lod['nbx'] = nbx_lod
cache_grid_lod['nby'] = nby_lod
if not shader:
numlist = glGenLists(nbx_lod * nby_lod)
cache_grid_lod['firstlist'] = numlist
logging.debug(_('OpenGL lists - allocation') + ' - ' +_('first list')+str(numlist) )
cache_grid_lod['cache'] = np.linspace(numlist, numlist + nbx_lod * nby_lod - 1, num=nbx_lod * nby_lod,
dtype=np.integer).reshape((nbx_lod, nby_lod), order='F')
else:
cache_grid_lod['cache'] = np.empty((nbx_lod, nby_lod), dtype= WolfArrayPlotShader, order='F')
cache_grid_lod['done'] = np.zeros((nbx_lod, nby_lod), dtype=np.integer, order='F')
else:
cache_grid_lod = self._cache_grid[lod]
return cache_grid_lod
[docs]
def _get_part_to_plot_LOD(self, lod:int, xmin:float, ymin:float, xmax:float, ymax:float):
"""Compute the tile index range visible in the current viewport.
:param lod: Level-of-detail factor.
:param xmin: Viewport lower-left X.
:param ymin: Viewport lower-left Y.
:param xmax: Viewport upper-right X.
:param ymax: Viewport upper-right Y.
:return: ``((istart, iend), (jstart, jend))`` tile indices
(inclusive).
"""
nbx_lod, nby_lod = self._get_grid_shape_LOD(lod)
istart, jstart = self.get_ij_from_xy(xmin, ymin, scale= float(self._get_scale_step_LOD(lod)))
iend, jend = self.get_ij_from_xy(xmax, ymax, scale= float(self._get_scale_step_LOD(lod)))
istart = max(0, istart)
jstart = max(0, jstart)
iend = min(nbx_lod - 1, iend)
jend = min(nby_lod - 1, jend)
return (istart, iend), (jstart, jend)
# ================================================================
# OpenGL rendering (display lists & shaders)
# ================================================================
[docs]
def _draw_2d_lists(self, sx:float, sy:float, xmin:float, ymin:float, xmax:float, ymax:float):
"""Draw the array using legacy OpenGL display lists.
:param sx: Horizontal scale (pixels per world-unit).
:param sy: Vertical scale (pixels per world-unit).
:param xmin: Viewport lower-left X.
:param ymin: Viewport lower-left Y.
:param xmax: Viewport upper-right X.
:param ymax: Viewport upper-right Y.
"""
lod = self._get_LOD(sx, sy, xmin, ymin, xmax, ymax)
if lod == -1:
logging.debug(_('WolfArray.plot - No pixels to plot'))
return
# self._get_grid_LOD(lod)
(istart, iend), (jstart, jend) = self._get_part_to_plot_LOD(lod, xmin, ymin, xmax, ymax)
if self.wolftype != WOLF_ARRAY_HILLSHAPE and self.shading:
self.hillshade(self.azimuthhill, self.altitudehill)
if VERSION_RGB==1 :
self.updatepalette(0)
self.shaded.updatepalette(0)
self.shading=False
if self.mapviewer is not None:
from ..PyDraw import draw_type
if not self.idx + '_hillshade' in self.mapviewer.get_list_keys(drawing_type=draw_type.ARRAYS, checked_state= None) :# .added['arrays'].keys():
self.mapviewer.add_object('array', newobj=self.shaded, ToCheck=True, id=self.idx + '_hillshade')
try:
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
for j in range(jstart, jend + 1):
for i in range(istart, iend + 1):
self._fill_ogllist_for_one_grid_cell(lod, i, j)
try:
mylistdone = self._cache_grid[lod]['done'][i, j]
if mylistdone == 1:
mylist = self._cache_grid[lod]['cache'][i, j]
if mylist > 0:
glCallList(self._cache_grid[lod]['cache'][i, j])
except Exception:
logging.exception(
_('OpenGL error in WolfArray.plot 1 -- array=%s lod=%s cell=(%s,%s) bounds_i=(%s,%s) bounds_j=(%s,%s)'),
self.idx,
lod,
i,
j,
istart,
iend,
jstart,
jend,
)
glDisable(GL_BLEND)
except Exception:
logging.exception(
_('OpenGL error in WolfArray.plot 2 -- array=%s lod=%s bounds_i=(%s,%s) bounds_j=(%s,%s)'),
self.idx,
lod,
istart,
iend,
jstart,
jend,
)
[docs]
def _fill_ogllist_for_one_grid_cell(self, lod, loci, locj, force=False):
"""Build the OpenGL display list for one LOD tile.
Delegates actual vertex generation to the Cython ``wolfogl``
helpers depending on ``VERSION_RGB`` and the array dtype.
:param lod: Level-of-detail factor.
:param loci: Tile column index.
:param locj: Tile row index.
:param force: If ``True``, rebuild even if already built.
"""
grid_lod = self._get_grid_LOD(lod)
exists = grid_lod['done'][loci, locj]
if exists == 0 or force:
logging.debug('Computing OpenGL List for '+str(loci)+';' +str(locj) + ' on scale factor '+str(lod))
ox = self.origx + self.translx
oy = self.origy + self.transly
dx = self.dx
dy = self.dy
numlist = int(grid_lod['cache'][loci, locj])
logging.debug(' - creation list{}'.format(numlist))
try:
glNewList(numlist, GL_COMPILE)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
step = self._get_scale_step_LOD(lod)
jstart = max(locj * step, 0)
jend = min(jstart + step, self.nby)
istart = max(loci * step, 0)
iend = min(istart + step, self.nbx)
try:
if VERSION_RGB == 1:
if self.wolftype != WOLF_ARRAY_FULL_SINGLE:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_uint8(self._tmp_float32, self.rgb, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, np.uint8(self.alpha*255))
elif self.nbnotnull > 0:
wolfogl.addmeall_uint8(self._tmp_float32, self.rgb, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, np.uint8(self.alpha*255))
else:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_uint8(self.array, self.rgb, ox, oy, dx, dy, jstart, jend, istart, iend, lod,
self.nullvalue, np.uint8(self.alpha*255))
elif self.nbnotnull > 0:
wolfogl.addmeall_uint8(self.array, self.rgb, ox, oy, dx, dy, jstart, jend, istart, iend, lod,
self.nullvalue, np.uint8(self.alpha*255))
elif VERSION_RGB == 2:
if self.wolftype == WOLF_ARRAY_FULL_INTEGER8:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_int8_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.nbnotnull > 0:
wolfogl.addmeall_int8_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.wolftype == WOLF_ARRAY_FULL_UINTEGER8:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_uint8_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.nbnotnull > 0:
wolfogl.addmeall_uint8_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.wolftype in [WOLF_ARRAY_FULL_INTEGER16, WOLF_ARRAY_FULL_INTEGER16_2]:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_int16_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.nbnotnull > 0:
wolfogl.addmeall_int16_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.wolftype == WOLF_ARRAY_FULL_INTEGER:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_int_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.nbnotnull > 0:
wolfogl.addmeall_int_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.wolftype == WOLF_ARRAY_FULL_DOUBLE:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_double_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.nbnotnull > 0:
wolfogl.addmeall_double_pal(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst))
elif self.wolftype not in [WOLF_ARRAY_FULL_SINGLE, WOLF_ARRAY_HILLSHAPE]:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_pal(self._tmp_float32, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), -1.)
elif self.nbnotnull > 0:
wolfogl.addmeall_pal(self._tmp_float32, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), -1.)
else:
clr_float = self.mypal.colorsflt.copy()
clr_float[:,3] = self.alpha
if '_hillshade' in self.idx:
clr_float[1,3] = 0.
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_pal(self.array, clr_float, self.mypal.values, ox, oy, dx, dy, jstart, jend, istart, iend, lod,
self.nullvalue, self.alpha, int(self.mypal.interval_cst), -1.)
elif self.nbnotnull > 0:
wolfogl.addmeall_pal(self.array, clr_float, self.mypal.values, ox, oy, dx, dy, jstart, jend, istart, iend, lod,
self.nullvalue, self.alpha, int(self.mypal.interval_cst), -1.)
elif VERSION_RGB == 3:
if self.wolftype == WOLF_ARRAY_FULL_INTEGER8:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_int8_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.nbnotnull > 0:
wolfogl.addmeall_int8_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.wolftype == WOLF_ARRAY_FULL_UINTEGER8:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_uint8_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.nbnotnull > 0:
wolfogl.addmeall_uint8_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.wolftype in [WOLF_ARRAY_FULL_INTEGER16, WOLF_ARRAY_FULL_INTEGER16_2]:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_int16_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.nbnotnull > 0:
wolfogl.addmeall_int16_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.wolftype == WOLF_ARRAY_FULL_INTEGER:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_int_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.nbnotnull > 0:
wolfogl.addmeall_int_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.wolftype == WOLF_ARRAY_FULL_DOUBLE:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_double_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.nbnotnull > 0:
wolfogl.addmeall_double_pal_mask(self.array, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), self.array.mask)
elif self.wolftype not in [WOLF_ARRAY_FULL_SINGLE, WOLF_ARRAY_HILLSHAPE]:
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_pal_mask(self._tmp_float32, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), -1., self.array.mask)
elif self.nbnotnull > 0:
wolfogl.addmeall_pal_mask(self._tmp_float32, self.mypal.colorsflt, self.mypal.values, ox, oy, dx, dy, jstart,
jend, istart, iend, lod, self.nullvalue, self.alpha, int(self.mypal.interval_cst), -1., self.array.mask)
else:
clr_float = self.mypal.colorsflt.copy()
clr_float[:,3] = self.alpha
if '_hillshade' in self.idx:
clr_float[1,3] = 0.
if self.nbnotnull != self.nbx * self.nby:
if self.nbnotnull > 0:
wolfogl.addme_pal_mask(self.array, clr_float, self.mypal.values, ox, oy, dx, dy, jstart, jend, istart, iend, lod,
self.nullvalue, self.alpha, int(self.mypal.interval_cst), -1., self.array.mask)
elif self.nbnotnull > 0:
wolfogl.addmeall_pal_mask(self.array, clr_float, self.mypal.values, ox, oy, dx, dy, jstart, jend, istart, iend, lod,
self.nullvalue, self.alpha, int(self.mypal.interval_cst), -1., self.array.mask)
except Exception as e:
logging.error(repr(e))
raise NameError(_('OpenGL error in WolfArray.fillonecellgrid -- Please report this case with the data file and the context in which the error occured'))
pass
glEndList()
except Exception as e:
logging.error(repr(e))
raise NameError(
'Opengl in WolfArray_fillonecellgrid -- maybe a conflict with an existing opengl32.dll file - please rename the opengl32.dll in the libs directory and retry')
grid_lod['done'][loci, locj] = 1
[docs]
def _get_xy_centers_LOD(self, lod:int, loci:int, locj:int, usenap:bool = True):
"""Compute cell-centre coordinates for one LOD tile.
Returns a flat ``float32`` array of ``(x, y)`` pairs suitable
for upload as a VBO.
:param lod: Level-of-detail factor.
:param loci: Tile column index.
:param locj: Tile row index.
:param usenap: If ``True`` (default), exclude masked (null) cells.
:return: 1-D ``np.float32`` array ``[x0, y0, x1, y1, …]``.
"""
step = self._get_scale_step_LOD(lod)
jstart = max(locj * step, 0)
jend = min(jstart + step, self.nby)
istart = max(loci * step, 0)
iend = min(istart + step, self.nbx)
i_range = np.arange(istart, iend, lod)
j_range = np.arange(jstart, jend, lod)
if len(i_range) == 0 or len(j_range) == 0:
return np.array([], dtype=np.float32)
ii, jj = np.meshgrid(i_range, j_range, indexing='ij')
ij = np.column_stack([ii.ravel(), jj.ravel()])
if usenap:
# Filter based on masked cells in the array
mask = self.array.mask[ij[:, 0], ij[:, 1]]
ij = ij[~mask]
xy = self.get_xy_from_ij_array(ij)
# if lod > 1:
# # We need to adapt the xy coordinates to the current LOD
# # by adding a step size to the coordinates
# xy[:,0] += float (lod-1) * self.dx / 2.
# xy[:,1] += float (lod-1) * self.dy / 2.
return xy.flatten().astype(np.float32) #, (self.origx + istart * self.dx, self.origy + jstart * self.dy)
[docs]
def _fill_oglshader_for_one_grid_cell(self, lod, loci, locj, force=False):
"""Prepare one LOD tile for the per-cell shader pipeline.
Creates a :class:`WolfArrayPlotShader` object containing the
vertex buffer and z-texture for the tile.
:param lod: Level-of-detail factor.
:param loci: Tile column index.
:param locj: Tile row index.
:param force: If ``True``, rebuild even if already built.
"""
grid_lod = self._get_grid_LOD(lod, shader=True)
exists = grid_lod['done'][loci, locj]
if exists == 0 or force:
logging.debug('Preparing OpenGL Shader for '+str(loci)+';' +str(locj) + ' on scale factor '+str(lod))
try:
grid_lod['quads']= self._get_xy_centers_LOD(lod, loci, locj)
if 'ztext' not in self._cache_grid or self._cache_grid['ztext'] is None:
self._cache_grid['ztext'] = np.require(self.array.data.copy(), dtype=np.float32, requirements=['C'])
assert self._cache_grid['ztext'].flags.c_contiguous, _('ztext is not C-contiguous')
self._cache_grid['ztext'][self.array.mask] = self.array.min()
grid_lod['cache'][loci, locj] = WolfArrayPlotShader(grid_lod['quads'],
self.dx, self.dy,
self.origx, self.origy,
zscale = 1.,
ztexture = self._cache_grid['ztext'],
color_palette=self.mypal.get_colors_f32().flatten(),
color_values=self.mypal.values.astype(np.float32),
lod = lod)
grid_lod['cache'][loci, locj].add_parent(self.mapviewer, 0)
grid_lod['cache'][loci, locj].compute_walls = 0
grid_lod['cache'][loci, locj].color_uniform_in_part = self.mypal.interval_cst
grid_lod['cache'][loci, locj].update_palette(self.mypal.get_colors_f32().flatten(), self.mypal.values.astype(np.float32), self.alpha)
grid_lod['done'][loci, locj] = 1
except Exception as e:
logging.error(repr(e))
[docs]
def _draw_2d_shader(self, sx:float=None, sy:float=None, xmin:float=None, ymin:float=None, xmax:float=None, ymax:float=None):
"""Delegate 2D shader rendering to :class:`WolfArrayShader2D`."""
if self._shader_2d is None:
self._shader_2d = WolfArrayShader2D(self)
self._shader_2d.draw(sx=sx, sy=sy, xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)
[docs]
def _cleanup_shader_2d_resources(self):
"""Release all shared GL resources for 2D shader rendering."""
if self._shader_2d is not None:
self._shader_2d.cleanup()
self._shader_2d = None
[docs]
def plot(self, sx:float=None, sy:float=None, xmin:float=None, ymin:float=None, xmax:float=None, ymax:float=None, size:float=None):
"""Draw the array in the current OpenGL context.
Dispatches to :meth:`_draw_2d_shader` or :meth:`_draw_2d_lists`
depending on the active rendering backend. Also draws the
current selection overlay and attached zones.
When :attr:`clip_zones` contains active zones, rendering is
restricted to those regions via ``GL_SCISSOR_TEST``. Each
active zone triggers a separate rendering pass so that the
union of all zones is visible. Attached :class:`ClipSlider`
bars are drawn afterwards (outside the scissor) for visual
feedback.
:param sx: Horizontal scale (pixels per world-unit).
:param sy: Vertical scale (pixels per world-unit).
:param xmin: Viewport lower-left X.
:param ymin: Viewport lower-left Y.
:param xmax: Viewport upper-right X.
:param ymax: Viewport upper-right Y.
:param size: Unused — kept for API compatibility with
:class:`Element_To_Draw`.
"""
if not self.plotted:
logging.info(_('Plot is not desired for this array -- change "plotted" attribute to True to plot it'))
return
intersect = self.find_intersection(header_wolf.make(orig = (xmin, ymin), nb = (1,1), d= (xmax-xmin, ymax-ymin)))
if intersect is None:
logging.debug(_('Array is not visible in the current extent -- skipping drawing'))
return
self.plotting = True
active_clips = [cz for cz in self._clip_zones if cz.active]
if active_clips:
vp = glGetIntegerv(GL_VIEWPORT)
vp_width, vp_height = int(vp[2]), int(vp[3])
normal = [cz for cz in active_clips if not cz.invert]
inverted = [cz for cz in active_clips if cz.invert]
if inverted:
# Use stencil buffer to punch holes for inverted zones.
# 1) Clear stencil to 0
glClearStencil(0)
glClear(GL_STENCIL_BUFFER_BIT)
glEnable(GL_STENCIL_TEST)
# 2) For each inverted zone, draw a quad into stencil (set to 1)
glStencilFunc(GL_ALWAYS, 1, 0xFF)
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE)
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE)
glDepthMask(GL_FALSE)
for cz in inverted:
glBegin(GL_QUADS)
glVertex2f(cz.clip_xmin, cz.clip_ymin)
glVertex2f(cz.clip_xmax, cz.clip_ymin)
glVertex2f(cz.clip_xmax, cz.clip_ymax)
glVertex2f(cz.clip_xmin, cz.clip_ymax)
glEnd()
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE)
glDepthMask(GL_TRUE)
# 3) Render array only where stencil == 0 (outside holes)
glStencilFunc(GL_EQUAL, 0, 0xFF)
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP)
if normal:
for cz in normal:
cz.enable_scissor(xmin, ymin, xmax, ymax,
vp_width, vp_height)
self._plot_array_core(sx, sy, xmin, ymin, xmax, ymax)
WolfArrayClipZone.disable_scissor()
else:
self._plot_array_core(sx, sy, xmin, ymin, xmax, ymax)
glDisable(GL_STENCIL_TEST)
else:
# Normal clips only — simple scissor passes
for cz in normal:
cz.enable_scissor(xmin, ymin, xmax, ymax,
vp_width, vp_height)
self._plot_array_core(sx, sy, xmin, ymin, xmax, ymax)
WolfArrayClipZone.disable_scissor()
else:
self._plot_array_core(sx, sy, xmin, ymin, xmax, ymax)
self.plotting = False
# Draw slider bars and clip-zone borders (outside scissor)
if self._clip_show_bars:
for cz in active_clips if active_clips else []:
cz.draw_border()
for s in cz.sliders:
s.draw(xmin, ymin, xmax, ymax)
# Plot selected nodes
if self.mngselection is not None:
self.mngselection.plot_selection()
# Plot zones attached to array
if self.myops is not None:
self.myops.myzones.plot()
[docs]
def _plot_array_core(self, sx, sy, xmin, ymin, xmax, ymax):
"""Internal: dispatch to the active 2D rendering backend."""
if self._rendering_machine == OGLRenderer.SHADER:
self._draw_2d_shader(sx=sx, sy=sy, xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)
else:
self._draw_2d_lists(sx=sx, sy=sy, xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)
[docs]
def delete_lists(self):
"""Release all cached OpenGL display lists and shader objects."""
logging.debug('OpenGL lists - deletion -- array {}'.format(self.idx))
for idx, lod in enumerate(self._cache_grid):
if lod == 'ztext':
self._cache_grid[lod] = None
continue
cache_grid_lod = self._cache_grid[lod]
nbx = cache_grid_lod['nbx']
nby = cache_grid_lod['nby']
if 'firstlist' in cache_grid_lod:
first = cache_grid_lod['firstlist']
try:
glDeleteLists(first, nbx * nby)
except Exception as e:
logging.error('OpenGL error in WolfArray.delete_lists -- Please report this case with the data file and the context in which the error occured')
logging.error(e)
logging.debug(str(first)+' '+str(nbx * nby))
self._cache_grid = {}
self._lod_max_whole_array = -1
self._cleanup_shader_2d_resources()