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