"""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.
"""
# ================================================================
# 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.
"""
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