Source code for wolfhece.wolf_array._base_gui

"""
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 add_clip_zone_from_header(self, h: header_wolf, active: bool = True) -> WolfArrayClipZone: """Create and add a clip zone from a :class:`header_wolf`. :return: The newly created clip zone. """ cz = WolfArrayClipZone.from_header(h, 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 # ================================================================
[docs] def extract_selection(self): """Create a new WolfArray from the current selection and add it to the map viewer with suffix ``'_extracted'``. """ newarray = self.SelectionData.get_newarray() mapviewer = self.get_mapviewer() if mapviewer is not None: mapviewer.add_object('array', newobj = newarray, ToCheck = True, id = self.idx + '_extracted')
# ================================================================ # 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()