"""Triangulation GUI class with OpenGL and matplotlib rendering."""
import wx
import logging
from OpenGL.GL import *
import matplotlib.pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from matplotlib.tri import Triangulation as mpl_tri
from ..PyTranslate import _
from ..drawing_obj import Element_To_Draw
from ._models import TriangulationModel
[docs]
class Triangulation(TriangulationModel, Element_To_Draw):
"""Triangulation with OpenGL rendering and matplotlib plotting.
Inherits all data operations from :class:`TriangulationModel` and
adds ``Element_To_Draw`` integration, OpenGL display-list rendering,
and matplotlib visualisation helpers.
"""
# ================================================================
# Constructor
# ================================================================
def __init__(self, fn='', pts=None, tri=None, idx: str = '', plotted: bool = True, mapviewer=None, need_for_wx: bool = False) -> None:
"""Initialise a GUI-enabled triangulation.
:param fn: File path to read (empty string to skip).
:param pts: Initial point array.
:param tri: Initial triangle connectivity array.
:param idx: Identifier string.
:param plotted: Whether the triangulation is plotted.
:param mapviewer: Parent map viewer instance.
:param need_for_wx: Whether wx integration is required.
"""
if pts is None:
pts = []
if tri is None:
tri = []
# Must exist before TriangulationModel.__init__(), which may call self.read().
# Initialise the drawing interface (plotted, mapviewer, bounds)
Element_To_Draw.__init__(self, idx=idx, plotted=plotted, mapviewer=mapviewer, need_for_wx=need_for_wx)
# Initialise the model (geometry, I/O, transforms). This must run after
# Element_To_Draw so model-computed bounds are not overwritten to 0.
TriangulationModel.__init__(self, fn=fn, pts=pts, tri=tri, idx=idx)
# ================================================================
# Factory overrides
# ================================================================
[docs]
def _make_triangulation(self, **kwargs) -> "Triangulation":
"""Factory: create a GUI-enabled triangulation."""
return Triangulation(**kwargs)
# ================================================================
# Properties
# ================================================================
@property
[docs]
def id_list(self):
"""Backward-compatible alias for the OpenGL display-list id."""
return self.idgllist
@id_list.setter
def id_list(self, value):
self.idgllist = value
# ================================================================
# I/O operations
# ================================================================
[docs]
def read(self, fn: str):
"""Read a file, then reset the GL display list.
:param fn: File path to read.
"""
super().read(fn)
self.reset_plot()
[docs]
def import_from_gltf(self, fn=''):
"""Import a GLTF/GLB file, with optional wx file dialog.
:param fn: File path (empty to show a file dialog).
"""
wx_exists = wx.GetApp() is not None
if fn == '' and wx_exists:
dlg = wx.FileDialog(None, _('Choose filename'),
wildcard='binary gltf2 (*.glb)|*.glb|gltf2 (*.gltf)|*.gltf|All (*.*)|*.*',
style=wx.FD_OPEN)
ret = dlg.ShowModal()
if ret == wx.ID_CANCEL:
dlg.Destroy()
return
fn = dlg.GetPath()
dlg.Destroy()
super().import_from_gltf(fn)
self.reset_plot()
[docs]
def export_to_gltf(self, fn=''):
"""Export to GLTF/GLB, with optional wx file dialog.
:param fn: File path (empty to show a file dialog).
"""
if fn == '':
dlg = wx.FileDialog(None, _('Choose filename'),
wildcard='binary gltf2 (*.glb)|*.glb|gltf2 (*.gltf)|*.gltf|All (*.*)|*.*',
style=wx.FD_SAVE)
ret = dlg.ShowModal()
if ret == wx.ID_CANCEL:
dlg.Destroy()
return
fn = dlg.GetPath()
dlg.Destroy()
return super().export_to_gltf(fn)
# ================================================================
# Object operations
# ================================================================
[docs]
def copy(self) -> "Triangulation":
"""Return a GUI-enabled copy of the triangulation."""
newtri = self._make_triangulation(pts=self.pts.copy(), tri=self.tri.copy(), idx=self.idx + '_copy')
return newtri
# ================================================================
# OpenGL rendering
# ================================================================
[docs]
def reset_listogl(self):
"""Reset the OpenGL display list for the triangulation."""
if getattr(self, 'idgllist', -99999) != -99999:
try:
glDeleteLists(self.idgllist, 1)
except Exception:
logging.warning('Problem while resetting OpenGL plot - Triangulation.reset_listogl')
finally:
self.idgllist = -99999
[docs]
def reset_plot(self):
"""Backward-compatible alias used by the viewer to invalidate the GL plot."""
self.reset_listogl()
[docs]
def plot(self, sx=None, sy=None, xmin=None, ymin=None, xmax=None, ymax=None, size=None ):
"""Plot the triangulation using OpenGL.
: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.idgllist == -99999:
try:
self.idgllist = glGenLists(1)
glNewList(self.idgllist, GL_COMPILE)
glPolygonMode(GL_FRONT_AND_BACK,GL_LINE)
for curtri in self.tri:
glBegin(GL_LINE_STRIP)
glColor3ub(int(0),int(0),int(0))
glVertex2d(self.pts[curtri[0]][0], self.pts[curtri[0]][1])
glVertex2d(self.pts[curtri[1]][0], self.pts[curtri[1]][1])
glVertex2d(self.pts[curtri[2]][0], self.pts[curtri[2]][1])
glVertex2d(self.pts[curtri[0]][0], self.pts[curtri[0]][1])
glEnd()
glPolygonMode(GL_FRONT_AND_BACK,GL_LINE)
glEndList()
glCallList(self.idgllist)
except Exception:
logging.exception(
'Problem with OpenGL plot - Triangulation.plot -- idx=%s npts=%s ntri=%s mapviewer=%s',
self.idx,
len(self.pts) if self.pts is not None else 0,
len(self.tri) if self.tri is not None else 0,
type(self.get_mapviewer()).__name__ if self.get_mapviewer() is not None else None,
)
else:
glCallList(self.idgllist)
# ================================================================
# Matplotlib plotting
# ================================================================
[docs]
def plot_matplotlib(self, ax:Axes | tuple[Figure, Axes] = None, color='black', alpha=1., lw=1.5, **kwargs):
"""Plot the triangulation using Matplotlib.
:param ax: Axes, (Figure, Axes) tuple, or None to create a new figure.
:param color: Line colour.
:param alpha: Opacity (0–1).
:param lw: Line width.
:param kwargs: Extra keyword arguments forwarded to ``ax.plot``.
:return: ``(fig, ax)`` tuple.
"""
if isinstance(ax, tuple):
fig, ax = ax
elif ax is None:
fig, ax = plt.subplots()
else:
fig = ax.figure
if self.nb_tri>0:
for curtri in self.tri:
x = [self.pts[curtri[0]][0], self.pts[curtri[1]][0], self.pts[curtri[2]][0], self.pts[curtri[0]][0]]
y = [self.pts[curtri[0]][1], self.pts[curtri[1]][1], self.pts[curtri[2]][1], self.pts[curtri[0]][1]]
ax.plot(x, y, color=color, alpha=alpha, lw=lw, **kwargs)
else:
logging.warning('No triangles to plot')
return fig, ax
@property
[docs]
def mpl_triangulation(self) -> mpl_tri:
"""Return the triangulation as a Matplotlib Triangulation object.
:return: ``matplotlib.tri.Triangulation`` or *None* if empty.
"""
if self.nb_tri>0:
return mpl_tri(self.pts[:,0], self.pts[:,1], self.tri)
else:
logging.warning('No triangles to plot')
return None
[docs]
def plot_matplotlib_3D(self, ax:Axes | tuple[Figure, Axes] = None, color='black', alpha=0.2, lw=1.5, edgecolor='k', shade=True, **kwargs):
"""Plot the triangulation as a 3-D surface using Matplotlib.
:param ax: 3-D Axes, (Figure, Axes) tuple, or None to create one.
:param color: Surface colour.
:param alpha: Surface opacity.
:param lw: Line width of triangle edges.
:param edgecolor: Edge colour.
:param shade: Whether to shade the surface.
:param kwargs: Extra keyword arguments forwarded to ``plot_trisurf``.
:return: ``(fig, ax)`` tuple, or ``(None, None)`` if empty.
"""
if self.nb_tri>0:
if isinstance(ax, tuple):
fig, ax = ax
elif ax is None:
fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
else:
fig = ax.figure
ax.plot_trisurf(self.mpl_triangulation, Z=self.pts[:,2], color=color, alpha=alpha, lw=lw, edgecolor=edgecolor, shade=shade, **kwargs)
return fig, ax
else:
logging.warning('No triangles to plot')
return None, None