Source code for wolfhece.pyvertexvectors._vector

"""GUI-enabled vector class with OpenGL, matplotlib, and wx integration."""
from __future__ import annotations

import logging
import warnings
import copy
import struct
import io
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 matplotlib import cm
from matplotlib.colors import Colormap
from pathlib import Path
from typing import Union, Literal, TYPE_CHECKING
from shapely.geometry import LineString, MultiLineString, Point, MultiPoint, Polygon, JOIN_STYLE, MultiPolygon

if TYPE_CHECKING:
    from ._zone import zone
from shapely.ops import nearest_points
from shapely import prepare, is_prepared, destroy_prepared

from ..PyTranslate import _
from ..CpGrid import CpGrid
from ..PyVertex import wolfvertex, cloud_vertices
from ..color_constants import getRGBfromI, getIfromRGB, Colors, RGB
from ..wolf_texture import Text_Image_Texture, Text_Infos
from ..matplotlib_fig import Matplotlib_Figure as MplFig
from ..PyPalette import wolfpalette
from ._models import vectorModel, VectorOGLRenderer
from ._vectorproperties import vectorproperties

from matplotlib import font_manager

[docs] _FONTNAMES = [Path(font).stem.lower() for font in font_manager.findSystemFonts(fontpaths=None, fontext='ttf')]
[docs] class vector(vectorModel): """ Objet de gestion d'informations vectorielles (GUI). Hérite de :class:`vectorModel` 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=[], is2D=True, name='NoName', parentzone:"zone"=None, fromshapely=None, fromnumpy:np.ndarray = None, fromlist: list = None) -> None: """Initialise a GUI-enabled vector. :param lines: Raw text lines to parse (from a .vec file). :param is2D: Whether this is a 2-D vector. :param name: Vector name. :param parentzone: Parent zone owning this vector. :param fromshapely: Optional Shapely geometry to import. :param fromnumpy: Optional NumPy array of shape ``(n, 2|3)``. :param fromlist: Optional list of coordinate tuples. """ self.mytree = None
[docs] self.textimage:Text_Image_Texture = None
# Shader rendering cache
[docs] self._vbo_cache: np.ndarray | None = None
[docs] self._vbo_vertex_count: int = 0
[docs] self._fill_vbo_cache: np.ndarray | None = None
[docs] self._fill_vbo_vertex_count: int = 0
super().__init__(lines=lines, is2D=is2D, name=name, parentzone=parentzone, fromshapely=fromshapely, fromnumpy=fromnumpy, fromlist=fromlist) # ================================================================ # Factory overrides # ================================================================
[docs] def _make_properties(self, *args, **kwargs) -> "vectorproperties": """Factory: create GUI-enabled vector properties.""" return vectorproperties(*args, **kwargs)
[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.""" from ._zone import zone return zone(**kwargs)
[docs] def intersects_view_bounds(self, xmin: float = None, ymin: float = None, xmax: float = None, ymax: float = None) -> bool: """Return whether the vector should be considered visible in the current view. This is a rendering-oriented wrapper around the model's generic bbox intersection predicate. """ return self.intersects_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)
[docs] def legend_anchor_in_view_bounds(self, xmin: float = None, ymin: float = None, xmax: float = None, ymax: float = None) -> bool: """Return whether the legend anchor should be rendered in the current view.""" if not getattr(self.myprop, 'legendvisible', False): return False if any(val is None for val in (xmin, ymin, xmax, ymax)): return True try: legend_x = float(self.myprop.legendx) legend_y = float(self.myprop.legendy) except (TypeError, ValueError): return self.intersects_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) return xmin <= legend_x <= xmax and ymin <= legend_y <= ymax
[docs] def text_along_in_view_bounds(self, xmin: float = None, ymin: float = None, xmax: float = None, ymax: float = None) -> bool: """Return whether text-along should be rendered in the current view.""" if not getattr(self.myprop, 'text_along_enabled', False): return False return self.intersects_view_bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)
@classmethod
[docs] def make_from_shapely(cls, shapelyobj, **kwargs) -> "vector": """Factory method to create a vector from a Shapely geometry.""" newvec = cls(fromshapely=shapelyobj, **kwargs) return newvec
@classmethod
[docs] def make_from_numpy(cls, array: np.ndarray, **kwargs) -> "vector": """Factory method to create a vector from a NumPy array of shape (N, 2) or (N, 3).""" newvec = cls(fromnumpy=array, **kwargs) return newvec
@classmethod
[docs] def make_from_list(cls, lst: list, **kwargs) -> "vector": """Factory method to create a vector from a list of coordinate tuples.""" newvec = cls(fromlist=lst, **kwargs) return newvec
[docs] def find_nearest_vertex(self, x, y) -> wolfvertex | None: """Return the nearest vertex. The returned object is an existing vertex from this GUI vector. """ return super().find_nearest_vertex(x, y)
# ================================================================ # Properties / Callbacks # ================================================================ @vectorModel.closed.setter def closed(self, value: bool): """Set whether the vector is closed and refresh the properties UI.""" self.myprop.closed = value if self.myprop.myprops is not None: self.myprop.myprops.Populate()
[docs] def show_properties(self): """ Show the properties """ if self.myprop is not None: self.myprop.show()
[docs] def hide_properties(self): """ Hide the properties """ if self.myprop is not None: self.myprop.hide_properties()
# ================================================================ # State management # ================================================================
[docs] def use(self): """Mark the vector as used and check it in the tree.""" self.myprop.used=True if self.mytree is not None: self.mytree.CheckItem(self.myitem) self._on_vertices_changed()
[docs] def unuse(self): """ L'objet n'est plus à utiliser """ self.myprop.used=False if self.mytree is not None: self.mytree.UncheckItem(self.myitem) self._on_vertices_changed()
[docs] def _on_vertices_changed(self): """Invalidate cached geometries and the parent zone's OpenGL display list.""" super()._on_vertices_changed() self._vbo_cache = None self._vbo_vertex_count = 0 self._fill_vbo_cache = None self._fill_vbo_vertex_count = 0 if self.parentzone is not None: self.parentzone.reset_listogl()
# ================================================================ # wx GUI / Tree # ================================================================
[docs] def add2tree(self, tree:TreeListCtrl, root): """Add the vector 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) if self.myprop.used: tree.CheckItem(self.myitem)
# ================================================================ # wx GUI / Grid # ================================================================
[docs] def fillgrid(self, gridto:CpGrid): """Fill a wx CpGrid with vertex data. :param gridto: Target grid control. """ curv:wolfvertex gridto.SetColLabelValue(0,'X') gridto.SetColLabelValue(1,'Y') gridto.SetColLabelValue(2,'Z') gridto.SetColLabelValue(3,'value') gridto.SetColLabelValue(4,'s curvi') gridto.SetColLabelValue(5,'in use') nb=gridto.GetNumberRows() if len(self.myvertices)-nb>0: gridto.AppendRows(len(self.myvertices)-nb) k=0 for curv in self.myvertices: gridto.SetCellValue(k,0,str(curv.x)) gridto.SetCellValue(k,1,str(curv.y)) gridto.SetCellValue(k,2,str(curv.z)) gridto.SetCellValue(k,5,'1' if curv.in_use else '0') k+=1
[docs] def _fillgrid_only_i(self, gridto:CpGrid): """Fill only the in-use column of a wx CpGrid. :param gridto: Target grid control. """ curv:wolfvertex gridto.SetColLabelValue(0,'X') gridto.SetColLabelValue(1,'Y') gridto.SetColLabelValue(2,'Z') gridto.SetColLabelValue(3,'value') gridto.SetColLabelValue(4,'s curvi') gridto.SetColLabelValue(5,'in use') nb=gridto.GetNumberRows() if len(self.myvertices)-nb>0: gridto.AppendRows(len(self.myvertices)-nb) k=0 for curv in self.myvertices: gridto.SetCellValue(k, 5, '1' if curv.in_use else '0') k+=1
[docs] def updatefromgrid(self,gridfrom:CpGrid): """Update vertices from a wx CpGrid. :param gridfrom: Source grid control. """ curv:wolfvertex nbl=gridfrom.GetNumberRows() k=0 while k<nbl: x=gridfrom.GetCellValue(k,0) y=gridfrom.GetCellValue(k,1) z=gridfrom.GetCellValue(k,2) inuse = gridfrom.GetCellValue(k,5) if z=='': z=0. if x!='': if k<self.nbvertices: self.myvertices[k].x=float(x) self.myvertices[k].y=float(y) self.myvertices[k].z=float(z) self.myvertices[k].in_use = inuse=='1' else: newvert=wolfvertex(float(x),float(y),float(z)) self.add_vertex(newvert) k+=1 else: break while k<self.nbvertices: self.myvertices.pop(k) if self._linestring is not None or self._polygon is not None: self.prepare_shapely() self._on_vertices_changed()
# ================================================================ # OpenGL rendering # ================================================================
[docs] def _plot_square_at_vertices(self, size=5): """Plot small squares at each vertex using OpenGL. :param size: Point size in pixels. """ if self.nbvertices == 0: return curvert: wolfvertex ongoing = True glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) # if filled: # glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) glPointSize(size) rgb = getRGBfromI(self.myprop.color) glBegin(GL_POINTS) for curvert in self.myvertices: glColor3ub(int(rgb[0]), int(rgb[1]), int(rgb[2])) glVertex2f(curvert.x, curvert.y) glEnd() glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) ongoing = False
[docs] def _plot_index_vertex(self, idx:int = None, xy:tuple[float,float] = None, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None): """ Plot OpenGL :param idx: index of the vertex to plot :param xy: coordinates (x,y) of the vertex to plot :param sx: scale x :param sy: scale y :param xmin: minimum x :param ymin: minimum y :param xmax: maximum x :param ymax: maximum y :param size: size of the text """ if self.get_mapviewer() is None: logging.warning(_('No mapviewer available for legend plot')) return if xy is not None: x, y = xy curvert = self.find_nearest_vertex(x, y) idx = self.myvertices.index(curvert) elif idx is not None: if idx < 0 or idx >= self.nbvertices: logging.warning(_('Index {} out of range for vector {}').format(idx, self.myname)) return curvert = self.myvertices[idx] else: logging.warning(_('No index or coordinates provided for plotting index vertex')) return if not (xmin is None or ymin is None or xmax is None or ymax is None): if curvert.x < xmin or curvert.x > xmax or curvert.y < ymin or curvert.y > ymax: logging.debug(_('Vertex {} at ({},{}) is out of bounds ({},{},{},{}))'.format(idx, curvert.x, curvert.y, xmin, ymin, xmax, ymax))) return self._textimage = Text_Image_Texture(str(idx+1), self.get_mapviewer(), # mapviewer de l'instance Zones qui contient le vecteur self._get_textfont_idx(), self, curvert.x, curvert.y) self._textimage.paint()
[docs] def _plot_all_indices(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None): """ Plot all indices of the vertices using OpenGL :param sx: scale x :param sy: scale y :param xmin: minimum x :param ymin: minimum y :param xmax: maximum x :param ymax: maximum y :param size: size of the text """ if self.get_mapviewer() is None: logging.warning(_('No mapviewer available for legend plot')) return if self.nbvertices == 0: logging.warning(_('No vertices to plot indices for vector {}').format(self.myname)) return if self.myprop.used: for idx in range(self.nbvertices): self._plot_index_vertex(idx=idx, sx=sx, sy=sy, xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax, size=size)
[docs] def _plot_length2D_vertex(self, idx:int = None, xy:tuple[float,float] = None, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None): """ Plot OpenGL :param idx: index of the vertex to plot :param xy: coordinates (x,y) of the vertex to plot :param sx: scale x :param sy: scale y :param xmin: minimum x :param ymin: minimum y :param xmax: maximum x :param ymax: maximum y :param size: size of the text """ if self.get_mapviewer() is None: logging.warning(_('No mapviewer available for legend plot')) return if xy is not None: x, y = xy curvert = self.find_nearest_vertex(x, y) idx = self.myvertices.index(curvert) elif idx is not None: if idx < 0 or idx >= self.nbvertices: logging.warning(_('Index {} out of range for vector {}').format(idx, self.myname)) return curvert = self.myvertices[idx] else: logging.warning(_('No index or coordinates provided for plotting index vertex')) return if not (xmin is None or ymin is None or xmax is None or ymax is None): if curvert.x < xmin or curvert.x > xmax or curvert.y < ymin or curvert.y > ymax: logging.debug(_('Vertex {} at ({},{}) is out of bounds ({},{},{},{}))'.format(idx, curvert.x, curvert.y, xmin, ymin, xmax, ymax))) return length2D = self.linestring.project(Point(curvert.x, curvert.y)) self._textimage = Text_Image_Texture(f'{length2D:.2f}', self.get_mapviewer(), # mapviewer de l'instance Zones qui contient le vecteur self._get_textfont_idx(), self, curvert.x, curvert.y) self._textimage.paint()
[docs] def _plot_all_lengths2D(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None): """Plot 2-D curvilinear lengths at each vertex using OpenGL. :param sx: scale x :param sy: scale y :param xmin: minimum x :param ymin: minimum y :param xmax: maximum x :param ymax: maximum y :param size: size of the text """ if self.get_mapviewer() is None: logging.warning(_('No mapviewer available for legend plot')) return if self.nbvertices == 0: logging.warning(_('No vertices to plot length for vector {}').format(self.myname)) return if self.myprop.used: for idx in range(self.nbvertices): self._plot_length2D_vertex(idx=idx, sx=sx, sy=sy, xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax, size=size)
[docs] def _ensure_vbo_cache(self): """Rebuild the VBO data cache if invalidated.""" # Fillet (join_style==3) bakes geometry into the VBO, so we must # rebuild whenever fillet parameters change. fillet_key = (getattr(self.myprop, 'join_style', 0), getattr(self.myprop, 'join_size', 0.5), getattr(self.myprop, 'join_size_mode', 0)) if self._vbo_cache is None or getattr(self, '_vbo_fillet_key', None) != fillet_key: self._vbo_cache, self._vbo_vertex_count = self.build_shader_vbo_data() self._vbo_fillet_key = fillet_key
[docs] def build_fill_shader_vbo_data(self) -> tuple[np.ndarray, int]: """Build contiguous float32 triangle vertices for filled polygon shader. Each vertex is ``(x, y, edgeDist)`` where *edgeDist* is the normalised distance from the vertex to the nearest polygon boundary (0 = on the edge, 1 = at the deepest interior point). This attribute drives the cushion/pillow normal effect. """ if not self.myprop.filled: return np.array([], dtype=np.float32), 0 tris = [] all_polys = self.get_subpolygons() for curpoly in all_polys: if len(curpoly) < 3: continue if len(curpoly) == 3: tris.extend([ [curpoly[0].x, curpoly[0].y], [curpoly[1].x, curpoly[1].y], [curpoly[2].x, curpoly[2].y], ]) continue # Fallback fan triangulation for pre-simplified polygons. # Triangles produced by get_subpolygons remain the preferred path. p0 = curpoly[0] for i in range(1, len(curpoly) - 1): p1 = curpoly[i] p2 = curpoly[i + 1] tris.extend([ [p0.x, p0.y], [p1.x, p1.y], [p2.x, p2.y], ]) if len(tris) == 0: return np.array([], dtype=np.float32), 0 xy = np.asarray(tris, dtype=np.float64) # Compute distance to the polygon boundary for each vertex. try: boundary = self.polygon.boundary from shapely.geometry import MultiPoint pts = MultiPoint(xy.tolist()) dists = np.array([boundary.distance(p) for p in pts.geoms], dtype=np.float64) max_dist = dists.max() if max_dist > 1e-12: dists /= max_dist else: dists[:] = 0.0 except Exception: dists = np.zeros(len(xy), dtype=np.float64) # Build (x, y, edgeDist) array data = np.zeros((len(xy), 3), dtype=np.float32) data[:, 0] = xy[:, 0].astype(np.float32) data[:, 1] = xy[:, 1].astype(np.float32) data[:, 2] = dists.astype(np.float32) return np.ascontiguousarray(data), int(data.shape[0])
[docs] def _ensure_fill_vbo_cache(self): """Rebuild the fill VBO cache if invalidated.""" if self._fill_vbo_cache is None: self._fill_vbo_cache, self._fill_vbo_vertex_count = self.build_fill_shader_vbo_data()
[docs] def _resolve_fill_anim_geometry(self) -> tuple[tuple[float, float], float]: """Return the fill-animation centre and its max radius in world space.""" if self.nbvertices <= 0: return (0.0, 0.0), 1.0 center_index = int(getattr(self.myprop, 'fill_anim_center_index', 0)) if center_index < 0 or center_index >= self.nbvertices: center_index = 0 center_vertex = self.myvertices[center_index] center = (float(center_vertex.x), float(center_vertex.y)) max_radius = max( np.hypot(float(curvert.x) - center[0], float(curvert.y) - center[1]) for curvert in self.myvertices ) return center, float(max(max_radius, 1e-6))
[docs] def _should_plot_fill_anim_center_preview(self, mapviewer=None) -> bool: """Return whether the transient fill-animation centre preview should be shown.""" if not getattr(self.myprop, 'fill_anim_center_preview', False): return False if not getattr(self.myprop, 'filled', False): return False if getattr(self.myprop, 'fill_anim_mode', 0) not in (4, 5, 6): return False props_dlg = getattr(self.myprop, 'myprops', None) if props_dlg is None: return False try: if not props_dlg.IsShown(): return False except Exception: return False if mapviewer is None: mapviewer = self.get_mapviewer() if mapviewer is None: return False return getattr(mapviewer, 'active_vector', None) is self
[docs] def _plot_fill_anim_center_preview(self, size: float = 9.0): """Draw a transient OpenGL marker at the fill-animation centre.""" if not self._should_plot_fill_anim_center_preview(): return (center_x, center_y), radius = self._resolve_fill_anim_geometry() cross_half = max(radius * 0.03, 1.0) glDisable(GL_BLEND) glPointSize(float(size)) glColor3ub(255, 170, 0) glBegin(GL_POINTS) glVertex2f(center_x, center_y) glEnd() glLineWidth(2.0) glBegin(GL_LINES) glVertex2f(center_x - cross_half, center_y) glVertex2f(center_x + cross_half, center_y) glVertex2f(center_x, center_y - cross_half) glVertex2f(center_x, center_y + cross_half) glEnd() glLineWidth(1.0) glPointSize(1.0)
[docs] def plot_shader(self, mvp: np.ndarray, viewport: tuple[int, int], anim_phase: float = 0.0): """Plot the vector using the modern shader pipeline. :param mvp: 4×4 projection matrix (column-major float32). :param viewport: ``(width_px, height_px)`` of the viewport. :param anim_phase: Animation phase ``[0, 1]`` for effects. """ if not self.myprop.used: return if self.myprop.filled: self._ensure_fill_vbo_cache() if self._fill_vbo_vertex_count == 0: return from ..opengl.filled_polygon_shader2d import FilledPolygonShader2D shader_fill = FilledPolygonShader2D.get_instance() # Resolve fill colour: dedicated fill_color overrides main color fill_src = getattr(self.myprop, 'fill_color', None) if fill_src is None: fill_src = self.myprop.color rgb = getRGBfromI(fill_src) if self.myprop.transparent: color = (rgb[0] / 255., rgb[1] / 255., rgb[2] / 255., self.myprop.alpha / 255.) else: color = (rgb[0] / 255., rgb[1] / 255., rgb[2] / 255., 1.0) anim_mode = getattr(self.myprop, 'anim_mode', 0) fill_anim_mode = getattr(self.myprop, 'fill_anim_mode', anim_mode) fill_anim_center, fill_anim_radius = self._resolve_fill_anim_geometry() fill_anim_start_angle = float(getattr(self.myprop, 'fill_anim_start_angle', 90.0)) try: material = getattr(self.myprop, 'fill_pbr_material', None) logging.debug( 'plot_shader FILL: material present=%s, enabled=%s, metallic=%.2f, orm=%s', material is not None, material.enabled if material else 'N/A', material.metallic_factor if material else 0.0, bool(material.orm_texture) if material else False, ) cushion = float(getattr(material, 'cushion_strength', 0.0)) if material else 0.0 # Extract polygon boundary for per-fragment cushion distance boundary_pts = None cushion_max_dist = 1.0 if cushion > 0: try: poly = self.polygon coords = np.array(poly.exterior.coords[:-1], dtype=np.float64)[:, :2] # Simplify boundary to fit GLSL uniform limit (256 vec2) if len(coords) > 256: from shapely.geometry import LinearRing ring = LinearRing(coords) tol = np.sqrt(poly.area / max(len(coords), 1)) * 0.1 simplified = ring.simplify(tol) while len(simplified.coords) - 1 > 256 and tol < 1e6: tol *= 2.0 simplified = ring.simplify(tol) coords = np.array(simplified.coords[:-1], dtype=np.float64)[:, :2] boundary_pts = coords.astype(np.float32) # Max inscribed distance ≈ centroid-to-boundary cushion_max_dist = float(poly.boundary.distance(poly.centroid)) logging.debug( 'cushion: %d boundary pts, max_dist=%.3f', len(boundary_pts), cushion_max_dist) except Exception: pass shader_fill.draw_filled( vbo_data=self._fill_vbo_cache, vertex_count=self._fill_vbo_vertex_count, mvp=mvp, viewport=viewport, color=color, material=material, fill_anim_center=fill_anim_center, fill_anim_radius=fill_anim_radius, fill_anim_start_angle=fill_anim_start_angle, anim_phase=anim_phase, anim_mode=fill_anim_mode, cushion_strength=cushion, boundary_pts=boundary_pts, cushion_max_dist=cushion_max_dist, ) except Exception as exc: logging.warning( 'FILL SHADER FALLBACK: vector %s (vcount=%s) - ' 'falling back to list rendering. Error: %s', self.myname, self._fill_vbo_vertex_count, exc, exc_info=True, ) self.plot_list() return # Optional contour pass drawn on top of the fill if getattr(self.myprop, 'contour_enabled', False): self._ensure_vbo_cache() if self._vbo_vertex_count > 0: from ..opengl.polyline_shader2d import PolylineShader2D shader_line = PolylineShader2D.get_instance() cc_src = getattr(self.myprop, 'contour_color', None) if cc_src is None: cc_src = self.myprop.color cc_rgb = getRGBfromI(cc_src) if self.myprop.transparent: cc = (cc_rgb[0]/255., cc_rgb[1]/255., cc_rgb[2]/255., self.myprop.alpha/255.) else: cc = (cc_rgb[0]/255., cc_rgb[1]/255., cc_rgb[2]/255., 1.0) cw = getattr(self.myprop, 'contour_width', 1.0) try: shader_line.draw_polyline( vbo_data=self._vbo_cache, vertex_count=self._vbo_vertex_count, mvp=mvp, viewport=viewport, color=cc, line_width=float(cw), width_in_pixels=getattr(self.myprop, 'width_in_pixels', True), anim_phase=0.0, anim_mode=0, ) except Exception as exc: logging.warning('CONTOUR SHADER ERROR: %s - %s', self.myname, exc) return self._ensure_vbo_cache() if self._vbo_vertex_count == 0: return from ..opengl.polyline_shader2d import PolylineShader2D shader = PolylineShader2D.get_instance() rgb = getRGBfromI(self.myprop.color) if self.myprop.transparent: color = (rgb[0] / 255., rgb[1] / 255., rgb[2] / 255., self.myprop.alpha / 255.) else: color = (rgb[0] / 255., rgb[1] / 255., rgb[2] / 255., 1.0) # Determine animation mode anim_mode = getattr(self.myprop, 'anim_mode', 0) dash_enabled = getattr(self.myprop, 'dash_enabled', False) dash_length = getattr(self.myprop, 'dash_length', 10.0) gap_length = getattr(self.myprop, 'gap_length', 5.0) glow_enabled = getattr(self.myprop, 'glow_enabled', False) glow_width = getattr(self.myprop, 'glow_width', 0.4) glow_color = getattr(self.myprop, 'glow_color', (1.0, 1.0, 1.0, 0.4)) width_in_pixels = getattr(self.myprop, 'width_in_pixels', True) join_style = getattr(self.myprop, 'join_style', 0) join_size = getattr(self.myprop, 'join_size', 1.0) try: shader.draw_polyline( vbo_data=self._vbo_cache, vertex_count=self._vbo_vertex_count, mvp=mvp, viewport=viewport, color=color, line_width=float(self.myprop.width), width_in_pixels=width_in_pixels, dash_enabled=dash_enabled, dash_length=dash_length, gap_length=gap_length, glow_enabled=glow_enabled, glow_width=glow_width, glow_color=glow_color, anim_phase=anim_phase, anim_mode=anim_mode, join_style=join_style, join_size=join_size, ) except Exception as exc: logging.warning( 'SHADER FALLBACK: vector %s (vcount=%s, viewport=%s) - ' 'falling back to list rendering (1px lines in core profile). Error: %s', self.myname, self._vbo_vertex_count, viewport, exc, exc_info=True, ) self.plot_list()
[docs] def plot_list(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None): """Plot the vector using legacy OpenGL immediate mode / display lists. :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. """ if self.myprop.used: if self.myprop.filled: glPolygonMode(GL_FRONT_AND_BACK,GL_FILL) else: glPolygonMode(GL_FRONT_AND_BACK,GL_LINE) # Resolve fill / line colour independently fill_src = getattr(self.myprop, 'fill_color', None) if fill_src is None: fill_src = self.myprop.color rgb = getRGBfromI(fill_src if self.myprop.filled else self.myprop.color) if self.myprop.transparent: glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) # glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) else: glDisable(GL_BLEND) glLineWidth(float(self.myprop.width)) #glPointSize(float(self.myprop.width)) if self.myprop.transparent: glColor4ub(int(rgb[0]),int(rgb[1]),int(rgb[2]),int(self.myprop.alpha)) else: glColor3ub(int(rgb[0]),int(rgb[1]),int(rgb[2])) if self.myprop.filled: # ls = self.polygon if False: #FIXME : Shapely have not constrained Delaunay triangulation -- using Delaunay from Wolf Fortran instead ls = ls.segmentize(.1) delaunay = delaunay_triangles(ls) for curpol in delaunay.geoms: if ls.contains(curpol.centroid): glBegin(GL_POLYGON) for curvert in curpol.exterior.coords: glVertex2d(curvert[0],curvert[1]) glEnd() else: logging.debug(_('Polygon not in Polygon')) else: # #En attendant de lier WOLF-Fortran, on utilise la triangulation contrainte de la librairie Triangle -- https://rufat.be/triangle/ # xx, yy = ls.exterior.xy # # On translate les coordonnées pour éviter les erreurs de triangulation # tr_x = np.array(xx).min() # tr_y = np.array(yy).min() # xx = np.array(xx)-tr_x # yy = np.array(yy)-tr_y # geom = {'vertices' : [[x,y] for x,y in zip(xx[:-1],yy[:-1])], 'segments' : [[i,i+1] for i in range(len(xx)-2)]+[[len(xx)-2,0]]} # try: # delaunay = triangle.triangulate(geom, 'p') # for curtri in delaunay['triangles']: # glBegin(GL_POLYGON) # for i in range(3): # # on retraduit les coordonnées pour revenir dans le monde réel # glVertex2d(delaunay['vertices'][curtri[i]][0] + tr_x, delaunay['vertices'][curtri[i]][1] + tr_y) # glEnd() # except: # pass all_polys = self.get_subpolygons() for curpoly in all_polys: glBegin(GL_POLYGON) for curvertex in curpoly: glVertex2d(curvertex.x, curvertex.y) glEnd() else: all_polys = self.get_subpolygons() glEnable(GL_LINE_SMOOTH) glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) if not self.myprop.transparent: glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) for curpoly in all_polys: glBegin(GL_LINE_STRIP) for curvertex in curpoly: glVertex2d(curvertex.x, curvertex.y) glEnd() glDisable(GL_LINE_SMOOTH) # Optional legacy contour pass on filled polygons if self.myprop.filled and getattr(self.myprop, 'contour_enabled', False): cc_src = getattr(self.myprop, 'contour_color', None) if cc_src is None: cc_src = self.myprop.color cc_rgb = getRGBfromI(cc_src) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) cw = getattr(self.myprop, 'contour_width', 1.0) glLineWidth(float(cw)) if self.myprop.transparent: glColor4ub(int(cc_rgb[0]), int(cc_rgb[1]), int(cc_rgb[2]), int(self.myprop.alpha)) else: glColor3ub(int(cc_rgb[0]), int(cc_rgb[1]), int(cc_rgb[2])) all_polys = self.get_subpolygons() for curpoly in all_polys: glBegin(GL_LINE_STRIP) for curvertex in curpoly: glVertex2d(curvertex.x, curvertex.y) glEnd() glPolygonMode(GL_FRONT_AND_BACK,GL_LINE) glDisable(GL_BLEND) glLineWidth(1.0)
[docs] def plot(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None, rendering_machine=None, mvp=None, viewport=None, anim_phase=0.0): """Plot the vector using OpenGL (dispatcher). When *rendering_machine* is ``VectorOGLRenderer.SHADER``, delegates to :meth:`plot_shader`. Otherwise falls back to the legacy :meth:`plot_list` path. :param rendering_machine: :class:`VectorOGLRenderer` enum value. :param mvp: 4×4 projection matrix (only for shader mode). :param viewport: ``(width_px, height_px)`` (only for shader mode). :param anim_phase: Animation phase ``[0, 1]`` (only for shader mode). """ if rendering_machine == VectorOGLRenderer.SHADER and mvp is not None and viewport is not None: self.plot_shader(mvp=mvp, viewport=viewport, anim_phase=anim_phase) else: self.plot_list(sx=sx, sy=sy, xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax, size=size)
[docs] def plot_legend(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None, rendering_machine=None, mvp=None, viewport=None, anim_phase=0.0): """Plot the legend as an OpenGL image texture or via the SDF atlas shader. :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 rendering_machine: ``VectorOGLRenderer.SHADER`` for atlas path. :param mvp: 4×4 MVP matrix (required for shader path). :param viewport: ``(width, height)`` in pixels (required for shader path). :param anim_phase: Animation phase ``[0, 1]`` for shader path. """ if self.get_mapviewer() is None: logging.warning(_('No mapviewer available for legend plot')) return if not (self.myprop.legendvisible and self.myprop.used): self.textimage = None return if rendering_machine == VectorOGLRenderer.SHADER and mvp is not None and viewport is not None: self.plot_legend_shader(mvp=mvp, viewport=viewport, anim_phase=anim_phase) else: self.textimage = Text_Image_Texture(self.myprop.legendtext, self.get_mapviewer(), self._get_textfont(), self, self.myprop.legendx, self.myprop.legendy) self.textimage.paint()
[docs] def plot_legend_shader(self, mvp: np.ndarray, viewport: tuple[int, int], anim_phase: float = 0.0): """Render the legend using the SDF glyph atlas shader. :param mvp: 4×4 MVP matrix (column-major float32). :param viewport: ``(width_px, height_px)``. :param anim_phase: Animation cycling phase ``[0, 1]``. """ from ..opengl.text_renderer2d import TextRenderer2D text = self.myprop.legendtext if not text: return r, g, b = getRGBfromI(self.myprop.legendcolor) color = (r / 255.0, g / 255.0, b / 255.0, 1.0) # Priority modes: 1=width, 2=height, 3=fontsize if self.myprop.legendpriority == 3: # FONTSIZE priority — use pixel-size mode. size_in_pixels = True font_size = float(self.myprop.legendfontsize) world_width = None world_height = None elif self.myprop.legendpriority == 1: # WIDTH priority — derive world scale from requested width. size_in_pixels = False font_size = 0.0 world_width = float(self.myprop.legendlength) world_height = None else: # HEIGHT priority — world-size mode driven by target height. size_in_pixels = False font_size = 0.0 world_width = None world_height = float(self.myprop.legendheight) tr = TextRenderer2D.get_instance() tr.draw_text( text, self.myprop.legendx, self.myprop.legendy, mvp, viewport, font_name=self.myprop.legendfontname, font_size=font_size, color=color, size_in_pixels=size_in_pixels, world_height=world_height, world_width=world_width, rotation=self.myprop.legendorientation, alignment=self.myprop.legend_alignment, line_spacing=self.myprop.legend_line_spacing, smoothing=self.myprop.legend_smoothing, glow_enabled=self.myprop.legend_glow_enabled, glow_width=self.myprop.legend_glow_width, glow_color=self.myprop.legend_glow_color, anim_mode=self.myprop.legend_anim_mode, anim_phase=anim_phase, anim_speed=self.myprop.legend_anim_speed, )
[docs] def plot_text_along_polyline(self, mvp: np.ndarray, viewport: tuple[int, int], anim_phase: float = 0.0): """Render text that follows the polyline path using the SDF atlas. Each character is oriented along the local tangent of the polyline. Controlled by ``myprop.text_along_*`` properties. :param mvp: 4×4 MVP matrix (column-major float32). :param viewport: ``(width_px, height_px)``. :param anim_phase: Animation cycling phase ``[0, 1]``. """ if not self.myprop.text_along_enabled: return text = self.myprop.text_along_text if not text: return if self.nbvertices < 2: return from ..opengl.text_renderer2d import TextRenderer2D pts = self.xy # (N, 2) cum = np.asarray(self.s_curvi, dtype=np.float64) # (N,) r, g, b = getRGBfromI(self.myprop.legendcolor) color = (r / 255.0, g / 255.0, b / 255.0, 1.0) # Priority modes: 1=width, 2=height, 3=fontsize if self.myprop.text_along_priority == 3: size_in_pixels = True font_size = float(self.myprop.text_along_font_size) world_width = None world_height = None elif self.myprop.text_along_priority == 1: size_in_pixels = False font_size = 0.0 world_width = float(self.myprop.text_along_world_width) world_height = None else: size_in_pixels = False font_size = 0.0 world_width = None world_height = float(self.myprop.text_along_world_height) tr = TextRenderer2D.get_instance() tr.draw_text_along_polyline( text, pts, cum, mvp, viewport, font_name=self.myprop.legendfontname, font_size=font_size, color=color, size_in_pixels=size_in_pixels, world_height=world_height, world_width=world_width, offset_along=self.myprop.text_along_offset, offset_perp=self.myprop.text_along_perp, alignment=self.myprop.text_along_alignment, smoothing=self.myprop.legend_smoothing, glow_enabled=self.myprop.legend_glow_enabled, glow_width=self.myprop.legend_glow_width, glow_color=self.myprop.legend_glow_color, anim_mode=self.myprop.legend_anim_mode, anim_phase=anim_phase, anim_speed=self.myprop.legend_anim_speed, )
[docs] def plot_tracking_label(self, mvp: np.ndarray, viewport: tuple[int, int], mouse_x: float, mouse_y: float, format_func=None): """Render a dynamic label snapped to the polyline at mouse position. The label content (by default the curvilinear distance) follows the mouse cursor projected onto the polyline. A small marker is drawn at the snap point on the polyline. :param mvp: 4×4 MVP matrix (column-major float32). :param viewport: ``(width_px, height_px)``. :param mouse_x: Mouse X in world coordinates. :param mouse_y: Mouse Y in world coordinates. :param format_func: ``(curvi_dist, total_length) -> str``. """ if not self.myprop.tracking_label_enabled: return if self.nbvertices < 2: return from ..opengl.text_renderer2d import TextRenderer2D, snap_to_polyline pts = self.xy cum = np.asarray(self.s_curvi, dtype=np.float64) # Single snap computation — reused for both marker and text snap_x, snap_y, curvi, total, tang_x, tang_y = snap_to_polyline( mouse_x, mouse_y, pts, cum) # Check snap radius — skip entirely if too far (squared distance avoids sqrt) snap_radius = self.myprop.tracking_label_snap_radius if snap_radius is not None: dx = snap_x - mouse_x dy = snap_y - mouse_y if dx * dx + dy * dy > snap_radius * snap_radius: return r, g, b = getRGBfromI(self.myprop.legendcolor) color = (r / 255.0, g / 255.0, b / 255.0, 1.0) # Priority modes: 1=width, 2=height, 3=fontsize if self.myprop.tracking_label_priority == 3: size_in_pixels = True font_size = float(self.myprop.tracking_label_font_size) world_width = None world_height = None elif self.myprop.tracking_label_priority == 1: size_in_pixels = False font_size = 0.0 world_width = float(self.myprop.tracking_label_world_width) world_height = None else: size_in_pixels = False font_size = 0.0 world_width = None world_height = float(self.myprop.tracking_label_world_height) # Draw snap-point marker (minimal immediate-mode GL) glPointSize(8.0) glBegin(GL_POINTS) glColor4f(color[0], color[1], color[2], color[3]) glVertex2f(snap_x, snap_y) glEnd() # Small cross for visibility if world_height is not None and world_height > 0: half = world_height * 0.4 else: try: m00 = mvp[0, 0] if mvp.shape == (4, 4) else 1.0 half = 5.0 / (abs(m00) * viewport[0] * 0.5) if m00 != 0 else 1.0 except Exception: half = 1.0 glLineWidth(2.0) glBegin(GL_LINES) glColor4f(color[0], color[1], color[2], color[3]) glVertex2f(snap_x - half, snap_y) glVertex2f(snap_x + half, snap_y) glVertex2f(snap_x, snap_y - half) glVertex2f(snap_x, snap_y + half) glEnd() glLineWidth(1.0) glPointSize(1.0) # Format label text from pre-computed snap data import math if format_func is not None: label = format_func(curvi, total) else: label = f"d = {curvi:.2f} m" if self.myprop.tracking_label_horizontal: rotation = 0.0 else: rotation = math.degrees(math.atan2(tang_y, tang_x)) # Apply perpendicular offset offset_perp = self.myprop.text_along_perp nx, ny = -tang_y, tang_x draw_x = snap_x + nx * offset_perp draw_y = snap_y + ny * offset_perp # Draw text directly — no second snap_to_polyline tr = TextRenderer2D.get_instance() tr.draw_text( label, draw_x, draw_y, mvp, viewport, font_name=self.myprop.legendfontname, font_size=font_size, color=color, size_in_pixels=size_in_pixels, world_height=world_height, world_width=world_width, rotation=rotation, alignment='left', smoothing=self.myprop.legend_smoothing, glow_enabled=True, glow_width=0.2, glow_color=(0.0, 0.0, 0.0, 0.8), )
[docs] def plot_image(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None): """Plot attached images as OpenGL textures. :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. """ if self.get_mapviewer() is None: logging.warning(_('No mapviewer available for image plot')) return if self.myprop.imagevisible and self.myprop.used: try: if self.myprop.textureimage is None: self.myprop.load_unload_image() if self.myprop.textureimage is not None: self.myprop.textureimage.paint() else: logging.warning(_('No image texture available for plot')) except Exception as e: logging.error(_('Error while plotting image texture: {}').format(e))
# ================================================================ # Matplotlib plotting # ================================================================
[docs] def plot_legend_mpl(self, ax:plt.Axes, xlim:tuple[float] | None = None, ylim:tuple[float] | None= None): """Plot the legend on a Matplotlib Axes. :param ax: Target Matplotlib Axes. :param xlim: Optional ``(xmin, xmax)`` bounds to skip off-screen legends. :param ylim: Optional ``(ymin, ymax)`` bounds to skip off-screen legends. """ if self.myprop.legendvisible and self.myprop.used: if xlim is not None and (self.myprop.legendx < xlim[0] or self.myprop.legendx > xlim[1]): logging.debug(_('Legend x position {} is out of xlim bounds ({}, {}) -- not plotting legend').format(self.myprop.legendx, xlim[0], xlim[1])) return if ylim is not None and (self.myprop.legendy < ylim[0] or self.myprop.legendy > ylim[1]): logging.debug(_('Legend y position {} is out of ylim bounds ({}, {}) -- not plotting legend').format(self.myprop.legendy, ylim[0], ylim[1])) return fontname = self.myprop.legendfontname.replace('"','').lower() if fontname not in _FONTNAMES: fontname = 'DejaVu Sans' # Default Matplotlib font rgb = getRGBfromI(self.myprop.legendcolor) ax.text(self.myprop.legendx, self.myprop.legendy, self.myprop.legendtext, fontsize=self.myprop.legendfontsize, fontname=fontname, color=(rgb[0]/255,rgb[1]/255,rgb[2]/255), rotation=self.myprop.legendorientation, ha='center', va='center')
[docs] def plot_matplotlib(self, ax:plt.Axes | tuple[Figure, Axes] = None, xlim:tuple[float] | None = None, ylim:tuple[float] | None= None): """Plot the vector using Matplotlib (XY coordinates only). :param ax: Matplotlib Axes, ``(fig, ax)`` tuple, or None to create one. :param xlim: Optional ``(xmin, xmax)`` bounds for legend clipping. :param ylim: Optional ``(ymin, ymax)`` bounds for legend clipping. :return: ``(fig, ax)`` tuple. """ if isinstance(ax, tuple): # if ax is a tuple, we assume it is (fig, ax) fig, ax = ax elif ax is None: fig, ax = plt.subplots() else: fig = ax.figure if self.myprop.used: if self.myprop.filled: rgb=getRGBfromI(self.myprop.color) subpoly = self.get_subpolygons() for curpoly in subpoly: if self.myprop.transparent: ax.fill([curvert.x for curvert in curpoly], [curvert.y for curvert in curpoly], color=(rgb[0]/255.,rgb[1]/255.,rgb[2]/255.,self.myprop.alpha)) else: ax.fill([curvert.x for curvert in curpoly], [curvert.y for curvert in curpoly], color=(rgb[0]/255.,rgb[1]/255.,rgb[2]/255.)) else: rgb=getRGBfromI(self.myprop.color) subpoly = self.get_subpolygons() for curpoly in subpoly: if self.myprop.transparent: ax.plot([curvert.x for curvert in curpoly], [curvert.y for curvert in curpoly], color=(rgb[0]/255.,rgb[1]/255.,rgb[2]/255.,self.myprop.alpha), linewidth=self.myprop.width) else: ax.plot([curvert.x for curvert in curpoly], [curvert.y for curvert in curpoly], color=(rgb[0]/255.,rgb[1]/255.,rgb[2]/255.), linewidth=self.myprop.width) self.plot_legend_mpl(ax, xlim=xlim, ylim=ylim) return fig, ax
[docs] def plot_matplotlib_sz(self, ax:plt.Axes | tuple[Figure, Axes] = None): """ Plot Matplotlib - SZ coordinates ONLY. S is the curvilinear abscissa, Z is the elevation. :param ax: Matplotlib Axes to plot on or a tuple (fig, ax) where fig is the figure and ax is the axes. If ax is None, a new figure and axes will be created. """ if isinstance(ax, tuple): # if ax is a tuple, we assume it is (fig, ax) fig, ax = ax elif ax is None: fig, ax = plt.subplots() else: fig = ax.figure if self.myprop.used: s,z = self.sz_curvi rgb=getRGBfromI(self.myprop.color) if self.myprop.transparent: ax.plot(s, z, color=(rgb[0]/255., rgb[1]/255., rgb[2]/255., self.myprop.alpha), linewidth=self.myprop.width) else: ax.plot(s, z, color=(rgb[0]/255., rgb[1]/255., rgb[2]/255.), linewidth=self.myprop.width) return fig, ax
[docs] def plot_linked(self, fig, ax, linked_arrays:dict): """ Plot linked array values on a Matplotlib figure using a vector as a trace. :param fig: Matplotlib Figure to plot on :param ax: Matplotlib Axes to plot on :param linked_arrays: Dictionary of linked arrays to plot, with the label as key and the array as value The linked arrays can be a WolfArray or a Wolfresults_2D, as long as they have the method get_value(x,y) to retrieve the value at coordinates (x,y) and the method get_dxdy_min() to get the minimum grid spacing used for vector subdivision. If you call this method by script, ensure that plotted=True for the linked arrays you want to plot, otherwise they will not be displayed. """ colors = ['black', 'red', 'blue', 'green', 'orange', 'purple', 'cyan', 'magenta'] exit=True for curlabel, curarray in linked_arrays.items(): if curarray.plotted: exit=False if exit: logging.warning(_('No plotted linked arrays')) return k=0 myls = self.linestring length = myls.length tol=length/10. ax.set_xlim(0-tol,length+tol) zmin=99999. zmax=-99999. nullvalue = -99999 for curlabel, curarray in linked_arrays.items(): if curarray.plotted: ds = curarray.get_dxdy_min() nb = int(np.ceil(length/ds*2)) alls = np.linspace(0,int(length),nb) pts = [myls.interpolate(curs) for curs in alls] allz = np.asarray([curarray.get_value(curpt.x,curpt.y, nullvalue= nullvalue) for curpt in pts]) zmaxloc=np.max(allz[allz!=nullvalue]) zminloc=np.min(allz[allz!=nullvalue]) zmax=max(zmax,zmaxloc) zmin=min(zmin,zminloc) if np.max(allz)>nullvalue: # select parts if nullvalue in allz: # find all parts separated by nullvalue nulls = np.argwhere(allz==nullvalue) nulls = np.insert(nulls,0,-1) nulls = np.append(nulls,len(allz)) addlabel = True for i in range(len(nulls)-1): if nulls[i+1]-nulls[i]>1: ax.plot(alls[nulls[i]+1:nulls[i+1]],allz[nulls[i]+1:nulls[i+1]], color=colors[np.mod(k,len(colors))], lw=2.0, label=curlabel if addlabel else None) addlabel = False else: ax.plot(alls,allz, color=colors[np.mod(k,len(colors))], lw=2.0, label=curlabel) k+=1 ax.set_ylim(zmin,zmax) ax.legend() ax.grid() fig.canvas.draw() return fig,ax
[docs] def plot_linked_difference(self, fig, ax, linked_arrays:dict): """Plot differences of linked array values along the vector trace. The first plotted array is used as reference; subsequent arrays are displayed as ``value - reference``. :param fig: Matplotlib Figure. :param ax: Matplotlib Axes. :param linked_arrays: ``{label: array}`` dictionary. """ colors = ['black', 'red', 'blue', 'green', 'orange', 'purple', 'cyan', 'magenta'] exit=True for curlabel, curarray in linked_arrays.items(): if curarray.plotted: exit=False if exit: logging.warning(_('No plotted linked arrays')) return k=0 myls = self.linestring length = myls.length tol=length/10. ax.set_xlim(0-tol, length+tol) zmin=99999. zmax=-99999. nullvalue = -99999 for curlabel, curarray in linked_arrays.items(): if curarray.plotted: ref_array = curarray ds = curarray.get_dxdy_min() nb = int(np.ceil(length/ds*2)) alls = np.linspace(0,int(length),nb) pts = [myls.interpolate(curs) for curs in alls] allz_ref = np.asarray([curarray.get_value(curpt.x,curpt.y, nullvalue= nullvalue) for curpt in pts]) break for curlabel, curarray in linked_arrays.items(): if curarray.plotted: ds = curarray.get_dxdy_min() nb = int(np.ceil(length/ds*2)) alls = np.linspace(0,int(length),nb) pts = [myls.interpolate(curs) for curs in alls] allz = np.asarray([curarray.get_value(curpt.x,curpt.y, nullvalue= nullvalue) for curpt in pts]) allz -= allz_ref zmaxloc=np.max(allz[allz!=nullvalue]) zminloc=np.min(allz[allz!=nullvalue]) zmax=max(zmax,zmaxloc) zmin=min(zmin,zminloc) if np.max(allz)>nullvalue: # select parts if nullvalue in allz: # find all parts separated by nullvalue nulls = np.argwhere(allz==nullvalue) nulls = np.insert(nulls,0,-1) nulls = np.append(nulls,len(allz)) addlabel = True for i in range(len(nulls)-1): if nulls[i+1]-nulls[i]>1: ax.plot(alls[nulls[i]+1:nulls[i+1]],allz[nulls[i]+1:nulls[i+1]], color=colors[np.mod(k,len(colors))], lw=2.0, label=curlabel if addlabel else None) addlabel = False else: ax.plot(alls,allz, color=colors[np.mod(k,len(colors))], lw=2.0, label=curlabel) k+=1 ax.set_ylim(-2., 2.) ax.legend() ax.grid() fig.canvas.draw() return fig,ax
[docs] def plot_linked_wx(self, fig:MplFig, linked_arrays:dict): """Plot linked array values using wxPython Matplotlib figure. :param fig: wxPython Matplotlib figure wrapper. :param linked_arrays: ``{label: array}`` dictionary. """ colors = ['black', 'red', 'blue', 'green', 'orange', 'purple', 'cyan', 'magenta'] exit=True for curlabel, curarray in linked_arrays.items(): if curarray.plotted: exit=False if exit: return k=0 myls = self.asshapely_ls() length = myls.length tol=length/10. fig.cur_ax.set_xlim(0-tol,length+tol) zmin=99999. zmax=-99999. nullvalue = -99999 for curlabel, curarray in linked_arrays.items(): if curarray.plotted: ds = curarray.get_dxdy_min() nb = int(np.ceil(length/ds*2)) alls = np.linspace(0,int(length),nb) pts = [myls.interpolate(curs) for curs in alls] allz = np.asarray([curarray.get_value(curpt.x,curpt.y, nullvalue= nullvalue) for curpt in pts]) zmaxloc=np.max(allz[allz!=nullvalue]) zminloc=np.min(allz[allz!=nullvalue]) zmax=max(zmax,zmaxloc) zmin=min(zmin,zminloc) if np.max(allz)>nullvalue: # select parts if nullvalue in allz: # find all parts separated by nullvalue nulls = np.argwhere(allz==nullvalue) nulls = np.insert(nulls,0,-1) nulls = np.append(nulls,len(allz)) addlabel = True for i in range(len(nulls)-1): if nulls[i+1]-nulls[i]>1: fig.plot(alls[nulls[i]+1:nulls[i+1]],allz[nulls[i]+1:nulls[i+1]], color=colors[np.mod(k,len(colors))], lw=2.0, label=curlabel if addlabel else None) addlabel = False else: fig.plot(alls,allz, color=colors[np.mod(k,len(colors))], lw=2.0, label=curlabel) k+=1 fig.cur_ax.set_ylim(zmin,zmax) fig.cur_ax.legend() fig.cur_ax.grid() return fig
[docs] def update_linked_wx(self, fig:MplFig, linked_arrays:dict): """Update an existing wxPython Matplotlib figure with new linked array values. :param fig: wxPython Matplotlib figure wrapper. :param linked_arrays: ``{label: array}`` dictionary. """ k=0 length = self.linestring.length tol=length/10. fig.cur_ax.set_xlim(0-tol,length+tol) zmin=99999. zmax=-99999. nullvalue = -99999 for curlabel, curarray in linked_arrays.items(): if curarray.plotted: ds = curarray.get_dxdy_min() nb = int(np.ceil(length/ds*2)) alls = np.linspace(0,int(length),nb) pts = [self.linestring.interpolate(curs) for curs in alls] allz = np.asarray([curarray.get_value(curpt.x,curpt.y, nullvalue= nullvalue) for curpt in pts]) zmaxloc=np.max(allz[allz!=nullvalue]) zminloc=np.min(allz[allz!=nullvalue]) zmax=max(zmax,zmaxloc) zmin=min(zmin,zminloc) if np.max(allz)>nullvalue: # select parts if nullvalue in allz: # find all parts separated by nullvalue nulls = np.argwhere(allz==nullvalue) nulls = np.insert(nulls,0,-1) nulls = np.append(nulls,len(allz)) addlabel = True for i in range(len(nulls)-1): if nulls[i+1]-nulls[i]>1: fig.update_line_from_xy(k + i, np.vstack((alls[nulls[i]+1:nulls[i+1]], allz[nulls[i]+1:nulls[i+1]])).T) else: fig.update_line_from_xy(k, np.vstack((alls, allz)).T) k+=1 return fig
[docs] def plot_mpl(self, show=False, forceaspect=True, fig:Figure=None, ax:Axes=None, labels:dict={}, clear_ax:bool =True): """Plot the vector in Matplotlib (SZ coordinates only). .. deprecated:: Use :meth:`plot_matplotlib_sz` instead. :param show: Whether to call ``fig.show()``. :param forceaspect: Whether to force the aspect ratio. :param fig: Existing Figure, or None to create one. :param ax: Existing Axes, or None to create one. :param labels: Dict with optional *title*, *xlabel*, *ylabel*. :param clear_ax: Whether to clear the axes before plotting. :return: ``(fig, ax)`` tuple. """ warnings.warn("plot_mpl is deprecated, use plot_matplotlib_sz instead", DeprecationWarning, stacklevel=2) x,y=self.get_sz() xmin=x[0] xmax=x[-1] ymin=np.min(y) ymax=np.max(y) if ax is None: redraw=False fig = plt.figure() ax=fig.add_subplot(111) else: redraw=True if clear_ax: # Clear the axes if specified ax.cla() if 'title' in labels.keys(): ax.set_title(labels['title']) if 'xlabel' in labels.keys(): ax.set_xlabel(labels['xlabel']) if 'ylabel' in labels.keys(): ax.set_ylabel(labels['ylabel']) if ymax>-99999.: dy=ymax-ymin ymin-=dy/4. ymax+=dy/4. ax.plot(x,y,color='black', lw=2.0, label=self.myname) ax.legend() tol=(xmax-xmin)/10. ax.set_xlim(xmin-tol,xmax+tol) ax.set_ylim(ymin,ymax) if forceaspect: aspect=1.0*(ymax-ymin)/(xmax-xmin)*(ax.get_xlim()[1] - ax.get_xlim()[0]) / (ax.get_ylim()[1] - ax.get_ylim()[0]) ax.set_aspect(aspect) if show: fig.show() if redraw: fig.canvas.draw() return fig,ax
# ================================================================ # Utility / helpers # ================================================================
[docs] def _get_textfont(self): """ Retunr a 'Text_Infos' instance for the legend """ r,g,b = getRGBfromI(self.myprop.legendcolor) tinfos = Text_Infos(self.myprop.legendpriority, (np.cos(self.myprop.legendorientation/180*np.pi), np.sin(self.myprop.legendorientation/180*np.pi)), self.myprop.legendfontname, self.myprop.legendfontsize, colour=(r,g,b,255), dimsreal=(self.myprop.legendlength, self.myprop.legendheight), relative_position=self.myprop.legendrelpos) return tinfos
[docs] def _get_textfont_idx(self): """ Retunr a 'Text_Infos' instance for the legend """ r,g,b = getRGBfromI(self.myprop.color) tinfos = Text_Infos(3, (1., 0.), self.myprop.legendfontname, 12, colour=(r,g,b,255), dimsreal=(self.myprop.legendlength, self.myprop.legendheight), relative_position=7) return tinfos