Source code for wolfhece.pyvertexvectors._zone

"""GUI-enabled zone class with OpenGL and wx integration."""
from __future__ import annotations

import logging
import time as time_module
import warnings
import copy
import numpy as np
import wx
import wx._dataview
from wx.dataview import *
from wx.core import TreeItemId
from OpenGL.GL import *
import matplotlib.pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from typing import Union, Literal, TYPE_CHECKING
from pathlib import Path
from shapely.geometry import LineString, MultiLineString, Point, Polygon, MultiPolygon

if TYPE_CHECKING:
    from ._zones import Zones

from .. import is_opengl_context_available
from ..PyTranslate import _
from ..PyParams import Type_Param, new_json
from ..PyVertex import wolfvertex
from ..color_constants import getRGBfromI, getIfromRGB
from ..matplotlib_fig import Matplotlib_Figure as MplFig
from ._models import zoneModel, vectorModel
from ._triangulation import Triangulation
from ._vectorproperties import vectorproperties
from ._vector import vector
from ._models import VectorOGLRenderer

[docs] class zone(zoneModel): """ Objet de gestion d'informations vectorielles (GUI). Hérite de :class:`zoneModel` pour les données/géométrie et ajoute OpenGL, wx et matplotlib. """
[docs] mytree:TreeListCtrl
[docs] myitem:TreeItemId
# ================================================================ # Constructor # ================================================================ def __init__(self, lines:list[str]=[], name:str='NoName', parent:"Zones"=None, is2D:bool=True, fromshapely:Union[LineString,Polygon,MultiLineString, MultiPolygon]=None) -> None: """Initialise a GUI-enabled zone. :param lines: Raw text lines to parse (from a .vec file). :param name: Zone name. :param parent: Parent Zones object. :param is2D: Whether this is a 2-D zone. :param fromshapely: Optional Shapely geometry to import. """
[docs] self.myprops = None
[docs] self.idgllist = -99999
self.mytree = None
[docs] self._rendering_machine = None # None = inherit from parent Zones; set to VectorOGLRenderer to override
super().__init__(lines=lines, name=name, parent=parent, is2D=is2D, fromshapely=fromshapely) # ================================================================ # Factory overrides # ================================================================
[docs] def _make_vector(self, **kwargs) -> "vector": """Factory: create a new GUI-enabled vector.""" return vector(**kwargs)
[docs] def _make_zone(self, **kwargs) -> "zone": """Factory: create a new GUI-enabled zone.""" return zone(**kwargs)
[docs] def _make_zones(self, **kwargs) -> "Zones": """Factory: create a GUI-enabled Zones collection.""" from ._zones import Zones return Zones(**kwargs)
[docs] def _make_triangulation(self, **kwargs) -> Triangulation: """Factory: create a GUI-enabled triangulation.""" return Triangulation(**kwargs)
[docs] def find_nearest_vertex(self, x: float, y: float) -> wolfvertex | None: """Return the nearest vertex across all GUI vectors in the zone.""" return super().find_nearest_vertex(x, y)
[docs] def find_nearest_vector(self, x: float, y: float) -> vector | None: """Return the nearest GUI vector in the zone.""" return super().find_nearest_vector(x, y)
# ================================================================ # State management # ================================================================
[docs] def use(self): """ A utiliser """ for curvect in self.myvectors: curvect.use() self.used=True if self.mytree is not None: self.mytree.CheckItem(self.myitem) self.reset_listogl()
[docs] def unuse(self): """ Ne plus utiliser """ for curvect in self.myvectors: curvect.unuse() self.used=False if self.mytree is not None: self.mytree.UncheckItem(self.myitem) self.reset_listogl()
# ================================================================ # wx GUI / Tree management # ================================================================
[docs] def add2tree(self,tree:TreeListCtrl,root): """Add the zone to a wx TreeListCtrl. :param tree: Target tree control. :param root: Parent tree item. """ self.mytree=tree self.myitem=tree.AppendItem(root, self.myname,data=self) for curvect in self.myvectors: curvect.add2tree(tree,self.myitem) if self.used: tree.CheckItem(self.myitem) else: tree.UncheckItem(self.myitem)
[docs] def _fill_structure(self): """ Mise à jour des structures """ if self.parent is not None: self.parent.fill_structure()
# ================================================================ # Properties / Callbacks # ================================================================
[docs] def show_properties(self): """ Show properties of the zone --> will be applied to all vectors int he zone """ if self.myprops is not None: try: # Raises if the underlying wx C++ object was already deleted. self.myprops.prop.GetPageCount() except Exception: self.myprops = None if self.myprops is None: locvec = self._make_vector(parentzone=self) locvec.show_properties() self.myprops = locvec.myprop.myprops self.myprops[('Legend','X')] = str(99999.) self.myprops[('Legend','Y')] = str(99999.) self.myprops[('Legend','Text')] = _('Not used') if self._rotation_center is None: self.myprops[('Rotation','Center X')] = 99999. self.myprops[('Rotation','Center Y')] = 99999. else: self.myprops[('Rotation','Center X')] = self._rotation_center.x self.myprops[('Rotation','Center Y')] = self._rotation_center.y if self._rotation_step is None: self.myprops[('Rotation','Step [degree]')] = 99999. else: self.myprops[('Rotation','Step [degree]')] = self._rotation_step self.myprops[('Rotation', 'Angle [degree]')] = 0. if self._move_start is None: self.myprops[('Move','Start X')] = 99999. self.myprops[('Move','Start Y')] = 99999. else: self.myprops[('Move','Start X')] = self._move_start.x self.myprops[('Move','Start Y')] = self._move_start.y if self._move_step is None: self.myprops[('Move','Step [m]')] = 99999. else: self.myprops[('Move','Step [m]')] = self._move_step jsonstr = new_json({ _('Inherit from Zones'): -1, _('Legacy display lists'): 0, _('Modern shader pipeline'): 1, }, _('Rendering backend for this zone')) self.myprops.addparam('Rendering', 'Mode', -1, Type_Param.Integer, '', whichdict='Default', jsonstr=jsonstr) if self._rendering_machine is None: self.myprops[('Rendering', 'Mode')] = -1 elif self.rendering_machine == VectorOGLRenderer.SHADER: self.myprops[('Rendering', 'Mode')] = 1 else: self.myprops[('Rendering', 'Mode')] = 0 self.myprops[('Move', 'Delta X')] = 0. self.myprops[('Move', 'Delta Y')] = 0. self.myprops.Populate() self.myprops.set_callbacks(self._callback_prop, self._callback_destroy_props) self.myprops.SetTitle(_('Zone properties - {}'.format(self.myname))) self.myprops.Center() self.myprops.Raise()
[docs] def hide_properties(self): """ Hide the properties window """ if self.myprops is not None: # window for general properties self.myprops.Hide() for curvect in self.myvectors: curvect.hide_properties()
[docs] def _callback_destroy_props(self): """ Callback to destroy the properties window """ # The wx frame is already closing; just drop the Python reference. self.myprops = None
[docs] def _callback_prop(self): """ Callback to update properties """ if self.myprops is None: logging.warning(_('No properties available')) return old_mode = self.rendering_machine mode = self.myprops[('Rendering', 'Mode')] if self.myprops.is_in_default('Rendering', 'Mode') else -1 if mode == -1: self._rendering_machine = None self.reset_listogl() elif mode == 1: self.rendering_machine = VectorOGLRenderer.SHADER else: self.rendering_machine = VectorOGLRenderer.LIST mode_changed = old_mode != self.rendering_machine for curvec in self.myvectors: curvec.myprop.fill_property(self.myprops, updateOGL = False) angle = self.myprops[('Rotation', 'Angle [degree]')] dx = self.myprops[('Move', 'Delta X')] dy = self.myprops[('Move', 'Delta Y')] if angle!=0. and (dx!=0. or dy!=0.): logging.warning(_('Rotation and translation are not compatible')) return elif angle!=0.: if self._rotation_center is None: logging.warning(_('No rotation center defined')) return else: self.rotate(angle, self._rotation_center) self.clear_cache() elif dx!=0. or dy!=0.: self.move(dx, dy) self.clear_cache() if self.parent.mapviewer is not None: self.prep_listogl() self.parent.mapviewer.Refresh() if mode_changed: self.show_properties()
# ================================================================ # OpenGL rendering # ================================================================ @property
[docs] def rendering_machine(self): """Current rendering backend. Returns the zone's own setting, or the parent Zones' setting, or LIST. """ if self._rendering_machine is not None: return self._rendering_machine if self.parent is not None and hasattr(self.parent, '_rendering_machine'): rm = self.parent._rendering_machine if rm is not None: return rm return VectorOGLRenderer.SHADER
@rendering_machine.setter def rendering_machine(self, value): self._rendering_machine = value self.reset_listogl()
[docs] def prep_listogl(self): """ Préparation des listes OpenGL pour augmenter la vitesse d'affichage """ if self.rendering_machine == VectorOGLRenderer.SHADER: return # No display list preparation needed in shader mode if is_opengl_context_available(): self.plot(prep = True)
[docs] def plot(self, prep:bool=False, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None, anim_phase: float = 0.0): """Plot the zone using OpenGL. Dispatches to shader or display-list path depending on :attr:`rendering_machine`. :param prep: If True, compile into an OpenGL display list (list mode only). :param sx: Scale factor along X. :param sy: Scale factor along Y. :param xmin: Minimum X of the viewport. :param ymin: Minimum Y of the viewport. :param xmax: Maximum X of the viewport. :param ymax: Maximum Y of the viewport. :param size: Reference size for rendering. :param anim_phase: Animation phase ``[0, 1]`` for shader effects. """ if not is_opengl_context_available(): logging.debug(_('OpenGL context not available, skipping plot for zone {}').format(self.myname)) return if self.rendering_machine == VectorOGLRenderer.SHADER : #and not prep: self._plot_shader(sx=sx, sy=sy, xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax, size=size, anim_phase=anim_phase) else: self._plot_list(prep=prep, sx=sx, sy=sy, xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax, size=size)
[docs] def _plot_shader(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None, anim_phase: float = 0.0): """Render all vectors in this zone using the shader pipeline.""" if len(self.myvectors) == 0: return mapviewer = self.get_mapviewer() if mapviewer is None: logging.debug('No mapviewer for shader rendering of zone %s', self.myname) return mvp = np.ascontiguousarray(mapviewer.mvp, dtype=np.float32) viewport = None # Prefer the true OpenGL viewport when available (accounts for DPI and GL state). try: _mv, proj, vp = mapviewer.get_MVP_Viewport_matrix() if proj is not None: # glGetFloatv returns column-major data that PyOpenGL wraps # as a C-contiguous numpy array. The C-order byte layout # IS already the column-major format that # glUniformMatrix4fv(GL_FALSE) expects — do NOT use order='F'. mvp = np.ascontiguousarray(proj, dtype=np.float32) if vp is not None and len(vp) >= 4: viewport = (int(vp[2]), int(vp[3])) except Exception: pass if viewport is None or viewport[0] <= 0 or viewport[1] <= 0: try: sz = mapviewer.canvas.GetSize() viewport = (int(sz[0]), int(sz[1])) except Exception: viewport = (0, 0) if viewport[0] <= 0 or viewport[1] <= 0: w = int(getattr(mapviewer, 'width', 0) or 0) h = int(getattr(mapviewer, 'height', 0) or 0) viewport = (w, h) if w > 0 and h > 0 else (800, 600) def _animation_load(curvect) -> int: load = 0 anim_mode = getattr(curvect.myprop, 'anim_mode', 0) legend_anim_mode = getattr(curvect.myprop, 'legend_anim_mode', 0) fill_anim_mode = getattr(curvect.myprop, 'fill_anim_mode', 0) if anim_mode != 0 and curvect.intersects_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax): load += 1 if fill_anim_mode != 0 and getattr(curvect.myprop, 'filled', False) and curvect.intersects_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax): load += 1 if legend_anim_mode != 0 and curvect.legend_anchor_in_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax): load += 1 if legend_anim_mode != 0 and curvect.text_along_in_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax): load += 1 return load # Compute animation phase from the global animation clock. # Each vector may have a different anim_speed; we get a common # time base from the clock and scale by each vector's speed. needs_anim_refresh = False animation_load = 0 for curvect in self.myvectors: vector_visible = curvect.intersects_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) current_load = _animation_load(curvect) if current_load > 0: needs_anim_refresh = True animation_load += current_load if getattr(curvect.myprop, 'filled', False) and getattr(curvect.myprop, 'fill_anim_mode', 0) != 0: anim_speed = getattr(curvect.myprop, 'fill_anim_speed', 1.0) else: anim_speed = getattr(curvect.myprop, 'anim_speed', 1.0) # Get phase from global clock phase = mapviewer.anim_clock.get_phase(anim_speed) else: phase = 0.0 if vector_visible: curvect.plot(rendering_machine=VectorOGLRenderer.SHADER, mvp=mvp, viewport=viewport, anim_phase=phase) # Register/unregister this zone with the global animation clock if needs_anim_refresh: mapviewer.anim_clock.subscribe(self, load=animation_load) else: mapviewer.anim_clock.unsubscribe(self) self.has_legend = False self.has_image = False for curvect in self.myvectors: self.has_legend |= curvect.legend_anchor_in_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) self.has_image |= curvect.myprop.imagevisible and curvect.intersects_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) if self.has_image: for curvect in self.myvectors: if curvect.myprop.imagevisible and curvect.intersects_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax): curvect.plot_image(sx, sy, xmin, ymin, xmax, ymax, size) if self.has_legend: for curvect in self.myvectors: if not curvect.legend_anchor_in_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax): continue legend_anim_mode = getattr(curvect.myprop, 'legend_anim_mode', 0) if legend_anim_mode != 0: legend_speed = getattr(curvect.myprop, 'legend_anim_speed', 1.0) legend_phase = mapviewer.anim_clock.get_phase(legend_speed) else: legend_phase = 0.0 curvect.plot_legend(sx, sy, xmin, ymin, xmax, ymax, size, rendering_machine=VectorOGLRenderer.SHADER, mvp=mvp, viewport=viewport, anim_phase=legend_phase) # Text along polyline + dynamic tracking label _mouse_pos = getattr(mapviewer, '_current_mouse_pos', None) if _mouse_pos is not None: mouse_x, mouse_y = _mouse_pos else: mouse_x, mouse_y = None, None has_tracking = False for curvect in self.myvectors: vector_visible = curvect.intersects_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) legend_anim_mode = getattr(curvect.myprop, 'legend_anim_mode', 0) if legend_anim_mode != 0: legend_speed = getattr(curvect.myprop, 'legend_anim_speed', 1.0) text_phase = mapviewer.anim_clock.get_phase(legend_speed) else: text_phase = 0.0 if curvect.text_along_in_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax): curvect.plot_text_along_polyline(mvp=mvp, viewport=viewport, anim_phase=text_phase) if getattr(curvect.myprop, 'tracking_label_enabled', False) and vector_visible: has_tracking = True if mouse_x is not None and mouse_y is not None: curvect.plot_tracking_label(mvp=mvp, viewport=viewport, mouse_x=mouse_x, mouse_y=mouse_y) # Let the mapviewer know whether this zone has active tracking labels. mapviewer.set_tracking_label(id(self), has_tracking)
[docs] def _plot_list(self, prep:bool=False, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None): """Render all vectors using legacy display lists / immediate mode.""" if prep: if len(self.myvectors) == 0: logging.debug(_('No vector in zone -- {}').format(self.myname)) return try: if self.idgllist==-99999: self.idgllist = glGenLists(1) self.has_legend = False self.has_image = False glNewList(self.idgllist,GL_COMPILE) for curvect in self.myvectors: curvect.plot() self.has_legend |= curvect.myprop.legendvisible self.has_image |= curvect.myprop.imagevisible glEndList() except Exception: logging.exception( _('OpenGL error in zone.plot -- zone=%s prep=%s vectors=%s idgllist=%s mapviewer=%s'), self.myname, prep, [curvect.myname for curvect in self.myvectors], self.idgllist, type(self.get_mapviewer()).__name__ if self.get_mapviewer() is not None else None, ) else: if len(self.myvectors) == 0: logging.debug(_('No vector in zone -- {}').format(self.myname)) return if self.idgllist!=-99999: glCallList(self.idgllist) else: self.has_legend = False self.has_image = False for curvect in self.myvectors: curvect.plot() self.has_legend |= curvect.myprop.legendvisible self.has_image |= curvect.myprop.imagevisible if self.has_image: for curvect in self.myvectors: curvect.plot_image(sx, sy, xmin, ymin, xmax, ymax, size) if self.has_legend: for curvect in self.myvectors: curvect.plot_legend(sx, sy, xmin, ymin, xmax, ymax, size)
[docs] def reset_listogl(self): """ Reset OpenGL lists. Force deletion of the OpenGL list. If the object is newly plotted, the lists will be recreated. """ if self.idgllist != -99999: if is_opengl_context_available(): try: glDeleteLists(self.idgllist, 1) except Exception: logging.debug('Failed to delete OpenGL list %s for zone %s', self.idgllist, self.myname) self.idgllist = -99999
# ================================================================ # Matplotlib plotting # ================================================================
[docs] def plot_matplotlib(self, ax:plt.Axes | tuple[Figure, Axes] = None, xlim:tuple[float] | None = None, ylim:tuple[float] | None= None, **kwargs): """Plot the zone using Matplotlib. :param ax: Matplotlib Axes, ``(fig, ax)`` tuple, or None. :param xlim: Optional ``(xmin, xmax)`` bounds for legend clipping. :param ylim: Optional ``(ymin, ymax)`` bounds for legend clipping. :param kwargs: Additional keyword arguments. :return: ``(fig, ax)`` tuple. """ if isinstance(ax, tuple): fig, ax = ax elif ax is None: fig, ax = plt.subplots() else: fig = ax.figure # for curvect in self.myvectors: # curvect.plot_matplotlib(ax) list(map(lambda curvect: curvect.plot_matplotlib(ax, xlim, ylim), self.myvectors)) return fig, ax
[docs] def plot_linked_polygons(self, fig:Figure, ax:Axes, linked_arrays:dict, linked_vec:dict[str,"Zones"]=None, linestyle:str='-', onlymedian:bool=False, withtopography:bool = True, ds:float = None): """ Création d'un graphique sur base des polygones Chaque polygone se positionnera sur base de la valeur Z de ses vertices - façon conventionnelle de définir une longueur - ceci est normalement fait lors de l'appel à 'create_polygon_from_parallel' - si les polygones sont créés manuellement, il faut donc prendre soin de fournir l'information adhoc ou alors utiliser l'rgument 'ds' ATTENTION : Les coordonnées Z ne sont sauvegardées sur disque que si le fichier est 3D, autrement dit au format '.vecz' :param fig: Figure :param ax: Axes :param linked_arrays: dictionnaire contenant les matrices à lier -- les clés sont les labels :param linked_vec: dictionnaire contenant les instances Zones à lier -- Besoin d'une zone et d'un vecteur 'trace/trace' pour convertir les positions en coordonnées curvilignes :param linestyle: style de ligne :param onlymedian: affiche uniquement la médiane :param withtopography: affiche la topographie :param ds: pas spatial le long de l'axe """ colors=['red','blue','green','darkviolet','fuchsia','lime'] #Vérifie qu'au moins une matrice liée est fournie, sinon rien à faire exit=True for curlabel, curarray in linked_arrays.items(): if curarray.plotted: exit=False if exit: return k=0 zmin=99999. zmax=-99999. if ds is None: # Récupération des positions srefs=np.asarray([curpol.myvertices[0].z for curpol in self.myvectors]) else: # Création des positions sur base de 'ds' srefs=np.arange(0., float(self.nbvectors) * ds, ds) for idx, (curlabel, curarray) in enumerate(linked_arrays.items()): if curarray.plotted: logging.info(_('Plotting linked polygons for {}'.format(curlabel))) logging.info(_('Number of polygons : {}'.format(self.nbvectors))) logging.info(_('Extracting values inside polygons...')) vals= [curarray.get_values_insidepoly(curpol) for curpol in self.myvectors] logging.info(_('Computing stats...')) values = np.asarray([cur[0] for cur in vals],dtype=object) valel = np.asarray([cur[1] for cur in vals],dtype=object) zmaxloc=np.asarray([np.max(curval) if len(curval) >0 else -99999. for curval in values]) zminloc=np.asarray([np.min(curval) if len(curval) >0 else -99999. for curval in values]) zmax=max(zmax,np.max(zmaxloc[np.where(zmaxloc>-99999.)])) zmin=min(zmin,np.min(zminloc[np.where(zminloc>-99999.)])) if zmax>-99999: zloc = np.asarray([np.median(curpoly) if len(curpoly) >0 else -99999. for curpoly in values]) ax.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], color=colors[np.mod(k,3)], lw=2.0, linestyle=linestyle, label=curlabel+'_median') zloc = np.asarray([np.min(curpoly) if len(curpoly) >0 else -99999. for curpoly in values]) if not onlymedian: ax.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], color=colors[np.mod(k,3)],alpha=.3, lw=2.0, linestyle=linestyle, label=curlabel+'_min') zloc = np.asarray([np.max(curpoly) if len(curpoly) >0 else -99999. for curpoly in values]) ax.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], color=colors[np.mod(k,3)],alpha=.3, lw=2.0, linestyle=linestyle, label=curlabel+'_max') if withtopography and idx==0: if valel[0] is not None: zmaxloc=np.asarray([np.max(curval) if len(curval) >0 else -99999. for curval in valel]) zminloc=np.asarray([np.min(curval) if len(curval) >0 else -99999. for curval in valel]) zmax=max(zmax,np.max(zmaxloc[np.where(zmaxloc>-99999.)])) zmin=min(zmin,np.min(zminloc[np.where(zminloc>-99999.)])) if zmax>-99999: zloc = np.asarray([np.median(curpoly) if len(curpoly) >0 else -99999. for curpoly in valel]) ax.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], color='black', lw=2.0, linestyle=linestyle, label=curlabel+'_top_median') # if not onlymedian: # zloc = np.asarray([np.min(curpoly) for curpoly in valel]) # ax.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], # color='black',alpha=.3, # lw=2.0, # linestyle=linestyle, # label=curlabel+'_top_min') # zloc = np.asarray([np.max(curpoly) for curpoly in valel]) # ax.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], # color='black',alpha=.3, # lw=2.0, # linestyle=linestyle, # label=curlabel+'_top_max') k+=1 for curlabel, curzones in linked_vec.items(): curzones:Zones names = [curzone.myname for curzone in curzones.myzones] trace = None tracels = None logging.info(_('Plotting linked zones for {}'.format(curlabel))) curzone: zone if 'trace' in names: curzone = curzones.get_zone('trace') trace = curzone.get_vector('trace') if trace is None: if curzone is not None: if curzone.nbvectors>0: trace = curzone.myvectors[0] if trace is not None: tracels = trace.asshapely_ls() else: logging.warning(_('No trace found in the vectors {}'.format(curlabel))) break if ('marks' in names) or ('repères' in names): if ('marks' in names): curzone = curzones.myzones[names.index('marks')] else: curzone = curzones.myzones[names.index('repères')] logging.info(_('Plotting marks for {}'.format(curlabel))) logging.info(_('Number of marks : {}'.format(curzone.nbvectors))) for curvect in curzone.myvectors: curls = curvect.asshapely_ls() if curls.intersects(tracels): inter = curls.intersection(tracels) curs = float(tracels.project(inter)) ax.plot([curs, curs], [zmin, zmax], linestyle='--', label=curvect.myname) ax.text(curs, zmax, curvect.myname, fontsize=8, ha='center', va='bottom') if ('banks' in names) or ('berges' in names): if ('banks' in names): curzone = curzones.myzones[names.index('banks')] else: curzone = curzones.myzones[names.index('berges')] logging.info(_('Plotting banks for {}'.format(curlabel))) logging.info(_('Number of banks : {}'.format(curzone.nbvectors))) for curvect in curzone.myvectors: curvect: vector curproj = curvect.projectontrace(trace) sz = curproj.asnparray() ax.plot(sz[:,0], sz[:,1], label=curvect.myname) if ('bridges' in names) or ('ponts' in names): if ('bridges' in names): curzone = curzones.myzones[names.index('bridges')] else: curzone = curzones.myzones[names.index('ponts')] logging.info(_('Plotting bridges for {}'.format(curlabel))) for curvect in curzone.myvectors: curvect: vector curls = curvect.asshapely_ls() if curls.intersects(tracels): logging.info(_('Bridge {} intersects the trace'.format(curvect.myname))) inter = curls.intersection(tracels) curs = float(tracels.project(inter)) locz = np.asarray([vert.z for vert in curvect.myvertices]) zmin = np.amin(locz) zmax = np.amax(locz) ax.scatter(curs, zmin, label=curvect.myname + ' min') ax.scatter(curs, zmax, label=curvect.myname + ' max') ax.set_ylim(zmin,zmax) zmodmin= np.floor_divide(zmin*100,25)*25/100 ax.set_yticks(np.arange(zmodmin,zmax,.25)) fig.canvas.draw()
[docs] def plot_linked_polygons_wx(self, fig:MplFig, linked_arrays:dict, linked_vec:dict[str,"Zones"]=None, linestyle:str='-', onlymedian:bool=False, withtopography:bool = True, ds:float = None): """ Création d'un graphique sur base des polygones Chaque polygone se positionnera sur base de la valeur Z de ses vertices - façon conventionnelle de définir une longueur - ceci est normalement fait lors de l'appel à 'create_polygon_from_parallel' - si les polygones sont créés manuellement, il faut donc prendre soin de fournir l'information adhoc ou alors utiliser l'rgument 'ds' ATTENTION : Les coordonnées Z ne sont sauvegardées sur disque que si le fichier est 3D, autrement dit au format '.vecz' :param fig: Figure :param ax: Axes :param linked_arrays: dictionnaire contenant les matrices à lier -- les clés sont les labels :param linked_vec: dictionnaire contenant les instances Zones à lier -- Besoin d'une zone et d'un vecteur 'trace/trace' pour convertir les positions en coordonnées curvilignes :param linestyle: style de ligne :param onlymedian: affiche uniquement la médiane :param withtopography: affiche la topographie :param ds: pas spatial le long de l'axe """ colors=['red','blue','green','darkviolet','fuchsia','lime'] #Vérifie qu'au moins une matrice liée est fournie, sinon rien à faire exit=True for curlabel, curarray in linked_arrays.items(): if curarray.plotted: exit=False if exit: return k=0 zmin=99999. zmax=-99999. if ds is None: # Récupération des positions srefs=np.asarray([curpol.myvertices[0].z for curpol in self.myvectors]) else: # Création des positions sur base de 'ds' srefs=np.arange(0., float(self.nbvectors) * ds, ds) for idx, (curlabel, curarray) in enumerate(linked_arrays.items()): if curarray.plotted: logging.info(_('Plotting linked polygons for {}'.format(curlabel))) logging.info(_('Number of polygons : {}'.format(self.nbvectors))) logging.info(_('Extracting values inside polygons...')) vals= [curarray.get_values_insidepoly(curpol) for curpol in self.myvectors] logging.info(_('Computing stats...')) values = np.asarray([cur[0] for cur in vals],dtype=object) valel = np.asarray([cur[1] for cur in vals],dtype=object) zmaxloc=np.asarray([np.max(curval) if len(curval) >0 else -99999. for curval in values]) zminloc=np.asarray([np.min(curval) if len(curval) >0 else -99999. for curval in values]) zmax=max(zmax,np.max(zmaxloc[np.where(zmaxloc>-99999.)])) zmin=min(zmin,np.min(zminloc[np.where(zminloc>-99999.)])) if zmax>-99999: zloc = np.asarray([np.median(curpoly) if len(curpoly) >0 else -99999. for curpoly in values]) fig.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], color=colors[np.mod(k,3)], lw=2.0, linestyle=linestyle, label=curlabel+'_median') zloc = np.asarray([np.min(curpoly) if len(curpoly) >0 else -99999. for curpoly in values]) if not onlymedian: fig.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], color=colors[np.mod(k,3)],alpha=.3, lw=2.0, linestyle=linestyle, label=curlabel+'_min') zloc = np.asarray([np.max(curpoly) if len(curpoly) >0 else -99999. for curpoly in values]) fig.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], color=colors[np.mod(k,3)],alpha=.3, lw=2.0, linestyle=linestyle, label=curlabel+'_max') if withtopography and idx==0: if valel[0] is not None: zmaxloc=np.asarray([np.max(curval) if len(curval) >0 else -99999. for curval in valel]) zminloc=np.asarray([np.min(curval) if len(curval) >0 else -99999. for curval in valel]) zmax=max(zmax,np.max(zmaxloc[np.where(zmaxloc>-99999.)])) zmin=min(zmin,np.min(zminloc[np.where(zminloc>-99999.)])) if zmax>-99999: zloc = np.asarray([np.median(curpoly) if len(curpoly) >0 else -99999. for curpoly in valel]) fig.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], color='black', lw=2.0, linestyle=linestyle, label=curlabel+'_top_median') # if not onlymedian: # zloc = np.asarray([np.min(curpoly) for curpoly in valel]) # ax.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], # color='black',alpha=.3, # lw=2.0, # linestyle=linestyle, # label=curlabel+'_top_min') # zloc = np.asarray([np.max(curpoly) for curpoly in valel]) # ax.plot(srefs[np.where(zloc!=-99999.)],zloc[np.where(zloc!=-99999.)], # color='black',alpha=.3, # lw=2.0, # linestyle=linestyle, # label=curlabel+'_top_max') k+=1 for curlabel, curzones in linked_vec.items(): curzones:Zones names = [curzone.myname for curzone in curzones.myzones] trace = None tracels = None logging.info(_('Plotting linked zones for {}'.format(curlabel))) curzone: zone if 'trace' in names: curzone = curzones.get_zone('trace') trace = curzone.get_vector('trace') if trace is None: if curzone is not None: if curzone.nbvectors>0: trace = curzone.myvectors[0] if trace is not None: tracels = trace.asshapely_ls() else: logging.warning(_('No trace found in the vectors {}'.format(curlabel))) break if ('marks' in names) or ('repères' in names): if ('marks' in names): curzone = curzones.myzones[names.index('marks')] else: curzone = curzones.myzones[names.index('repères')] logging.info(_('Plotting marks for {}'.format(curlabel))) logging.info(_('Number of marks : {}'.format(curzone.nbvectors))) for curvect in curzone.myvectors: curls = curvect.asshapely_ls() if curls.intersects(tracels): inter = curls.intersection(tracels) curs = float(tracels.project(inter)) fig.plot([curs, curs], [zmin, zmax], linestyle='--', label=curvect.myname) fig.text(curs, zmax, curvect.myname, fontsize=8, ha='center', va='bottom') if ('banks' in names) or ('berges' in names): if ('banks' in names): curzone = curzones.myzones[names.index('banks')] else: curzone = curzones.myzones[names.index('berges')] logging.info(_('Plotting banks for {}'.format(curlabel))) logging.info(_('Number of banks : {}'.format(curzone.nbvectors))) for curvect in curzone.myvectors: curvect: vector curproj = curvect.projectontrace(trace) sz = curproj.asnparray() fig.plot(sz[:,0], sz[:,1], label=curvect.myname) if ('bridges' in names) or ('ponts' in names): if ('bridges' in names): curzone = curzones.myzones[names.index('bridges')] else: curzone = curzones.myzones[names.index('ponts')] logging.info(_('Plotting bridges for {}'.format(curlabel))) for curvect in curzone.myvectors: curvect: vector curls = curvect.asshapely_ls() if curls.intersects(tracels): logging.info(_('Bridge {} intersects the trace'.format(curvect.myname))) inter = curls.intersection(tracels) curs = float(tracels.project(inter)) locz = np.asarray([vert.z for vert in curvect.myvertices]) zmin = np.amin(locz) zmax = np.amax(locz) fig.plot(curs, zmin, label=curvect.myname + ' min', marker='x') fig.plot(curs, zmax, label=curvect.myname + ' max', marker='x') fig.cur_ax.set_ylim(zmin,zmax) zmodmin= np.floor_divide(zmin*100,25)*25/100 fig.cur_ax.set_yticks(np.arange(zmodmin,zmax,.25))
# ================================================================ # Geometric operations / Triangulation # ================================================================
[docs] def create_multibin(self, nb:int = None, nb2:int = 0) -> Triangulation: """Create a triangulation from the zone's vectors. If *nb* is not provided and a wx app is running, a dialog is shown. :param nb: Number of interpolation points along each polyline. :param nb2: Number of intermediate points between polylines. :return: A :class:`Triangulation` instance or *None*. """ wx_exists = wx.App.Get() is not None if nb is None and wx_exists: myls = [curv.asshapely_ls() for curv in self.myvectors] dlg = wx.NumberEntryDialog(None, _('How many points along polylines ?') + '\n' + _('Length size is {} meters').format(myls[0].length), 'nb', 'dl size', 100, 1, 10000) ret = dlg.ShowModal() if ret == wx.ID_CANCEL: dlg.Destroy() return None nb = int(dlg.GetValue()) dlg.Destroy() if nb2 == 0 and wx_exists: dlg = wx.NumberEntryDialog(None, _('How many points between two polylines ?'), 'nb2', 'perpendicular', 0, 0, 10000) ret = dlg.ShowModal() if ret == wx.ID_CANCEL: dlg.Destroy() return None nb2 = int(dlg.GetValue()) dlg.Destroy() return super().create_multibin(nb=nb, nb2=nb2)
[docs] def create_tri_crosssection(self, ds:float = 1.) -> Triangulation: """Create a triangulation from cross-section and support vectors. :param ds: Spacing used for the interpolation. :return: An :class:`Interpolators` instance or *None*. """ supports = [curv for curv in self.myvectors if curv.myname.startswith('support')] others = [curv for curv in self.myvectors if curv not in supports] if len(supports) ==0: logging.error(_('No support vector found')) return None if len(others) == 0: logging.error(_('No cross section vector found')) return None from ..PyCrosssections import Interpolators, crosssections, profile banks = self._make_zones(plotted=False) onezone = self._make_zone(name='support') banks.add_zone(onezone, forceparent=True) onezone.myvectors = supports cs = crosssections(plotted=False) for curprof in others: cs.add(curprof) cs.verif_bed() cs.find_minmax(True) cs.init_cloud() cs.sort_along(supports[0].asshapely_ls(), 'poly', downfirst = False) # cs.set_zones(True) interp = Interpolators(banks, cs, ds) interp.export_gltf() return interp
[docs] def create_constrainedDelaunay(self, nb:int = None) -> Triangulation: """Create a constrained Delaunay triangulation. If *nb* is not provided and a wx app is running, a dialog is shown. :param nb: Number of points along each polyline (0 to keep as-is). :return: A :class:`Triangulation` instance or *None*. """ wx_exists = wx.App.Get() is not None if nb is None and wx_exists: myls = [curv.linestring for curv in self.myvectors] meanlength = np.mean([curline.length for curline in myls]) dlg = wx.NumberEntryDialog(None, _('How many points along polylines ? (0 to use as it is)') + '\n' + _('Mean length size is {} meters').format(meanlength), 'nb', 'dl size', 100, 0, 10000) ret = dlg.ShowModal() if ret == wx.ID_CANCEL: dlg.Destroy() return None nb = int(dlg.GetValue()) dlg.Destroy() return super().create_constrainedDelaunay(nb=nb)
[docs] def createmultibin_proj(self, nb=None, nb2=0) -> Triangulation: """Create a triangulation by projection. If *nb* is not provided and a wx app is running, a dialog is shown. :param nb: Number of points along each polyline. :param nb2: Number of intermediate points between polylines. :return: A :class:`Triangulation` instance or *None*. """ wx_exists = wx.App.Get() is not None if nb is None and wx_exists: myls = [curv.asshapely_ls() for curv in self.myvectors] dlg = wx.NumberEntryDialog(None, _('How many points along polylines ?') + '\n' + _('Length size is {} meters').format(myls[0].length), 'nb', 'dl size', 100, 1, 10000) ret = dlg.ShowModal() if ret == wx.ID_CANCEL: dlg.Destroy() return None nb = int(dlg.GetValue()) dlg.Destroy() if nb2 == 0 and wx_exists: dlg = wx.NumberEntryDialog(None, _('How many points between two polylines ?'), 'nb2', 'perpendicular', 0, 0, 10000) ret = dlg.ShowModal() if ret == wx.ID_CANCEL: dlg.Destroy() return None nb2 = int(dlg.GetValue()) dlg.Destroy() return super().createmultibin_proj(nb=nb, nb2=nb2)
# ================================================================ # Polygon operations # ================================================================
[docs] def create_sliding_polygon_from_parallel(self, poly_length:float, ds_sliding:float, farthest_parallel:float, interval_parallel:float=None, howmanypoly:int = 1, ds:float = None): """Create sliding polygons from parallel vectors and refresh the view. :param poly_length: Length of each polygon along the trace. :param ds_sliding: Sliding step between successive polygons. :param farthest_parallel: Maximum distance for the farthest parallel. :param interval_parallel: Interval between parallels (None = auto). :param howmanypoly: Number of polygons to create. :param ds: Discretisation step for the vectors. """ result = super().create_sliding_polygon_from_parallel( poly_length=poly_length, ds_sliding=ds_sliding, farthest_parallel=farthest_parallel, interval_parallel=interval_parallel, howmanypoly=howmanypoly, ds=ds) if self.get_mapviewer() is not None: self.get_mapviewer().Paint() return result