"""
Author: HECE - University of Liege, Pierre Archambeau
Date: 2024
Copyright (c) 2024 University of Liege. All rights reserved.
This script and its content are protected by copyright law. Unauthorized
copying or distribution of this file, via any medium, is strictly prohibited.
"""
try:
from osgeo import gdal
except ImportError as e:
print(e)
raise ImportError("I can't find the 'gdal' package. You should get it from https://www.lfd.uci.edu/~gohlke/pythonlibs/")
import warnings
from os import path
try:
import numpy as np
import math
from wx import dataview, TreeCtrl
import wx
import wx.propgrid as pg
from wx.dataview import *
import asyncio
from wx.core import VERTICAL, BoxSizer, Height, ListCtrl, StaticText, TextCtrl, Width
from wx.glcanvas import GLCanvas, GLContext
from wx.dataview import TreeListCtrl
import wx.lib.ogl as ogl
from PIL import Image, ImageOps
from PIL.PngImagePlugin import PngInfo
import io
import json
import glob
import traceback
from datetime import datetime
from sklearn import linear_model, datasets
import geopandas as gpd
from tqdm import tqdm
except ImportError as e:
print(e)
raise ImportError("Error importing wxPython, numpy, PIL, json, glob, traceback, sklearn. Please check your installation.")
try:
import time as time_module
from time import sleep
from datetime import timedelta
from multiprocessing import Pool
from pathlib import Path
except ImportError as e:
print(e)
raise ImportError("Error importing time, datetime, multiprocessing, pathlib. Please check your installation.")
try:
from OpenGL.GL import *
from OpenGL.GLUT import *
except ImportError as e:
[docs]
msg=_('Error importing OpenGL library')
msg+=_(' Python version : ' + sys.version)
msg+=_(' Please check your version of opengl32.dll -- conflict may exist between different files present on your desktop')
raise Exception(msg)
try:
import matplotlib.pyplot as plt
from matplotlib.widgets import Button as mplButton
from matplotlib.ticker import FormatStrFormatter
from os import scandir, listdir
from os.path import exists, join, normpath
from pptx import Presentation
import threading
from enum import Enum
import logging
except ImportError as e:
print(e)
raise ImportError("Error importing matplotlib, os, threading, enum, typing, logging. Please check your installation.")
from typing import TYPE_CHECKING, Generic, Literal, Union, Sequence, TypeVar, overload
if TYPE_CHECKING:
from wolfhece.tablet_wintab import WinTabContext
try:
from .wolf_texture import genericImagetexture,imagetexture,Text_Image_Texture
from .drawing_obj import Element_To_Draw
from .xyz_file import xyz_scandir, XYZFile
from .mesh2d import wolf2dprev
from .PyPalette import wolfpalette
from .wolfresults_2D import Wolfresults_2D, views_2D, Extractable_results
from .PyTranslate import _
from .PyVertex import cloud_vertices, cloud_of_clouds
from .color_constants import getRGBfromI, getIfromRGB
from .RatingCurve import SPWMIGaugingStations, SPWDCENNGaugingStations
from .wolf_array import WOLF_ARRAY_MB, SelectionData, WolfArray, WolfArrayMB, CropDialog, header_wolf, WolfArrayMNAP, WOLF_ARRAY_FULL_SINGLE, WOLF_ARRAY_FULL_INTEGER8, WOLF_ARRAY_FULL_INTEGER16, WOLF_ARRAY_FULL_DOUBLE, WOLF_ARRAY_FULL_INTEGER, HillshadeRenderParams
from .PyParams import Wolf_Param, key_Param, Type_Param
from .mesh2d.bc_manager import BcManager
from .PyVertexvectors import *
from .Results2DGPU import wolfres2DGPU
from .PyCrosssections import crosssections, profile, Interpolator, Interpolators
from .GraphNotebook import PlotNotebook
from .lazviewer.laz_viewer import myviewer, read_laz, clip_data_xyz, xyz_laz_grids, choices_laz_colormap, Classification_LAZ, Wolf_LAZ_Data, viewer as viewerlaz
from . import Lidar2002
from .picc import Picc_data, Cadaster_data
from .wolf_zi_db import ZI_Databse_Elt, PlansTerrier, Ouvrages, Particularites, Enquetes, Profils
from .math_parser.calculator import Calculator
from .wintab.wintab import Wintab # noqa: F401 (kept for back-compat imports)
from .images_tiles import ImagesTiles
from .PyWMS import Alaro_Navigator, get_Alaro_legend
from .PyPictures import PictureCollection
from .assets.pie import PieZonesController
from .assets.bar import BarZonesController
from .assets.curve import CurveZonesController
from .opengl.text_renderer2d import TextRenderer2D, GlyphAtlas, measure_text
from .irm_alaro import IRM_Alaro, GribFiles, _convert_col2date_str
from .hydrology.flowaccdir import Lidaxe
from .pydownloader import toys_dataset
from .wolf_sculpt import BrushShape, SculptMode, ProfileShape
except ImportError as e:
print(e)
raise ImportError("Error importing wolf_texture, xyz_file, mesh2d, PyPalette, wolfresults_2D, PyTranslate, PyVertex, RatingCurve, wolf_array, PyParams, mesh2d.bc_manager, PyVertexvectors, Results2DGPU, PyCrosssections, GraphNotebook, lazviewer, picc, wolf_zi_db, math_parser.calculator, wintab. Please check your installation.")
try:
from ._dike_manager import DikeManager
from .dike import DikeWolf, InjectorWolfDike as InjectorDike
[docs]
WOLFPYDIKE_AVAILABLE = True
except:
logging.warning(_("Missing package. Install wolfpydike module via pip."))
WOLFPYDIKE_AVAILABLE = False
try:
from .hydrometry.kiwis_wolfgui import hydrometry_wolfgui
except ImportError as e:
print(e)
raise ImportError("Error importing hydrometry.kiwis_wolfgui. Please check your installation.")
try:
from .pyshields import get_d_cr
from .pyviews import WolfViews
from .PyConfig import handle_configuration_dialog, WolfConfiguration, ConfigurationKeys
from .GraphProfile import ProfileNotebook
from .pybridges import Bridges, Bridge, Weirs, Weir
from .tools_mpl import *
from .wolf_tiles import Tiles
from .lagrangian.particle_system_ui import Particle_system_to_draw as Particle_system
from .opengl.py3d import Wolf_Viewer3D
from .pyGui1D import GuiNotebook1D
from .matplotlib_fig import Matplotlib_Figure as MplFig, PRESET_LAYOUTS
except ImportError as e:
print(e)
raise ImportError("Error importing pyshields, pyviews, PyConfig, GraphProfile, pybridges, tools_mpl, wolf_tiles, lagrangian.particle_system_ui, opengl.py3d, pyGui1D. Please check your installation.")
try:
from .apps.curvedigitizer import Digitizer
except ImportError as e:
print(e)
raise ImportError("Error importing apps.curvedigitizer. Please check your installation.")
try:
from ._drowning_manager import DrowningManager
from .drowning_victims.drowning_class import Drowning_victim_Viewer
except ImportError as e:
print(e)
raise ImportError("Error importing Drowning_victims.Class. Please check your installation.")
from ._lidaxe_manager import LidaxeManager
from ._alaro_manager import AlaroManager
from ._lulc_manager import LulcManager
from ._pictcollection_manager import PictureCollectionManager
from ._landmap_manager import LandmapManager
from ._bridge_manager import BridgeManager
from ._weir_manager import WeirManager
from ._qdfidf_manager import QdfidfManager
from ._laz_manager import LazManager
from ._particlesystem_manager import ParticleSystemManager
from ._tiles_manager import TilesManager
from ._simtools2d_manager import SimTools2DManager
from ._simtools2d_gpu_manager import SimTools2DGPUManager
from ._wolf2dresults_manager import Wolf2DResultsManager
from ._analyze_manager import AnalyzeManager
from .dialog_provider import DialogProvider, DialogStyles
[docs]
THROTTLING_FREQUENCY = 10 # Maximum frequency of background updates in Hz (10 means max 10 updates per second, i.e. at least 0.1 sec between updates)
[docs]
LIST_1TO9 = [wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3, wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, wx.WXK_NUMPAD8, wx.WXK_NUMPAD9 ] + [ord(str(cur)) for cur in range(1,10)]
[docs]
PROJECT_ACTION = 'action'
[docs]
PROJECT_CS = 'cross_sections'
[docs]
PROJECT_VECTOR = 'vector'
[docs]
PROJECT_ARRAY = 'array'
[docs]
PROJECT_TILES = 'tiles'
[docs]
PROJECT_LAZ = 'laz_grid'
[docs]
PROJECT_CLOUD = 'cloud'
[docs]
PROJECT_WOLF2D = 'wolf2d'
[docs]
PROJECT_GPU2D = 'gpu2d'
[docs]
PROJECT_PALETTE = 'palette'
[docs]
PROJECT_PALETTE_ARRAY = 'palette-array'
[docs]
PROJECT_LINK_CS = 'cross_sections_link'
[docs]
PROJECT_LINK_VEC_ARRAY = 'vector_array_link'
[docs]
PROJECT_GROUP_KEYS = {PROJECT_ACTION : {'which': 'compare_arrays'},
PROJECT_CS: {'id - file': 'id to use - full or relative path to CS file',
'format': '(mandatory) 2000, 2022, vecz, sxy',
'dirlaz': 'Path to LAZ data (prepro Numpy)'},
PROJECT_VECTOR: {'id - file': 'id to use - full or relative path to vector file (.vec, .vecz, .shp)'},
PROJECT_ARRAY: {'id - file': 'id to use - full or relative path to array file (.bin, .tif, .npy, .npz)'},
PROJECT_TILES : {'id': '(mandatory) id to use',
'tiles_file': '(mandatory) Path to tiles file',
'data_dir': '(mandatory) Path to data directory',
'comp_dir': 'Path to comparison directory'},
PROJECT_LAZ: {'data_dir': '(mandatory) Path to data directory (prepro Numpy)',
'classification': 'Color classification for LAZ data - default SPW-Geofit 2023',},
PROJECT_CLOUD: {'id - file': 'id to use - full or relative path to cloud file (.xyz, .txt)'},
PROJECT_WOLF2D: {'id - dir': 'id to use - full or relative path to wolf2d simulation directory'},
PROJECT_GPU2D: {'id - dir': 'id to use - full or relative path to gpu2d simulation directory'},
PROJECT_PALETTE : {'id - file': 'id to use - full or relative path to palette file (.pal)'},
PROJECT_PALETTE_ARRAY : {'idarray - idpal': 'id of array - id of palette to link'},
PROJECT_LINK_CS : {'linkzones' : '(mandatory) id of vector to link to cross sections',
'sortzone' : '(mandatory) id of the zone to use for sorting',
'sortname' : '(mandatory) id of the polyline to use for sorting',
'downfirst' : 'is the first vertex downstream or upstream? (1 is True, 0 is False - default is False)'},
PROJECT_LINK_VEC_ARRAY : {'id - id vector': 'id of array/wolf2d/gpu2d - id of vector to link (only 1 vector in 1 zone)'},
}
# ======================================================================
# Utility classes → wolfhece/_pydraw_utils.py
# ======================================================================
from ._pydraw_utils import (
MplFigViewer,
Memory_View,
Memory_View_encoder,
Memory_View_decoder,
Memory_Views,
Memory_Views_GUI,
draw_type,
Colors_1to9,
DragdropFileTarget,
)
# ======================================================================
# Overlay classes imported from wolfhece/_overlays.py
# ======================================================================
from ._overlays import (
HillshadePanel,
HillshadeOverlay,
_ToolButton,
CutFillOverlay,
ToolbarOverlay,
PaletteOverlay,
)
from ._action_kind import (
ActionKind,
SELECT_BY_VECTOR_ACTIONS,
SELECT_ACTIVE_VECTOR_ACTIONS,
SELECT_NODE_ACTIONS,
PICK_LANDMAP_ACTIONS,
POLYGON_VERTEX_ACTIONS,
HEAVY_GL_ACTIONS,
)
from ._viewer_plugin_handlers import (
ACTION_RDOWN_HANDLERS,
ACTION_MOTION_HANDLERS,
KeyboardSnapshot,
MouseContext,
_RightDownHandler as _MouseHandler,
_LeftDownHandler as _LeftDownHandler,
_KeyHandler as _KeyHandler,
_PaintHandler as _PaintHandler,
)
# ---------------------------------------------------------------------------
# Keys polled at every mouse-motion event to populate KeyboardSnapshot.held.
# All ASCII upper-case letters A–Z (ord 65–90) plus SPACE (32).
# ---------------------------------------------------------------------------
[docs]
_POLLED_KEYS: tuple[int, ...] = tuple(range(65, 91)) + (32,) # A-Z + SPACE
# ======================================================================
# Asset-chart / transform companion object → wolfhece/_asset_manager.py
# ======================================================================
from ._asset_manager import AssetManager
from ._sculpt_manager import SculptManager
# ======================================================================
# Sim/video/drowning/precomputed/animation classes → wolfhece/_sim_panels.py
# ======================================================================
from ._sim_panels import (
Sim_Explorer,
Sim_VideoCreation,
Drowning_Explorer,
Select_Begin_end_interval_step,
PrecomputedDEM_DTM,
Precomputed_DEM_DTM_Dialog,
GlobalAnimationClock,
)
[docs]
class ActiveSlot(Generic[_T]):
"""
Descripteur d'attribut ``active_*`` auto-enregistré.
L'objet descripteur est unique au niveau de la classe (attribut de classe),
mais la **valeur** est stockée par instance dans ``instance.__dict__``
sous la clé privée ``_slot_<name>``. Cela garantit une parfaite isolation
entre instances et une interface publique indiscernable d'un attribut
d'instance classique.
À l'affectation de ``active_X = ActiveSlot()`` dans le corps de la classe,
Python appelle automatiquement ``__set_name__`` qui enregistre le slot dans
le registre de classe ``_active_slots``. Ce registre permet les opérations
groupées (reset, recherche par identité d'objet).
Rétrocompatibilité : ``instance.active_X = val`` et ``instance.active_X``
fonctionnent exactement comme avant pour tout code interne ou externe.
Gardien de type :
- ``expected_type`` est passé au constructeur (type unique ou tuple de types).
- À l'affectation, un ``isinstance`` vérifie la valeur (hors ``None``).
- Les overloads ``__get__`` permettent à Pylance d'inférer ``_T | None``
directement depuis la déclaration de classe, sans analyser tous les
sites d'affectation.
"""
def __init__(self, expected_type: 'type[_T] | tuple[type, ...] | None' = None) -> None:
[docs]
self._expected_type = expected_type
def __set_name__(self, owner: type, name: str) -> None:
self._public_name = name
self._private_name = f'_slot_{name}'
if not hasattr(owner, '_active_slots'):
owner._active_slots = {}
owner._active_slots[name] = self
@overload
def __get__(self, obj: None, objtype: type) -> 'ActiveSlot[_T]': ...
@overload
def __get__(self, obj: object, objtype: type) -> '_T | None': ...
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self._private_name)
def __set__(self, obj, value: '_T | None') -> None:
if value is not None and self._expected_type is not None:
if not isinstance(value, self._expected_type):
exp_name = (
'|'.join(t.__name__ for t in self._expected_type)
if isinstance(self._expected_type, tuple)
else self._expected_type.__name__
)
raise TypeError(
f'{self._public_name} expects {exp_name}, '
f'got {type(value).__name__}'
)
obj.__dict__[self._private_name] = value
[docs]
class WolfMapViewer(wx.Frame):
"""
Fenêtre de visualisation de données WOLF grâce aux WxWidgets
"""
[docs]
TIMER_ID = 100 # délai d'attente avant action
[docs]
mybc: list[BcManager] # Gestionnaire de CL
[docs]
myarrays: list # matrices ajoutées
[docs]
myvectors: list[Zones] # zones vectorielles ajoutées
[docs]
myclouds: list[cloud_vertices, cloud_of_clouds] # nuages de vertices
[docs]
mytri: list[Triangulation] # triangulations
[docs]
myimagestiles: list[ImagesTiles]
[docs]
mypartsystems: list[Particle_system]
[docs]
myviewers3d:list[Wolf_Viewer3D]
[docs]
mylazdata:list[Wolf_LAZ_Data]
[docs]
mypicturecollections: list[PictureCollection]
[docs]
myinjectors: list[InjectorDike]
[docs]
mymplfigs:list[MplFigViewer]
[docs]
sim_explorers: dict[Wolfresults_2D:Sim_Explorer]
[docs]
canvas: GLCanvas # canvas OpenGL
[docs]
context: GLContext # context OpenGL
[docs]
treelist: TreeListCtrl # Gestion des éléments sous forme d'arbre
[docs]
_lbl_selecteditem: StaticText
# DEPRECEATED
# added: dict # dictionnaire des éléments ajoutés
# Registry populated automatically by ActiveSlot.__set_name__; maps slot
# name → descriptor instance. Declared here so Pylance resolves the type.
[docs]
_active_slots: dict[str, 'ActiveSlot']
# --- Active-object slots (ActiveSlot descriptors) ---
# Each slot stores its value per-instance in instance.__dict__['_slot_<name>'].
# Behaviour is identical to a plain instance attribute for all callers.
# Adding a new active type = one line here; reset/removal are automatic.
[docs]
active_vector = ActiveSlot(vector)
[docs]
active_zone = ActiveSlot(zone)
[docs]
active_zones = ActiveSlot(Zones)
[docs]
active_array = ActiveSlot(WolfArray)
[docs]
active_bc = ActiveSlot(BcManager)
[docs]
active_view = ActiveSlot(WolfViews)
[docs]
active_vertex = ActiveSlot(wolfvertex)
[docs]
active_cs = ActiveSlot(crosssections)
[docs]
active_tri = ActiveSlot(Triangulation)
[docs]
active_tile = ActiveSlot(Tiles)
[docs]
active_imagestiles = ActiveSlot(ImagesTiles)
[docs]
active_particle_system = ActiveSlot(Particle_system)
[docs]
active_viewer3d = ActiveSlot(Wolf_Viewer3D)
[docs]
active_viewerlaz = ActiveSlot(viewerlaz)
[docs]
active_bridges = ActiveSlot(Bridges)
[docs]
active_bridge = ActiveSlot(Bridge)
[docs]
active_weirs = ActiveSlot(Weirs)
[docs]
active_weir = ActiveSlot(Weir)
[docs]
active_laz = ActiveSlot(Wolf_LAZ_Data)
[docs]
active_injector = ActiveSlot() # InjectorDike — import optionnel (wolfpydike)
[docs]
active_picturecollection = ActiveSlot(PictureCollection)
[docs]
active_fig = ActiveSlot(MplFigViewer)
[docs]
active_cloud = ActiveSlot((cloud_vertices, cloud_of_clouds)) # Union
[docs]
active_profile = ActiveSlot(profile)
[docs]
active_res2d = ActiveSlot() # Wolfresults_2D
[docs]
active_landmap = ActiveSlot() # PlansTerrier
[docs]
active_qdfidf = ActiveSlot() # QdFiDF
[docs]
active_cloud_vertex_id = ActiveSlot() # int or None
[docs]
alaro_navigator: Alaro_Navigator
def __init__(self,
wxparent = None,
title:str = _('Default Wolf Map Viewer'),
w:int=500,
h:int=500,
treewidth:int=200,
wolfparent=None,
wxlogging=None,
enable_async_background_updates:bool=False):
"""
Create a Viewer for WOLF data/simulation
:params wxparent: wx parent - set to None if main window
:params title: title of the window
:params w: width of the window in pixels
:params h: height of the window in pixels
:params treewidth: width of the tree list in pixels
:params wolfparent: WOLF object parent -- see PyGui.py
:params wxlogging: wx logging object
:params enable_async_background_updates: if True, background WMS images load asynchronously
during pan/zoom; if False, uses synchronous loading (default: True)
"""
self._init_runtime_state(
wxparent=wxparent,
title=title,
w=w,
h=h,
treewidth=treewidth,
wolfparent=wolfparent,
wxlogging=wxlogging,
enable_async_background_updates=enable_async_background_updates,
)
self._init_active_state_and_managers()
self._init_menus_and_bindings()
self._init_canvas_tree_layout(w=w, h=h)
self._init_auxiliary_ui_state()
self._init_post_init_objects()
self._init_wintab_context()
[docs]
def _init_runtime_state(self,
wxparent,
title: str,
w: int,
h: int,
treewidth: int,
wolfparent,
wxlogging,
enable_async_background_updates: bool) -> None:
"""Initialize non-menu runtime state and base frame."""
self._last_paint_bounds = None
self._last_async_background_update_time = None
self._last_bounds_change_time = None
self.enable_async_background_updates = enable_async_background_updates
self._hillshade_enabled: bool = False
self._hillshade_multidirectional: bool = False
self._sun_altitude: float = 45.0
self._sun_azimuth: float = 315.0
self._sun_intensity: float = 1.0
self._hillshade_sync: bool = True
self._hillshade_shared_params = HillshadeRenderParams()
self._hillshade_panel = None
self._hillshade_overlay: "HillshadeOverlay | None" = None
self._palette_overlay: "PaletteOverlay | None" = None
self._toolbar_overlay: "ToolbarOverlay | None" = ToolbarOverlay(self)
self._cutfill_overlay: "CutFillOverlay" = CutFillOverlay(self)
self._sculpt = SculptManager(self)
self._wintab: "WinTabContext | None" = None
self._show_dialog_wx = True
self._dialogs = DialogProvider()
self.treewidth = treewidth
super(WolfMapViewer, self).__init__(wxparent, title=title, size=(w + self.treewidth, h))
self._wxlogging = wxlogging
self.action = None
# Per-instance plugin handlers — checked before the global tables.
# Keys are lowercase action-id strings (same convention as self.action).
self._custom_rdown_handlers: dict[str, '_MouseHandler'] = {}
self._custom_motion_handlers: dict[str, '_MouseHandler'] = {}
self._custom_ldown_handlers: dict[str, '_LeftDownHandler'] = {}
self._custom_key_handlers: dict[str, '_KeyHandler'] = {}
self._custom_paint_handlers: dict[str, '_PaintHandler'] = {}
# Saved handlers for overloaded actions — restored by unregister_action().
# Structure: {action_id: {slot_name: previous_handler_or_None}}
self._saved_handlers: dict[str, dict[str, object]] = {}
self.update_absolute_minmax = False
self.copyfrom = None
self.wolfparent = wolfparent
self.regular = True
self.sx = 1
self.sy = 1
self.samescale = True
self.dynapar_dist = 1.
self.xmin = 0.
self.ymin = 0.
self.xmax = 40.
self.ymax = 40.
self.width = self.xmax - self.xmin
self.height = self.ymax - self.ymin
self.canvaswidth = 100
self.canvasheight = 100
self._center_x = self.width / 2.
self._center_y = self.height / 2.
self._mouse_context = MouseContext(
x=0,
y=0,
x_pixel=0,
y_pixel=0,
x_snap=0,
y_snap=0,
keyboard=KeyboardSnapshot(held=set(), alt=False, ctrl=False, shift=False),
)
self._mouse_context_down = None
self._assets = AssetManager(self)
self._snap_grid_unit = 0.01
self._snap_grid_round_base = 1000.0
self.bordersize = 0
self.titlesize = 0
# Kept intentionally for backward behavior parity.
self.treewidth = 200
self.backcolor = wx.Colour(255, 255, 255)
self.oneclick = True
self.linked = False
self.link_shareopsvect = True
self.linkedList = None
self.link_params = None
self.project_pal = None
self.forcemimic = True
self.currently_readresults = False
self.mylazgrid: xyz_laz_grids = None
self.colors1to9 = Colors_1to9(self)
self._dragdrop = DragdropFileTarget(self)
self.SetDropTarget(self._dragdrop)
self.menubar = wx.MenuBar()
self.timer_ps = None
self.anim_clock = GlobalAnimationClock(self)
self.alaro_navigator = None
[docs]
def _init_active_state_and_managers(self) -> None:
"""Initialize active selections and manager objects."""
self.mybc = []
# All active_* slots default to None via ActiveSlot.__get__; no manual
# initialisation is required. The two non-slot selections are kept as
# plain instance attributes.
self.selected_treeitem = None
self.selected_object = None
self._drowning = DrowningManager(self)
self._dike = DikeManager(self)
self._lidaxe = LidaxeManager(self)
self._alaro = AlaroManager(self)
self._lulc = LulcManager(self)
self._pictcollection = PictureCollectionManager(self)
self._landmap_mgr = LandmapManager(self)
self._bridge_mgr = BridgeManager(self)
self._weir_mgr = WeirManager(self)
self._qdfidf = QdfidfManager(self)
self._laz_mgr = LazManager(self)
self._particlesystem_mgr = ParticleSystemManager(self)
self._tiles_mgr = TilesManager(self)
self._simtools2d_mgr = SimTools2DManager(self)
self._simtools2d_gpu_mgr = SimTools2DGPUManager(self)
self._wolf2dresults_mgr = Wolf2DResultsManager(self)
self._analyze_mgr = AnalyzeManager(self)
# ------------------------------------------------------------------
# Helpers for grouped operations on active_* slots
# ------------------------------------------------------------------
[docs]
def reset_all_actives(self) -> None:
"""Set every active_* slot to None (slots + delegated-manager actives)."""
for slot in self._active_slots.values():
slot.__set__(self, None)
# Delegated-manager properties are @property, not slots — reset manually.
self._drowning.active = None
self._dike.active = None
self._alaro.active = None
self._lidaxe.active = None
[docs]
def clear_active_if_is(self, obj) -> bool:
"""
If *obj* is currently stored in any active_* slot, set that slot to None.
Returns True if at least one slot was cleared, False otherwise.
This replaces the long if/elif chain in removeobj_from_id.
"""
cleared = False
for slot in self._active_slots.values():
if slot.__get__(self) is obj:
slot.__set__(self, None)
cleared = True
# Also check delegated-manager actives
for mgr in (self._drowning, self._dike, self._alaro, self._lidaxe):
if mgr.active is obj:
mgr.active = None
cleared = True
return cleared
[docs]
def _init_menus_and_bindings(self) -> None:
"""Build all top-level menus and wire their handlers."""
self.filemenu = wx.Menu()
openitem = self.filemenu.Append(wx.ID_OPEN, _('Open/Add project'), _('Open a full project from file'))
saveproject = self.filemenu.Append(wx.ID_ANY, _('Save project as...'), _('Save the current project to file'))
self.filemenu.AppendSeparator()
saveitem = self.filemenu.Append(wx.ID_SAVE, _('Save'), _('Save all checked arrays or vectors to files'))
saveasitem = self.filemenu.Append(wx.ID_SAVEAS, _('Save as...'), _('Save all checked arrays or vectors to new files --> one file dialog per data'))
savecanvas = self.filemenu.Append(wx.ID_ANY, _('Save to image...'), _('Save the canvas to image file on disk'))
copycanvas = self.filemenu.Append(wx.ID_ANY, _('Copy image...'), _('Copy the canvas to image file to the clipboard'))
self.filemenu.AppendSeparator()
self.menugltf = wx.Menu()
self.filemenu.Append(wx.ID_ANY, _('Gltf2...'), self.menugltf)
exportgltf = self.menugltf.Append(wx.ID_ANY, _('Export...'), _('Save data to gltf files'))
importgltf = self.menugltf.Append(wx.ID_ANY, _('Import...'), _('Import data from gltf files'))
gltfcompareitem = self.menugltf.Append(wx.ID_ANY, _('Compare...'), _('Create new frames to compare sculpting'))
updategltf = self.menugltf.Append(wx.ID_ANY, _('Update...'), _('Update data from gltf files'))
self.filemenu.AppendSeparator()
self.menu_sim2d = wx.Menu()
self.menu_sim2d_cpu = wx.Menu()
self.menu_sim2d_gpu = wx.Menu()
self.menu_sim1d = wx.Menu()
sim2d = self.menu_sim2d_cpu.Append(wx.ID_ANY, _('Create/Open multiblock model'), _('Create or open a multiblock model in the viewer --> CPU/Fortran Wolf2D model'))
check2D = self.menu_sim2d_cpu.Append(wx.ID_ANY, _('Check headers'), _('Check the header .txt files from an existing 2D CPU simulation'))
sim2dgpu = self.menu_sim2d_gpu.Append(wx.ID_ANY, _('Create/Open GPU model'), _('Create or open a GPU model in the viewer --> GPU Wolf2D model'))
create1Dmodel = self.menu_sim1d.Append(wx.ID_ANY, _('Create Wolf1D...'), ('Create a 1D model using crossections, vectors and arrays...'))
self.menu_sim2d.Append(wx.ID_ANY, _('2D GPU'), self.menu_sim2d_gpu)
self.menu_sim2d.Append(wx.ID_ANY, _('2D CPU'), self.menu_sim2d_cpu)
self.filemenu.Append(wx.ID_ANY, _('2D Model'), self.menu_sim2d)
self.filemenu.Append(wx.ID_ANY, _('1D Model'), self.menu_sim1d)
self.menu_hydrology = wx.Menu()
hydrol = self.menu_hydrology.Append(wx.ID_ANY, _('Create/Open Hydrological model'), _('Hydrological simulation'))
self.filemenu.Append(wx.ID_ANY, _('Hydrology'), self.menu_hydrology)
self.menu_hydrology.AppendSeparator()
addlidaxe = self.menu_hydrology.Append(wx.ID_ANY, _('Activate Lidaxe tools...'), _('Add Lidaxe tools'))
self.filemenu.AppendSeparator()
setcomparisonitem = self.filemenu.Append(wx.ID_ANY, _('Set comparison'), _('Set comparison'))
multiview = self.filemenu.Append(wx.ID_ANY, _('Multiviewer'), _('Multiviewer'))
viewer3d = self.filemenu.Append(wx.ID_ANY, _('3D viewer'), _('3D viewer'))
self.filemenu.AppendSeparator()
self.menucreateobj = wx.Menu()
self.filemenu.Append(wx.ID_ANY, _('Create...'), self.menucreateobj)
createarray = self.menucreateobj.Append(wx.ID_FILE6, _('Create array...'), _('New array (binary file - real)'))
createarray2002 = self.menucreateobj.Append(wx.ID_ANY, _('Create array from Lidar 2002...'), _('Create array from Lidar 2002 (binary file - real)'))
createarrayxyz = self.menucreateobj.Append(wx.ID_ANY, _('Create array from bathymetry file...'), _('Create array from XYZ (ascii file - real)'))
createvector = self.menucreateobj.Append(wx.ID_FILE7, _('Create vectors...'), _('New vectors'))
createview = self.menucreateobj.Append(wx.ID_ANY, _('Create view...'), _('New view'))
createcloud = self.menucreateobj.Append(wx.ID_FILE8, _('Create clouds...'), _('New cloud of clouds'))
createmanager2D = self.menucreateobj.Append(wx.ID_ANY, _('Create Wolf2D manager ...'), _('New manager 2D'))
createscenario2D = self.menucreateobj.Append(wx.ID_ANY, _('Create scenarios manager ...'), _('New scenarios manager 2D'))
createbcmanager2D = self.menucreateobj.Append(wx.ID_ANY, _('Create BC manager Wolf2D...'), _('New BC manager 2D'))
createpartsystem = self.menucreateobj.Append(wx.ID_ANY, _('Create particle system...'), _('Create a particle system - Lagrangian view'))
create_acceptability = self.menucreateobj.Append(wx.ID_ANY, _('Create acceptability manager...'), _('Create acceptability manager'))
create_inbe = self.menucreateobj.Append(wx.ID_ANY, _('Create INBE manager...'), _('Create INBE manager'))
createdrowning = self.menucreateobj.Append(wx.ID_ANY, _('Create a drowning...'), _('Create a drowning'))
if WOLFPYDIKE_AVAILABLE:
createdike = self.menucreateobj.Append(wx.ID_ANY, _('Create dike...'), _('New dike'))
self.Bind(wx.EVT_MENU, self._on_create_dike, createdike)
self.filemenu.AppendSeparator()
self.menuaddobj = wx.Menu()
self.filemenu.Append(wx.ID_ANY, _('Add...'), self.menuaddobj)
addarray = self.menuaddobj.Append(wx.ID_FILE1, _('Add array...'), _('Add array (binary file - real)'))
addarraycrop = self.menuaddobj.Append(wx.ID_ANY, _('Add array and crop...'), _('Add array and crop (binary file - real)'))
addvector = self.menuaddobj.Append(wx.ID_FILE2, _('Add vectors...'), _('Add vectors'))
addpictcollection = self.menuaddobj.Append(wx.ID_ANY, _('Add picture collection...'), _('Add a collection of pictures'))
addtiles = self.menuaddobj.Append(wx.ID_ANY, _('Add tiles...'), _('Add tiles'))
addimagestiles = self.menuaddobj.Append(wx.ID_ANY, _('Add images tiles...'), _('Add georeferenced images tiles'))
addtilescomp = self.menuaddobj.Append(wx.ID_ANY, _('Add tiles comparator...'), _('Add tiles comparator'))
addtilesgpu = self.menuaddobj.Append(wx.ID_ANY, _('Add tiles GPU...'), _('Add tiles from 2D GPU model -- 2 arrays will be added'))
addcloud = self.menuaddobj.Append(wx.ID_FILE3, _('Add cloud...'), _('Add cloud'))
addclouds = self.menuaddobj.Append(wx.ID_ANY, _('Add clouds...'), _('Add cloud of clouds'))
addtri = self.menuaddobj.Append(wx.ID_ANY, _('Add triangulation...'), _('Add triangulation'))
addprofiles = self.menuaddobj.Append(wx.ID_FILE4, _('Add cross sections...'), _('Add cross sections'))
addres2D = self.menuaddobj.Append(wx.ID_ANY, _('Add Wolf2D results...'), _('Add Wolf 2D results'))
addres2Dgpu = self.menuaddobj.Append(wx.ID_ANY, _('Add Wolf2D GPU results...'), _('Add Wolf 2D GPU results'))
addpartsystem = self.menuaddobj.Append(wx.ID_ANY, _('Add particle system...'), _('Add a particle system - Lagrangian view'))
addbridges = self.menuaddobj.Append(wx.ID_ANY, _('Add bridges...'), _('Add bridges from directory'))
addweirs = self.menuaddobj.Append(wx.ID_ANY, _('Add weirs...'), _('Add bridges from directory'))
addview = self.menuaddobj.Append(wx.ID_ANY, _('Add view...'), _('Add view from project file'))
adddrowning = self.menuaddobj.Append(wx.ID_ANY, _('Add a drowning result...'), _('Add a drowning result'))
if WOLFPYDIKE_AVAILABLE:
adddike = self.menuaddobj.Append(wx.ID_ANY, _('Add dike...'), _('Add dike'))
self.Bind(wx.EVT_MENU, self._on_add_dike, adddike)
self.precomputed_menu = None
if self.default_dem != "":
self.filemenu.AppendSeparator()
self.precomputed_menu = wx.Menu()
_precomp_dem = self.precomputed_menu.Append(wx.ID_ANY, _('Precomputed DEM'))
self.Bind(wx.EVT_MENU, self._on_precomputed_dem, _precomp_dem)
self.filemenu.Append(wx.ID_ANY, _('Precomputed...'), self.precomputed_menu)
if self.default_dtm != "":
if self.precomputed_menu is None:
self.filemenu.AppendSeparator()
self.precomputed_menu = wx.Menu()
self.filemenu.Append(wx.ID_ANY, _('Precomputed...'), self.precomputed_menu)
_precomp_dtm = self.precomputed_menu.Append(wx.ID_ANY, _('Precomputed DTM'))
self.Bind(wx.EVT_MENU, self._on_precomputed_dtm, _precomp_dtm)
self.filemenu.AppendSeparator()
addscan = self.filemenu.Append(wx.ID_FILE5, _('Recursive scan...'), _('Add recursively'))
self.tools_menu = wx.Menu()
self.menu_contour_from_arrays = self.tools_menu.Append(wx.ID_ANY, _("Create contour from checked arrays..."), _("Create contour"))
self.menu_calculator = self.tools_menu.Append(wx.ID_ANY, _("Calculator..."), _("Calculator"))
self.menu_views = self.tools_menu.Append(wx.ID_ANY, _("Memory views..."), _("Memory views"))
self.tools_menu.AppendSeparator()
self.menu_pie = wx.Menu()
self.menu_pie_create = self.menu_pie.Append(wx.ID_ANY, _("Create pie chart..."), _("Create an editable pie chart asset"))
self.menu_pie_edit = self.menu_pie.Append(wx.ID_ANY, _("Edit pie chart..."), _("Open pie chart editor for an existing asset"))
self.menu_pie_load_json = self.menu_pie.Append(wx.ID_ANY, _("Load pie chart JSON..."), _("Load an editable pie chart from JSON"))
self.tools_menu.Append(wx.ID_ANY, _("Pie charts..."), self.menu_pie)
self.menu_bar = wx.Menu()
self.menu_bar_create = self.menu_bar.Append(wx.ID_ANY, _("Create bar chart..."), _("Create an editable bar chart asset"))
self.menu_bar_edit = self.menu_bar.Append(wx.ID_ANY, _("Edit bar chart..."), _("Open bar chart editor for an existing asset"))
self.menu_bar_load_json = self.menu_bar.Append(wx.ID_ANY, _("Load bar chart JSON..."), _("Load an editable bar chart from JSON"))
self.tools_menu.Append(wx.ID_ANY, _("Bar charts..."), self.menu_bar)
self.menu_curve = wx.Menu()
self.menu_curve_create = self.menu_curve.Append(wx.ID_ANY, _("Create curve chart..."), _("Create an editable curve chart asset"))
self.menu_curve_edit = self.menu_curve.Append(wx.ID_ANY, _("Edit curve chart..."), _("Open curve chart editor for an existing asset"))
self.menu_curve_load_json = self.menu_curve.Append(wx.ID_ANY, _("Load curve chart JSON..."), _("Load an editable curve chart from JSON"))
self.tools_menu.Append(wx.ID_ANY, _("Curve charts..."), self.menu_curve)
self.menu_boxplot = wx.Menu()
self.menu_boxplot_create = self.menu_boxplot.Append(wx.ID_ANY, _("Create boxplot..."), _("Create an editable boxplot asset"))
self.menu_boxplot_edit = self.menu_boxplot.Append(wx.ID_ANY, _("Edit boxplot..."), _("Open boxplot editor for an existing asset"))
self.menu_boxplot_load_json = self.menu_boxplot.Append(wx.ID_ANY, _("Load boxplot JSON..."), _("Load an editable boxplot from JSON"))
self.tools_menu.Append(wx.ID_ANY, _("Boxplots..."), self.menu_boxplot)
self.menu_distances = self.tools_menu.Append(wx.ID_ANY, _("Memory distances..."), _("Memory distances"))
self.menu_distances_add = self.tools_menu.Append(wx.ID_ANY, _("Add distances to viewer..."), _("Add memory distances"))
self.menu_digitizer = self.tools_menu.Append(wx.ID_ANY, _("Image digitizer..."), _("Image Digitizer"))
self.tools_menu.AppendSeparator()
self.menu_jupyter_kernel = self.tools_menu.Append(
wx.ID_ANY,
_("Scripting — start Jupyter kernel..."),
_("Start an in-process Jupyter kernel and show the connection file for VSCode / JupyterLab"),
)
self.calculator = None
self.memory_views = None
self._memory_views_gui = None
self.cs_menu = wx.Menu()
self.link_cs_zones = self.cs_menu.Append(wx.ID_ANY, _("Link cross sections to active zones"), _("Use active zones to define some points of the cross sections"))
self.sortalong = self.cs_menu.Append(wx.ID_ANY, _("Sort along..."), _("Sort cross sections along support vector"))
self.cs_menu.AppendSeparator()
self.select_cs = self.cs_menu.Append(wx.ID_ANY, _("Pick one cross section"), _("Select cross section"), kind=wx.ITEM_CHECK)
self.plot_cs = self.cs_menu.Append(wx.ID_ANY, _("Dashboard"), _("Pick the nearest CS and create a dashboard with relations (h-A-P), uniform discharge..."), kind=wx.ITEM_CHECK)
self.cs_menu.AppendSeparator()
self.menumanagebanks = self.cs_menu.Append(wx.ID_ANY, _("Manage banks..."), _("Manage banks"))
self.menucreatenewbanks = self.cs_menu.Append(wx.ID_ANY, _("Create banks from vertices..."), _("Manage banks"))
self.renamecs = self.cs_menu.Append(wx.ID_ANY, _("Rename cross sections from upstream..."), _("Rename"))
self.cs_menu.AppendSeparator()
self.menutrianglecs = self.cs_menu.Append(wx.ID_ANY, _("Triangulate cross sections..."), _("Triangulate"))
self.cs_menu.AppendSeparator()
self.menuexportgltfonebyone = self.cs_menu.Append(wx.ID_ANY, _("Export cross sections to gltf..."), _("Export gltf"))
self.menupontgltfonebyone = self.cs_menu.Append(wx.ID_ANY, _("Create bridge and export gltf..."), _("Bridge gltf"))
self.menuviewerinterpcs = None
self.menuinterpcs = None
self.minmaxmenu = wx.Menu()
self.locminmax = self.minmaxmenu.Append(wx.ID_ANY, _("Local minmax"), _("Adapt colormap on current zoom"), kind=wx.ITEM_CHECK)
paluniform = self.minmaxmenu.Append(wx.ID_ANY, _("Compute and apply unique colormap on all..."), _("Unique colormap"))
paluniform_fomfile = self.minmaxmenu.Append(wx.ID_ANY, _("Load and apply unique colormap on all..."), _("Unique colormap"))
paluniform_inparts = self.minmaxmenu.Append(wx.ID_ANY, _("Force uniform in parts on all..."), _("Uniform in parts"))
pallinear = self.minmaxmenu.Append(wx.ID_ANY, _("Force linear interpolation on all..."), _("Linear colormap"))
self.filemenu.AppendSeparator()
menuquit = self.filemenu.Append(wx.ID_EXIT, _('&Quit\tCTRL+Q'), _('Quit application'))
accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord('Q'), menuquit.GetId())])
self.SetAcceleratorTable(accel_tbl)
self.menubar.Append(self.filemenu, _('&File'))
self.helpmenu = wx.Menu()
item_shortcuts = self.helpmenu.Append(wx.ID_ANY, _('Shortcuts'), _('Shortcuts'))
item_proj = self.helpmenu.Append(wx.ID_ANY, _('Project .proj'), _('A project file ".proj", what is it?'))
item_logs = self.helpmenu.Append(wx.ID_ANY, _('Show logs/informations'), _('Logs'))
item_values = self.helpmenu.Append(wx.ID_ANY, _('Show values'), _('Data/Values'))
item_about = self.helpmenu.Append(wx.ID_ANY, _('About'), _('About'))
item_updates = self.helpmenu.Append(wx.ID_ANY, _('Check for updates'), _('Update?'))
self.menubar.Append(self.helpmenu, _('&Help'))
self.menu_laz()
self.menubar.Append(self.tools_menu, _('&Tools'))
self.menubar.Append(self.cs_menu, _('&Cross sections'))
self.menubar.Append(self.minmaxmenu, _('&Colormap'))
self._analyze_mgr.menu_build()
self.SetMenuBar(self.menubar)
self.Bind(wx.EVT_MENU, self._on_open_project, openitem)
self.Bind(wx.EVT_MENU, self._on_save_project, saveproject)
self.Bind(wx.EVT_MENU, self._on_save_canvas, savecanvas)
self.Bind(wx.EVT_MENU, self._on_copy_canvas, copycanvas)
self.Bind(wx.EVT_MENU, self._on_save_all, saveitem)
self.Bind(wx.EVT_MENU, self._on_save_all_as, saveasitem)
self.Bind(wx.EVT_MENU, self._on_exit, menuquit)
self.Bind(wx.EVT_MENU, self._on_add_array, addarray)
self.Bind(wx.EVT_MENU, self._on_add_vector, addvector)
self.Bind(wx.EVT_MENU, self._on_add_cloud, addcloud)
self.Bind(wx.EVT_MENU, self._on_add_cross_sections, addprofiles)
self.Bind(wx.EVT_MENU, self._on_recursive_scan, addscan)
self.Bind(wx.EVT_MENU, self._on_create_array, createarray)
self.Bind(wx.EVT_MENU, self._on_create_vector, createvector)
self.Bind(wx.EVT_MENU, self._on_gltf_export, exportgltf)
self.Bind(wx.EVT_MENU, self._on_gltf_import, importgltf)
self.Bind(wx.EVT_MENU, self._on_gltf_compare, gltfcompareitem)
self.Bind(wx.EVT_MENU, self._on_gltf_update, updategltf)
self.Bind(wx.EVT_MENU, self._on_sim_create_mb, sim2d)
self.Bind(wx.EVT_MENU, self._on_sim_check_headers, check2D)
self.Bind(wx.EVT_MENU, self._on_sim_create_gpu, sim2dgpu)
self.Bind(wx.EVT_MENU, self._on_sim_create_1d, create1Dmodel)
self.Bind(wx.EVT_MENU, self._on_sim_create_hydro, hydrol)
self.Bind(wx.EVT_MENU, self._on_add_lidaxe, addlidaxe)
self.Bind(wx.EVT_MENU, self._on_set_comparison, setcomparisonitem)
self.Bind(wx.EVT_MENU, self._on_multiviewer, multiview)
self.Bind(wx.EVT_MENU, self._on_viewer3d, viewer3d)
self.Bind(wx.EVT_MENU, self._on_create_array_xyz, createarrayxyz)
self.Bind(wx.EVT_MENU, self._on_create_array_lidar2002, createarray2002)
self.Bind(wx.EVT_MENU, self._on_create_view, createview)
self.Bind(wx.EVT_MENU, self._on_create_clouds, createcloud)
self.Bind(wx.EVT_MENU, self._on_create_manager2d, createmanager2D)
self.Bind(wx.EVT_MENU, self._on_create_scenario2d, createscenario2D)
self.Bind(wx.EVT_MENU, self._on_create_bc_manager, createbcmanager2D)
self.Bind(wx.EVT_MENU, self._on_create_particle_system, createpartsystem)
self.Bind(wx.EVT_MENU, self._on_create_acceptability, create_acceptability)
self.Bind(wx.EVT_MENU, self._on_create_inbe, create_inbe)
self.Bind(wx.EVT_MENU, self._on_create_drowning, createdrowning)
self.Bind(wx.EVT_MENU, self._on_add_array_crop, addarraycrop)
self.Bind(wx.EVT_MENU, self._on_add_picture_collection, addpictcollection)
self.Bind(wx.EVT_MENU, self._on_add_tiles, addtiles)
self.Bind(wx.EVT_MENU, self._on_add_images_tiles, addimagestiles)
self.Bind(wx.EVT_MENU, self._on_add_tiles_comparator, addtilescomp)
self.Bind(wx.EVT_MENU, self._on_add_tiles_gpu, addtilesgpu)
self.Bind(wx.EVT_MENU, self._on_add_clouds, addclouds)
self.Bind(wx.EVT_MENU, self._on_add_triangulation, addtri)
self.Bind(wx.EVT_MENU, self._on_add_wolf2d, addres2D)
self.Bind(wx.EVT_MENU, self._on_add_wolf2d_gpu, addres2Dgpu)
self.Bind(wx.EVT_MENU, self._on_add_particle_system, addpartsystem)
self.Bind(wx.EVT_MENU, self._on_add_bridges, addbridges)
self.Bind(wx.EVT_MENU, self._on_add_weirs, addweirs)
self.Bind(wx.EVT_MENU, self._on_add_view, addview)
self.Bind(wx.EVT_MENU, self._on_add_drowning, adddrowning)
self.Bind(wx.EVT_MENU, self._on_contour_from_arrays, self.menu_contour_from_arrays)
self.Bind(wx.EVT_MENU, self._on_calculator, self.menu_calculator)
self.Bind(wx.EVT_MENU, self._on_memory_views, self.menu_views)
self.Bind(wx.EVT_MENU, self._on_memory_distances, self.menu_distances)
self.Bind(wx.EVT_MENU, self._on_add_distances_viewer, self.menu_distances_add)
self.Bind(wx.EVT_MENU, self._on_image_digitizer, self.menu_digitizer)
self.Bind(wx.EVT_MENU, self._on_jupyter_kernel, self.menu_jupyter_kernel)
self.Bind(wx.EVT_MENU, self.OnCreatePieChart, self.menu_pie_create)
self.Bind(wx.EVT_MENU, self.OnEditPieChart, self.menu_pie_edit)
self.Bind(wx.EVT_MENU, self.OnLoadPieChartJSON, self.menu_pie_load_json)
self.Bind(wx.EVT_MENU, self.OnCreateBarChart, self.menu_bar_create)
self.Bind(wx.EVT_MENU, self.OnEditBarChart, self.menu_bar_edit)
self.Bind(wx.EVT_MENU, self.OnLoadBarChartJSON, self.menu_bar_load_json)
self.Bind(wx.EVT_MENU, self.OnCreateCurveChart, self.menu_curve_create)
self.Bind(wx.EVT_MENU, self.OnEditCurveChart, self.menu_curve_edit)
self.Bind(wx.EVT_MENU, self.OnLoadCurveChartJSON, self.menu_curve_load_json)
self.Bind(wx.EVT_MENU, self.OnCreateBoxplot, self.menu_boxplot_create)
self.Bind(wx.EVT_MENU, self.OnEditBoxplot, self.menu_boxplot_edit)
self.Bind(wx.EVT_MENU, self.OnLoadBoxplotJSON, self.menu_boxplot_load_json)
self.Bind(wx.EVT_MENU, self._on_select_cs, self.select_cs)
self.Bind(wx.EVT_MENU, self._on_plot_cs, self.plot_cs)
self.Bind(wx.EVT_MENU, self._on_sort_along, self.sortalong)
self.Bind(wx.EVT_MENU, self._on_locminmax, self.locminmax)
self.Bind(wx.EVT_MENU, self._on_cs_link_zones, self.link_cs_zones)
self.Bind(wx.EVT_MENU, self._on_cs_manage_banks, self.menumanagebanks)
self.Bind(wx.EVT_MENU, self._on_cs_create_banks, self.menucreatenewbanks)
self.Bind(wx.EVT_MENU, self._on_cs_rename, self.renamecs)
self.Bind(wx.EVT_MENU, self._on_cs_triangulate, self.menutrianglecs)
self.Bind(wx.EVT_MENU, self._on_cs_export_gltf, self.menuexportgltfonebyone)
self.Bind(wx.EVT_MENU, self._on_cs_bridge_gltf, self.menupontgltfonebyone)
self.Bind(wx.EVT_MENU, self._on_colormap_unique, paluniform)
self.Bind(wx.EVT_MENU, self._on_colormap_from_file, paluniform_fomfile)
self.Bind(wx.EVT_MENU, self._on_colormap_uniform_parts, paluniform_inparts)
self.Bind(wx.EVT_MENU, self._on_colormap_linear, pallinear)
self.Bind(wx.EVT_MENU, lambda e: self.print_shortcuts(True), item_shortcuts)
self.Bind(wx.EVT_MENU, lambda e: self.help_project(), item_proj)
self.Bind(wx.EVT_MENU, lambda e: self.check_logging(), item_logs)
self.Bind(wx.EVT_MENU, lambda e: self.check_tooltip(), item_values)
self.Bind(wx.EVT_MENU, lambda e: self.print_About(), item_about)
self.Bind(wx.EVT_MENU, lambda e: self.check_for_updates(), item_updates)
self.Bind(wx.EVT_MENU_HIGHLIGHT, self.OnMenuHighlight)
[docs]
def _init_canvas_tree_layout(self, w: int, h: int) -> None:
"""Create OpenGL canvas, object tree and left layout."""
from wx.glcanvas import WX_GL_RGBA, WX_GL_DOUBLEBUFFER, WX_GL_DEPTH_SIZE, WX_GL_STENCIL_SIZE
_gl_attribs = [WX_GL_RGBA, WX_GL_DOUBLEBUFFER, WX_GL_DEPTH_SIZE, 24, WX_GL_STENCIL_SIZE, 8, 0]
self.canvas = GLCanvas(self, attribList=_gl_attribs)
self.canvas.SetDropTarget(self._dragdrop)
self.context = GLContext(self.canvas)
self.mybackisloaded = False
self.myfrontisloaded = False
self.treelist = TreeListCtrl(self, style=wx.dataview.TL_CHECKBOX | wx.LC_EDIT_LABELS | wx.TR_FULL_ROW_HIGHLIGHT)
self._lbl_selecteditem = StaticText(self, style=wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)
self.root = self.treelist.GetRootItem()
self.treelist.AppendColumn(_('Objects to plot'))
self.myitemsarray = self.treelist.AppendItem(self.root, _("Arrays"))
self.myitemsvector = self.treelist.AppendItem(self.root, _("Vectors"))
self.myitemscloud = self.treelist.AppendItem(self.root, _("Clouds"))
self.myitemslaz = self.treelist.AppendItem(self.root, _("Laz"))
self.myitemstri = self.treelist.AppendItem(self.root, _("Triangulations"))
self.myitemsres2d = self.treelist.AppendItem(self.root, _("Wolf2D"))
self.myitemsps = self.treelist.AppendItem(self.root, _("Particle systems"))
self.myitemsothers = self.treelist.AppendItem(self.root, _("Others"))
self.myitemsviews = self.treelist.AppendItem(self.root, _("Views"))
self.myitemswmsback = self.treelist.AppendItem(self.root, _("WMS-background"))
self.myitemswmsfore = self.treelist.AppendItem(self.root, _("WMS-foreground"))
self.myitemsdrowning = self.treelist.AppendItem(self.root, _("Drowning"))
self.myitemsdike = self.treelist.AppendItem(self.root, _("Dikes"))
self.myitemsinjector = self.treelist.AppendItem(self.root, _("Injectors"))
self.myitemspictcollection = self.treelist.AppendItem(self.root, _("Pictures"))
width, height = self.GetClientSize()
self.bordersize = int((w - width + self.treewidth) / 2)
self.titlesize = h - height - self.bordersize
self.SetSize(w + 2 * self.bordersize + self.treewidth, h + self.bordersize + self.titlesize)
self.canvas.SetSize(width - self.treewidth, height)
self.canvas.SetPosition((self.treewidth, 0))
self.setbounds()
self.leftbox = BoxSizer(orient=wx.VERTICAL)
self.leftbox.Add(self.treelist, 1, wx.LEFT)
self.leftbox.Add(self._lbl_selecteditem, 0, wx.LEFT)
self.treelist.SetSize(self.treewidth, height)
self.CreateStatusBar(1)
self.SetSizer(self.leftbox)
[docs]
def _init_auxiliary_ui_state(self) -> None:
"""Initialize tooltip, notebook placeholders and figure references."""
self.mytooltip = Wolf_Param(self, _("Data/Results"), to_read=False, withbuttons=False, toolbar=False, DestroyAtClosing=False)
self.mytooltip.preserve_view_state = False
self.mytooltip.SetSize(300, 400)
self.mytooltip.prop.SetDescBoxHeight(20)
self.mytooltip.Show(True)
self._tooltip_ctrl_active = False
self._oldpos_tooltip = None
self._overlay_xy_text_renderer = None
self._overlay_xy_text_atlas = None
self.notebookcs = None
self.notebookprof = None
self.notebookbanks = None
self.myaxcs = None
self.myaxprof = None
self.myfigcs = None
self.myfigprof = None
self.cloudmenu = None
self.trianglesmenu = None
self._configuration = None
self.compare_results = None
[docs]
def _init_post_init_objects(self) -> None:
"""Initialize UI bindings and post-UI runtime objects."""
self.InitUI()
self._tmp_vector_distance = None
self._distances = Zones(mapviewer=self, idx=_('Distances/Areas'), parent=self)
self._distances.add_zone(zone(name='memory distances', parent=self._distances))
self.menu_alaro_forecasts()
[docs]
def _init_wintab_context(self) -> None:
"""WinTab pressure context initialisation.
DISABLED: WTOpenA/WTEnable in wintab32.dll corrupt the Windows process
heap on certain Wacom driver versions, causing STATUS_HEAP_CORRUPTION
(0xC0000374) regardless of when the call is made (during init or via
wx.CallAfter after MainLoop). The bug is inside the Wacom driver and
cannot be worked around by changing call timing.
Pressure is still available via wx.MouseEvent.GetPressure() when the
tablet driver operates in Windows Ink mode (Wacom Desktop Center →
"Use Windows Ink"). See _sculpt_manager.get_event_pressure().
To re-enable WinTab (e.g. after a Wacom driver update that fixes the
heap bug), set WolfMapViewer._WINTAB_ENABLED = True before creating
the viewer.
"""
if not WolfMapViewer._WINTAB_ENABLED:
logging.debug(
"WinTab : désactivé (pilote Wacom corrompt le heap Windows). "
"Pression via Windows Ink (e.GetPressure()) si disponible."
)
return
wx.CallAfter(self._do_init_wintab_context)
[docs]
_WINTAB_ENABLED: bool = False # set to True only when the driver bug is fixed
[docs]
def _do_init_wintab_context(self) -> None:
"""Actual WinTab initialisation, called by wx.CallAfter after MainLoop."""
try:
from wolfhece.tablet_wintab import WinTabContext, DllNotFoundError
wt = WinTabContext(self.GetHandle())
wt.enable()
self._wintab = wt
except DllNotFoundError:
import ctypes as _ct
_rdp = bool(_ct.windll.user32.GetSystemMetrics(0x1000))
if _rdp:
logging.debug(
"WinTab : session Remote Desktop détectée, "
"pression stylet non disponible sur machine distante.",
)
else:
logging.warning(
"WinTab : Wintab32.dll introuvable — "
"pilote Wacom non installé sur cette machine.",
)
self._wintab = None
except Exception as _exc:
logging.warning("WinTab pression indisponible : %s", _exc)
self._wintab = None
[docs]
def _check_id_for_fig(self, idx:str):
""" Check if an ID is already used for a figure """
ids = [cur.idx for cur in self.mymplfigs]
if idx in ids:
return True
return False
[docs]
def _create_id_for_fig(self):
idx = 'Figure'
while not self._check_id_for_fig(idx):
idx += '_'
return idx
[docs]
def _validate_id_for_fig(self, idx:str):
""" Validate an ID for a figure """
if idx is None:
return self._create_id_for_fig()
while self._check_id_for_fig(idx):
idx += '_'
return idx
[docs]
def new_fig(self, caption:str, idx:str = None, layout = PRESET_LAYOUTS.DEFAULT, size = (800,600), show:bool = True) -> MplFigViewer:
""" Create a new figure """
if idx is None:
idx = self._dialogs.ask_text(_('Enter an id for the figure'), _('Figure id'), default=_('Figure'), parent=self)
if idx is None:
return None
idx = self._validate_id_for_fig(idx)
else:
idx = self._validate_id_for_fig(idx)
logging.info(f'Figure ID: {idx}')
added_fig = MplFigViewer(layout, idx= idx, mapviewer = self, caption = caption, size= size)
if show:
added_fig.Show()
else:
added_fig.Hide()
self.mymplfigs.append(added_fig)
return added_fig
[docs]
def destroy_fig_by_id(self, idx:str) -> bool:
""" Destroy a figure by its ID """
for id, fig in enumerate(self.mymplfigs):
if fig.idx == idx:
if self.active_fig is fig:
self.active_fig = None
fig.Destroy()
self.mymplfigs.pop(id)
return True
return False
[docs]
def get_fig(self, idx:str) -> MplFigViewer:
""" Get a figure by its ID """
for cur in self.mymplfigs:
if cur.idx == idx:
return cur
return None
[docs]
def list_ids_figs(self) -> list[str]:
""" List all IDs of figures """
return [cur.idx for cur in self.mymplfigs]
@property
[docs]
def viewer_name(self):
return self.GetTitle()
@viewer_name.setter
def viewer_name(self, value):
self.SetTitle(value)
@property
[docs]
def wxlogging(self):
return self._wxlogging
@wxlogging.setter
def wxlogging(self, value):
self._wxlogging = value
[docs]
def check_logging(self):
""" Check if logging window is shown """
if self._wxlogging is None:
logging.info(_('No logging window'))
return
self._wxlogging.Show()
[docs]
def open_hydrological_model(self):
""" Open a hydrological model """
from .PyGui import HydrologyModel
newview = HydrologyModel(splash = False)
[docs]
def create_2D_MB_model(self):
""" Create a 2D model """
from .PyGui import Wolf2DModel
newview = Wolf2DModel(splash = False)
[docs]
def create_2D_GPU_model(self):
""" Create a 2D GPU model """
from .PyGui import Wolf2DGPUModel
newview = Wolf2DGPUModel(splash = False)
[docs]
def get_mapviewer(self):
""" Retourne une instance WolfMapViewer """
return self
[docs]
def do_quit(self):
pass
[docs]
def add_points_to_cloud(self):
""" Add points to cloud """
msg = _('Add points to cloud -- Right click to add points, ALT enables snap, Enter to finish')
self.start_action('add points to cloud', msg)
[docs]
def _on_split_cloud(self, event: wx.Event) -> None:
missing = False
if self.active_cloud is None:
logging.warning(_('No active cloud -- Please activate a cloud first'))
missing = True
if self.active_vector is None:
logging.warning(_('No active vector -- Please activate a vector first'))
missing = True
if missing:
return
self.split_cloud_by_vector()
[docs]
def move_point_in_cloud(self):
"""Interactive move of one cloud point preserving row id."""
if self.active_cloud is None:
logging.warning(_('No active cloud -- Please load data first'))
return
msg = _('Move point in cloud -- Right click to pick/confirm, move mouse to drag, ALT enables snap, Enter to finish')
self.active_cloud_vertex_id = None
self.start_action('move point in cloud', msg)
[docs]
def _cloud_move_pick_tolerance(self, pixel_tol:float = 40.0) -> float:
"""Convert a pixel tolerance to map units for nearest-point picking."""
width_px, height_px = self.canvas.GetSize()
if width_px <= 0 or height_px <= 0:
return max(self.width, self.height) * 0.05
dx = abs(float(self.width) / float(width_px))
dy = abs(float(self.height) / float(height_px))
return float(pixel_tol) * max(dx, dy)
[docs]
def _snap_xy_on_grid(self, x: float, y: float, do_snap: bool = True) -> tuple[float, float]:
return self._assets._snap_xy_on_grid(x, y, do_snap)
[docs]
def _overlay_xy_for_mouse(self, x: float, y: float, altdown: bool) -> tuple[float, float]:
"""Return XY used by OpenGL overlay, with snap when interaction supports it."""
if not altdown:
return x, y
if self.action in (ActionKind.ADD_POINTS_TO_CLOUD, ActionKind.MOVE_POINT_IN_CLOUD, ActionKind.TRANSFORM_ASSET_BOUNDS):
return self._snap_xy_on_grid(x, y, do_snap=True)
return x, y
[docs]
def split_cloud_by_vector(self):
""" Split cloud by vector """
if self.active_cloud is None:
logging.warning(_('No active cloud -- Please load data first'))
return
if self.active_vector is None:
logging.warning(_('No active vector -- Please load data first'))
return
inside_cloud, outside_cloud = self.active_cloud.split_cloud(self.active_vector)
self.add_object('cloud', newobj = inside_cloud, id = inside_cloud.idx)
self.add_object('cloud', newobj = outside_cloud, id = outside_cloud.idx)
[docs]
def get_choices_arrays(self):
"""Boîte de dialogue permettant de choisir une ou plusieurs matrices parmi celles chargées"""
idx = self._dialogs.ask_multi_choice(
_('Choose one or multiple arrays'),
_('Choose'),
[cur.idx for cur in self.myarrays],
parent=self,
)
if idx is None:
return None
mychoices = [self.myarrays[cur] for cur in idx]
return mychoices
[docs]
def pîck_image_tile(self, event: wx.Event):
self._tiles_mgr.on_pick_image_tile(event)
[docs]
def action_qdfidf(self, event: wx.Event):
warnings.warn("action_qdfidf is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[docs]
def action_pictcollections(self, event: wx.Event):
warnings.warn("action_pictcollections is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[docs]
def pick_tile(self, event: wx.Event):
self._tiles_mgr.on_pick_tile(event)
[docs]
def create_data_from_tiles_activevec(self, event: wx.Event):
self._tiles_mgr.on_create_data_from_tiles_activevec(event)
[docs]
def _create_data_from_tiles_common(self):
self._tiles_mgr._create_data_from_tiles_common()
[docs]
def create_data_from_tiles_tmpvec(self, event: wx.Event):
self._tiles_mgr.on_create_data_from_tiles_tmpvec(event)
[docs]
def menu_landuse_landcover(self):
self._lulc.menu_build()
[docs]
def Onmenu_landuse_landcover_importfromfile(self, event: wx.MenuEvent):
warnings.warn("Onmenu_landuse_landcover_importfromfile is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
# --- drowning ---
@property
[docs]
def mydrownings(self):
return self._drowning.mydrownings
@property
[docs]
def active_drowning(self):
return self._drowning.active
@active_drowning.setter
def active_drowning(self, value):
self._drowning.active = value
[docs]
def newdrowning(self, itemlabel):
self._drowning.new_drowning(itemlabel)
# --- dike ---
@property
[docs]
def mydikes(self):
return self._dike.mydikes
@property
[docs]
def active_dike(self):
return self._dike.active
@active_dike.setter
def active_dike(self, value):
self._dike.active = value
[docs]
def new_dike(self, itemlabel):
self._dike.new_dike(itemlabel)
# --- alaro ---
@property
[docs]
def active_alaro(self):
return self._alaro.active
@active_alaro.setter
def active_alaro(self, value):
self._alaro.active = value
# --- lidaxe ---
@property
[docs]
def active_lidaxe(self):
return self._lidaxe.active
@active_lidaxe.setter
def active_lidaxe(self, value):
self._lidaxe.active = value
[docs]
def add_lidaxe(self):
self._lidaxe.activate()
[docs]
def get_canvas_bounds(self, gridsize:float = None):
"""
Retourne les limites de la zone d'affichage
:return: [xmin, ymin, xmax, ymax]
"""
if gridsize is None:
return [self.xmin, self.ymin, self.xmax, self.ymax]
else:
xmin = float(np.rint(self.xmin / gridsize) * gridsize)
ymin = float(np.rint(self.ymin / gridsize) * gridsize)
xmax = float(np.rint(self.xmax / gridsize) * gridsize)
ymax = float(np.rint(self.ymax / gridsize) * gridsize)
return [xmin, ymin, xmax, ymax]
[docs]
def get_bounds(self, gridsize:float = None) -> tuple:
"""
Retourne les limites de la zone d'affichage, voir aussi get_canvas_bounds
:return: ([xmin, xmax], [ymin, ymax])
"""
xmin, ymin, xmax, ymax = self.get_canvas_bounds(gridsize=gridsize)
return ([xmin, xmax], [ymin, ymax])
[docs]
def get_bounds_as_polygon(self, gridsize:float = None) -> vector:
"""
Retourne les limites de la zone d'affichage sous forme de polygone
:return: vector
"""
xmin, ymin, xmax, ymax = self.get_canvas_bounds(gridsize=gridsize)
poly = vector()
poly.add_vertex(wolfvertex(xmin, ymin))
poly.add_vertex(wolfvertex(xmax, ymin))
poly.add_vertex(wolfvertex(xmax, ymax))
poly.add_vertex(wolfvertex(xmin, ymax))
poly.force_to_close()
return poly
[docs]
def _lulc_handle_importfromfile(self, event: wx.MenuEvent):
warnings.warn("_lulc_handle_importfromfile is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[docs]
def _lulc_handle_walous_ocs(self, event: wx.MenuEvent):
warnings.warn("_lulc_handle_walous_ocs is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[docs]
def _lulc_handle_walous_uts(self, event: wx.MenuEvent):
warnings.warn("_lulc_handle_walous_uts is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[docs]
def _add_sim_explorer(self, which:Wolfresults_2D):
""" Add a step chooser """
if which in self.sim_explorers:
logging.warning(_('Step chooser already exists for this result'))
self.sim_explorers[which].Show()
self.sim_explorers[which].Raise()
self.sim_explorers[which].SetFocus()
self.sim_explorers[which].Center()
return
self.sim_explorers[which] = Sim_Explorer(self, which.idx, self, which)
self.sim_explorers[which]._set_all(which.current_result)
[docs]
def _pop_sim_explorer(self, which:Wolfresults_2D):
""" Pop a step chooser """
if which in self.sim_explorers:
self.sim_explorers.pop(which)
logging.debug(_('Pop step chooser for result {}'.format(which.idx)))
else:
logging.warning(_('No step chooser for this result'))
[docs]
def _update_sim_explorer(self, which:Wolfresults_2D = None):
if which is None:
if self.active_res2d is None:
logging.warning(_('No active 2D result -- Please activate a 2D result first'))
return
which = self.active_res2d
if which in self.sim_explorers:
self.sim_explorers[which]._set_all(which.current_result)
[docs]
def menu_landmaps(self):
self._landmap_mgr.menu_build()
[docs]
def change_transparent_color_landmap(self, event: wx.Event):
self._landmap_mgr.on_transparent_color(event)
[docs]
def set_tolerance_landmap(self, event: wx.Event):
self._landmap_mgr.on_set_tolerance(event)
[docs]
def change_colors_landmap(self, event: wx.Event):
self._landmap_mgr.on_change_colors(event)
[docs]
def pick_landmap_full(self, event: wx.Event):
self._landmap_mgr.on_pick_full(event)
[docs]
def pick_landmap_low(self, event: wx.Event):
self._landmap_mgr.on_pick_low(event)
[docs]
def menu_particlesystem(self):
self._particlesystem_mgr.menu_build()
[docs]
def action_menu_particlesystem(self, event: wx.Event):
warnings.warn("action_menu_particlesystem is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[docs]
def update_particlesystem(self, event: wx.Event):
self._particlesystem_mgr.on_timer(event)
[docs]
def get_configuration(self) -> Union[WolfConfiguration, None]:
""" Get global configuration parameters """
# At this point, I'm not too sure about
# which window/frame does what. So to be on
# the safe side, I make sure that the configuration
# menu is active only on the "first" window.
# Moreover, I try to go up the frame/window
# hierarchy to get the configuration (which will therefore
# be treated as a singleton)
if self.wolfparent:
return self.wolfparent.get_configuration()
else:
return None
@property
[docs]
def epsg(self) -> int:
""" Return the EPSG code from configs """
config = self.get_configuration()
if config is None:
logging.debug(_('No configuration found -- Using default EPSG:31370'))
return 31370 # Default EPSG code - Lambert 1970
else:
strcode = config[ConfigurationKeys.EPSG_CODE]
try:
code = int(strcode.lower().replace('epsg:', ''))
return code
except:
logging.error(_('Bad EPSG code in configuration -- Using default EPSG:31370'))
return 31370 # Default EPSG code - Lambert 1970
@property
[docs]
def active_vector_color(self) -> list[int]:
""" Return the active vector color from configs """
config = self.get_configuration()
if config is None:
return [0, 0, 0, 255] # Default black color
else:
return config[ConfigurationKeys.ACTIVE_VECTOR_COLOR]
@property
[docs]
def active_vector_square_size(self) -> list[int]:
""" Return the active vector square size from configs """
config = self.get_configuration()
if config is None:
return 0
else:
return config[ConfigurationKeys.ACTIVE_VECTOR_SIZE_SQUARE]
@property
[docs]
def default_dem(self) -> Path:
""" Return the default DEM file from configs """
config = self.get_configuration()
if config is None:
return Path('')
else:
return Path(config[ConfigurationKeys.DIRECTORY_DEM])
@property
[docs]
def default_dtm(self) -> Path:
""" Return the default DTM file from configs """
config = self.get_configuration()
if config is None:
return Path('')
else:
return Path(config[ConfigurationKeys.DIRECTORY_DTM])
@property
[docs]
def default_laz(self):
""" Return the default LAZ file from configs """
config = self.get_configuration()
if config is None:
return Path('')
else:
return Path(config[ConfigurationKeys.DIRECTORY_LAZ])
@property
[docs]
def default_hece_database(self) -> Path:
""" Return the default HECE database file from configs """
config = self.get_configuration()
if config is None:
return Path('')
else:
return Path(config[ConfigurationKeys.XLSX_HECE_DATABASE])
@property
[docs]
def bkg_color(self):
""" Return the background color from configs """
config = self.get_configuration()
if config is None:
return [255.,255.,255.,255.]
else:
return config[ConfigurationKeys.COLOR_BACKGROUND]
@property
[docs]
def ticks_size(self) -> float:
""" Return the ticks spacing from configs """
config = self.get_configuration()
if config is None:
return 100.
else:
return config[ConfigurationKeys.TICKS_SIZE]
@property
[docs]
def ticks_xrotation(self) -> float:
""" Return the ticks x rotation from configs """
config = self.get_configuration()
if config is None:
return 30.
else:
return config[ConfigurationKeys.TICKS_XROTATION]
@property
[docs]
def ticks_fontsize(self) -> int:
""" Return the ticks font size from configs """
config = self.get_configuration()
if config is None:
return 14
else:
return config[ConfigurationKeys.TICKS_FONTSIZE]
@property
[docs]
def overlay_xy_font_name(self) -> str:
"""Return font name used by OpenGL XY overlay text."""
config = self.get_configuration()
if config is None:
return 'arial.ttf'
value = str(config[ConfigurationKeys.OVERLAY_XY_FONT_NAME]).strip().lower()
# force .ttf extension if not present
if value == '':
return 'arial.ttf'
if not value.endswith('.ttf'):
value += '.ttf'
return value
@property
[docs]
def overlay_xy_font_size(self) -> int:
"""Return font size in pixels used by OpenGL XY overlay text."""
config = self.get_configuration()
if config is None:
return 13
try:
return max(6, int(config[ConfigurationKeys.OVERLAY_XY_FONT_SIZE]))
except Exception:
return 13
@property
@property
[docs]
def overlay_bg_color(self) -> tuple:
"""Return overlay background as an (R, G, B, A) float tuple in [0, 1]."""
config = self.get_configuration()
if config is not None:
try:
c = config[ConfigurationKeys.OVERLAY_BG_COLOR]
return (c[0] / 255.0, c[1] / 255.0, c[2] / 255.0, c[3] / 255.0)
except Exception:
pass
return (0.10, 0.10, 0.12, 0.75)
@property
[docs]
def snap_grid_unit(self) -> float:
"""Return the base snap grid unit [m]."""
config = self.get_configuration()
if config is None:
return max(float(getattr(self, '_snap_grid_unit', 0.01)), 1e-12)
try:
return max(1e-12, float(config[ConfigurationKeys.SNAP_GRID_UNIT]))
except Exception:
return max(float(getattr(self, '_snap_grid_unit', 0.01)), 1e-12)
@property
[docs]
def snap_grid_round_base(self) -> float:
"""Return the coarse base used to align the snap origin [m]."""
config = self.get_configuration()
if config is None:
return max(float(getattr(self, '_snap_grid_round_base', 1000.0)), 1e-12)
try:
return max(1e-12, float(config[ConfigurationKeys.SNAP_GRID_ROUND_BASE]))
except Exception:
return max(float(getattr(self, '_snap_grid_round_base', 1000.0)), 1e-12)
@property
[docs]
def assembly_mode(self) -> str:
""" Return the assembly mode from configs """
config = self.get_configuration()
if config is None:
return 'square'
else:
return config[ConfigurationKeys.ASSEMBLY_IMAGES]
@property
[docs]
def ticks_bounds(self) -> bool:
""" Return the ticks bounds from configs """
config = self.get_configuration()
if config is None:
return True
else:
return config[ConfigurationKeys.TICKS_BOUNDS]
@property
[docs]
def palette_for_copy(self) -> wolfpalette:
""" Return the palette for copy from configs """
config = self.get_configuration()
if config is None:
if self.active_array is not None:
return self.active_array.palette
elif self.active_res2d is not None:
return self.active_res2d.palette
else:
return wolfpalette()
else:
act_array = config[ConfigurationKeys.ACTIVE_ARRAY_PALETTE_FOR_IMAGE]
act_res2d = config[ConfigurationKeys.ACTIVE_RES2D_PALETTE_FOR_IMAGE]
if act_array:
if self.active_array is not None:
return self.active_array.mypal
else:
if self.active_res2d is not None:
logging.warning(_('No active array -- Using active 2D result palette instead'))
return self.active_res2d.mypal
else:
return wolfpalette()
elif act_res2d:
if self.active_res2d is not None:
return self.active_res2d.mypal
else:
if self.active_array is not None:
logging.warning(_('No active 2D result -- Using active array palette instead'))
return self.active_array.mypal
else:
return wolfpalette()
else:
return wolfpalette()
[docs]
def GlobalOptionsDialog(self, event):
handle_configuration_dialog(self, self.get_configuration())
# def import_3dfaces(self):
# dlg = wx.FileDialog(None, _('Choose filename'),
# wildcard='dxf (*.dxf)|*.dxf|gltf (*.gltf)|*.gltf|gltf binary (*.glb)|*.glb|All (*.*)|*.*', style=wx.FD_OPEN)
# ret = dlg.ShowModal()
# if ret == wx.ID_CANCEL:
# dlg.Destroy()
# return
# fn = dlg.GetPath()
# dlg.Destroy()
# mytri = Triangulation(plotted=True,mapviewer=self)
# if fn.endswith('.dxf'):
# mytri.import_dxf(fn)
# elif fn.endswith('.gltf') or fn.endswith('.glb'):
# mytri.import_from_gltf(fn)
# self.add_object('triangulation',newobj=mytri,id=fn)
# self.active_tri = mytri
[docs]
def triangulate_cs(self):
""" Triangulate the active cross sections """
msg = ''
if self.active_zones is None:
msg += _(' The active zones is None. Please activate the desired object !\n')
if self.active_cs is None:
msg += _(' The is no cross section. Please active the desired object or load file !')
if msg != '':
logging.warning(msg)
dlg = wx.MessageBox(msg, 'Required action')
return
_ds = self._dialogs.ask_integer(_('What is the desired size [cm] ?'), 'ds', 'ds size', 100, 1, 10000)
if _ds is None:
return
ds = float(_ds) / 100.
self.set_interp_cs(Interpolators(self.active_zones, self.active_cs, ds))
[docs]
def set_interp_cs(self, obj:Interpolators, add_zones:bool = True):
""" Set the active cross-sections interpolator """
assert isinstance(obj, Interpolators), _('Please provide an Interpolators object')
self.myinterp = obj
if add_zones:
self.add_object('vector', newobj=self.myinterp.myzones, ToCheck=False, id='Interp_mesh')
if self.menuviewerinterpcs is None:
self.menuviewerinterpcs = self.cs_menu.Append(wx.ID_ANY, _("New cloud Viewer..."),
_("Cloud viewer Interpolate"))
self.Bind(wx.EVT_MENU, self._on_viewer_interpcs, self.menuviewerinterpcs)
if self.menuinterpcs is None:
self.menuinterpcs = self.cs_menu.Append(wx.ID_ANY, _("Interpolate on active array..."), _("Interpolate"))
self.Bind(wx.EVT_MENU, self._on_interpcs, self.menuinterpcs)
self.Refresh()
[docs]
def interpolate_cloud(self):
"""
Interpolation d'un nuage de point sur une matrice
Il est possible d'utiliser une autre valeur que la coordonnées Z des vertices
"""
if self.active_cloud is None:
logging.warning(_('No active cloud -- Please activate a cloud first'))
return
if self.active_array is None:
logging.warning(_('No active array -- Please activate an array first'))
return
keyvalue='z'
if self.active_cloud.has_values:
choices = list(self.active_cloud.myvertices[0].keys())
selected = self._dialogs.ask_single_choice("Pick the value to interpolate", "Choices", choices, parent=self)
if selected is None:
return
keyvalue = selected
choices = ["nearest", "linear", "cubic"]
method = self._dialogs.ask_single_choice("Pick an interpolate method", "Choices", choices, parent=self)
if method is None:
return
self.active_cloud.interp_on_array(self.active_array,keyvalue,method)
[docs]
def interpolate_cs(self):
""" Interpolate the active cross sections by interpolators """
if self.active_array is None:
logging.warning(_('No active array -- Please activate an array first'))
return
if self.myinterp is None:
logging.warning(_('No active interpolator -- Please create an interpolator first'))
return
choices = ["nearest", "linear", "cubic"]
method = self._dialogs.ask_single_choice("Pick an interpolate method", "Choices", choices, parent=self)
if method is None:
return
self.myinterp.interp_on_array(self.active_array, method)
[docs]
def interpolate_triangulation(self, keep:Literal['all', 'above', 'below'] = 'all'):
""" Alias to interpolate on triangulation
:param keep: 'all' to keep all points, 'above' to keep only points above the current array's value, 'below' to keep only points below the current array's value
"""
if self.active_array is None:
logging.warning(_('No active array -- Please activate an array first'))
return
if self.active_tri is None:
logging.warning(_('No active triangulation -- Please activate a triangulation first'))
return
self.active_array.interpolate_on_triangulation(self.active_tri.pts, self.active_tri.tri, keep=keep)
[docs]
def compare_cloud2array(self):
"""
Compare the active cloud points to the active array
"""
if self.active_array is None :
logging.warning(_('No active array -- Please activate an array first'))
return
if self.active_cloud is None:
logging.warning(_('No active cloud -- Please activate a cloud first'))
return
self.active_array.compare_cloud(self.active_cloud)
[docs]
def compare_tri2array(self):
if self.active_array is not None and self.active_tri is not None:
self.active_array.compare_tri(self.active_tri)
[docs]
def move_triangles(self):
""" Move the active triangles """
if self.active_tri is None:
logging.warning(_('No active triangles -- Please activate triangles first'))
return
self.start_action('move triangles', 'Move the current triangulation -- Please select 2 points to define the translation vector')
[docs]
def rotate_triangles(self):
""" Rotate the active triangles """
if self.active_tri is None:
logging.warning(_('No active triangles -- Please activate triangles first'))
return
self.start_action('rotate triangles', 'Rotate the current triangulation -- Please select 1 point for the center')
[docs]
def display_canvasogl(self, mpl =True,
ds=0., fig: Figure = None, ax: Axes = None,
clear = True, redraw =True, palette=False, title=''):
"""
This method takes a matplotlib figure and axe and,
returns a clear screenshot of the information displayed in the wolfpy GUI.
"""
self.Paint()
myax = ax
if redraw:
if clear:
myax.clear()
if self.SetCurrentContext():
glPixelStorei(GL_PACK_ALIGNMENT, 1)
data = glReadPixels(0,0,self.canvaswidth, self.canvasheight, GL_RGBA,GL_UNSIGNED_BYTE)
myimage: Image.Image
myimage = Image.frombuffer("RGBA",(self.canvaswidth,self.canvasheight),data)
myimage = myimage.transpose(1)
if mpl:
if ds ==0.:
ds = self.ticks_size
extent = (self.xmin, self.xmax, self.ymin, self.ymax)
myax.imshow(myimage, origin ='upper', extent=extent)
x1 = np.ceil((self.xmin//ds)*ds)
if x1 < self.xmin:
x1 += ds
x2 = int((self.xmax//ds)*ds)
if x2 >self.xmax:
x2 -= ds
y1 = np.ceil((self.ymin//ds)*ds)
if y1 < self.ymin:
y1 += ds
y2 = int((self.ymax // ds) * ds)
if y2 > self.ymax:
y2 -= ds
x_label_list = np.linspace(x1,x2, int((x2-x1)/ds) +1, True)
if self.ticks_bounds:
x_label_list = np.insert(x_label_list,0,self.xmin)
x_label_list = np.insert(x_label_list,-1, self.xmax)
x_label_list = np.unique(x_label_list)
y_label_list = np.linspace(y1, y2, int((y2 - y1) / ds) + 1, True)
if self.ticks_bounds:
y_label_list = np.insert(y_label_list, 0, self.ymin)
y_label_list = np.insert(y_label_list, -1, self.ymax)
y_label_list = np.unique(y_label_list)
myax.set_xticks(x_label_list)
myax.set_yticks(y_label_list)
myax.set_xticklabels(FormatStrFormatter('%.1f').format_ticks(x_label_list),
fontsize = self.ticks_fontsize, rotation = self.ticks_xrotation)
myax.set_yticklabels(FormatStrFormatter('%.1f').format_ticks(y_label_list),
fontsize = self.ticks_fontsize)
myax.xaxis.set_ticks_position('top')
myax.xaxis.set_label_position('top')
myax.set_xlabel('X ($m$)')
myax.set_ylabel('Y ($m$)')
myax.xaxis.set_ticks_position('bottom')
myax.xaxis.set_label_position('bottom')
if title!='':
myax.set_title(title)
# try:
# fig.tight_layout()
# except:
# pass
fig.canvas.draw()
fig.canvas.flush_events()
else:
logging.warning( "Can't open the clipboard", "Error")
[docs]
def get_mpl_plot(self, center = [0., 0.], width = 500., height = 500., title='', toshow=True) -> tuple[Figure, Axes]:
"""
Récupère un graphique matplotlib sur base de la fenêtre OpenGL et de la palette de la matrice/résultat actif.
"""
self.zoom_on(center=center, width=width, height= height, canvas_height=self.canvasheight, forceupdate=True)
fig,axes = plt.subplots(1,2, gridspec_kw={'width_ratios': [20, 1]})
self.display_canvasogl(fig=fig,ax=axes[0])
palette = self.palette_for_copy
palette.export_image(None, h_or_v='v', figax=(fig,axes[1]))
# if self.active_array is not None:
# self.active_array.mypal.export_image(None, h_or_v='v', figax=(fig,axes[1]))
# elif self.active_res2d is not None:
# self.active_res2d.mypal.export_image(None, h_or_v='v', figax=(fig,axes[1]))
axes[0].xaxis.set_ticks_position('bottom')
axes[0].xaxis.set_label_position('bottom')
fig.set_size_inches(12,10)
fontsize(axes[0], 12)
fontsize(axes[1], 12)
if title!='':
axes[0].set_title(title)
fig.tight_layout()
if toshow:
fig.show()
return fig, axes
[docs]
def create_video(self, fn:str = '', framerate:int = 0, start_step:int = 0, end_step:int = 0, every:int = 0):
"""
Création d'une vidéo sur base des résultats
"""
try:
import cv2
except:
logging.error(_('Please install opencv-python'))
return
dlg = Sim_VideoCreation(None, title = _('Video creation'), sim= self.active_res2d, mapviewer=self)
ret = dlg.ShowModal()
if ret == wx.ID_CANCEL:
dlg.Destroy()
return
fn, framerate, start_step, end_step, interval, fontsize, fontcolor, timeposition, date_ref, tz_ref, tz_plot, check_date = dlg.get_values()
dlg.Destroy()
# Adjust date_ref to tz_plot
date_ref += tz_plot - tz_ref
times,steps = self.active_res2d.get_times_steps()
framesize = (int(self.canvaswidth), int(self.canvasheight))
video = cv2.VideoWriter(fn, cv2.VideoWriter_fourcc(*'XVID'), framerate, framesize)
def str_time_or_date(tsec:float, check_date:bool, date_ref:datetime) -> str:
if check_date:
cur_date = date_ref + timedelta(seconds=int(tsec))
return 'Date {:04}-{:02}-{:02} {:02}:{:02}:{:02}'.format(cur_date.year, cur_date.month, cur_date.day,
cur_date.hour, cur_date.minute, cur_date.second)
else:
el_time = str(timedelta(seconds=tsec))
return 'Time {:0>8} s'.format(el_time)
el_time = str(timedelta(seconds=int(times[self.active_res2d.current_result])))
zones_time = Zones(mapviewer=self)
self.add_object('vector', newobj=zones_time, ToCheck=True, id='__VideoTime__')
zone_time = zone(name='Time')
vec_time = vector(name='Time')
zones_time.add_zone(zone_time, forceparent=True)
zone_time.add_vector(vec_time, forceparent=True)
if timeposition == 'top-center':
x = (self.xmax+self.xmin)/2.
y = self.ymax - 0.05*(self.ymax-self.ymin)
elif timeposition == 'bottom-center':
x = (self.xmax+self.xmin)/2.
y = self.ymin + 0.05*(self.ymax-self.ymin)
elif timeposition == 'top-left':
x = self.xmin + 0.15*(self.xmax-self.xmin)
y = self.ymax - 0.05*(self.ymax-self.ymin)
elif timeposition == 'bottom-left':
x = self.xmin + 0.15*(self.xmax-self.xmin)
y = self.ymin + 0.05*(self.ymax-self.ymin)
elif timeposition == 'top-right':
x = self.xmax - 0.15*(self.xmax-self.xmin)
y = self.ymax - 0.05*(self.ymax-self.ymin)
elif timeposition == 'bottom-right':
x = self.xmax - 0.15*(self.xmax-self.xmin)
y = self.ymin + 0.05*(self.ymax-self.ymin)
else:
x = (self.xmax+self.xmin)/2.
y = self.ymax - 0.05*(self.ymax-self.ymin)
vec_time.add_vertex(wolfvertex(x,y))
vec_time.set_legend_position(x,y)
vec_time.set_legend_text(str_time_or_date(times[self.active_res2d.current_result], check_date, date_ref))
vec_time.set_legend_visible(True)
vec_time.myprop.legendfontsize = fontsize
vec_time.myprop.legendcolor = getIfromRGB(fontcolor)
for curmodel in self.iterator_over_objects(draw_type.RES2D):
curmodel: Wolfresults_2D
curmodel.step_interval_results = interval
all_steps = range(0, int((end_step-start_step) // interval) + 1)
pgbar = self._dialogs.create_progress(
_('Video creation'),
_('Creating video...'),
len(all_steps),
None,
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_ESTIMATED_TIME | wx.PD_REMAINING_TIME,
)
self.read_one_result(start_step-1)
for idx in tqdm(all_steps, desc=_('Creating video')):
image = self.get_canvas_as_image()
video.write(cv2.cvtColor(np.asarray(image),cv2.COLOR_RGB2BGR))
self.simul_next_step()
vec_time.set_legend_text(str_time_or_date(times[self.active_res2d.current_result], check_date, date_ref))
if not pgbar.update(idx + 1):
break
pgbar.close()
video.release()
for curmodel in self.iterator_over_objects(draw_type.RES2D):
curmodel: Wolfresults_2D
curmodel.step_interval_results = 1
self.removeobj_from_id('__VideoTime__')
[docs]
def get_canvas_as_image(self) -> Image.Image:
"""
Récupère la fenêtre OpenGL sous forme d'image
"""
self.Paint(ignore_overlays=True)
if self.SetCurrentContext():
glPixelStorei(GL_PACK_ALIGNMENT, 1)
data = glReadPixels(0, 0, self.canvaswidth, self.canvasheight, GL_RGBA, GL_UNSIGNED_BYTE)
myimage: Image.Image
myimage = Image.frombuffer("RGBA", (self.canvaswidth, self.canvasheight), data)
myimage = myimage.transpose(1)
return myimage
[docs]
def copy_canvasogl(self,
mpl:bool= True,
ds:float= 0.,
figsizes= [10.,10.],
palette:wolfpalette = None):
"""
Generate image based on UI context and copy to the Clipboard
:param mpl: Using Matplolib as renderer. Defaults to True.
:type mpl: bool, optional
:parem ds: Ticks size. Defaults to 0..
:type ds: float, optional
:parem figsizes: fig size in inches
:type figsizes: list, optional
"""
if wx.TheClipboard.Open():
self.Paint(ignore_overlays = True)
if self.SetCurrentContext():
myimage = self.get_canvas_as_image()
metadata = PngInfo()
metadata.add_text('xmin', str(self.xmin))
metadata.add_text('ymin', str(self.ymin))
metadata.add_text('xmax', str(self.xmax))
metadata.add_text('ymax', str(self.ymax))
if mpl:
if ds == 0.:
ds = self.ticks_size # Global parameters
if ds == 0.:
ds = 100.
nb_ticks_x = (self.xmax - self.xmin) // ds
nb_ticks_y = (self.ymax - self.ymin) // ds
if nb_ticks_x > 10 or nb_ticks_y > 10:
logging.error(_('Too many ticks for the image. Please raise the ticks size in the global options.'))
self._dialogs.show_message(_('Too many ticks for the image. Please raise the ticks size in the global options.'), _('Error'), style=DialogStyles.OK)
wx.TheClipboard.Close()
return
# Création d'un graphique Matplotlib
extent = (self.xmin, self.xmax, self.ymin, self.ymax)
fig, ax = plt.subplots(1, 1)
w, h = [self.width, self.height]
neww = figsizes[0]
newh = h/w * figsizes[0]
fig.set_size_inches(neww, newh)
pos = ax.imshow(myimage,
origin='upper',
extent=extent)
x1 = np.ceil((self.xmin // ds) * ds)
if x1 < self.xmin:
x1 += ds
x2 = int((self.xmax // ds) * ds)
if x2 > self.xmax:
x2 -= ds
y1 = np.ceil((self.ymin // ds) * ds)
if y1 < self.ymin:
y1 += ds
y2 = int((self.ymax // ds) * ds)
if y2 > self.ymax:
y2 -= ds
x_label_list = np.linspace(x1, x2, int((x2 - x1) / ds) + 1, True)
if self.ticks_bounds:
x_label_list = np.insert(x_label_list, 0, self.xmin)
x_label_list = np.insert(x_label_list, -1, self.xmax)
x_label_list = np.unique(x_label_list)
y_label_list = np.linspace(y1, y2, int((y2 - y1) / ds) + 1, True)
if self.ticks_bounds:
y_label_list = np.insert(y_label_list, 0, self.ymin)
y_label_list = np.insert(y_label_list, -1, self.ymax)
y_label_list = np.unique(y_label_list)
ax.set_xticks(x_label_list)
ax.set_yticks(y_label_list)
ax.set_xticklabels(plt.FormatStrFormatter('%.1f').format_ticks(x_label_list),
fontsize = self.ticks_fontsize, rotation = self.ticks_xrotation)
ax.set_yticklabels(plt.FormatStrFormatter('%.1f').format_ticks(y_label_list),
fontsize = self.ticks_fontsize)
ax.set_xlabel('X ($m$)')
ax.set_ylabel('Y ($m$)')
fig.tight_layout()
#création d'un'buffers
buf = io.BytesIO()
#sauvegarde de la figure au format png
fig.savefig(buf, format='png')
#déplacement au début du buffer
buf.seek(0)
#lecture du buffer et conversion en image avec PIL
im = Image.open(buf)
if palette is None:
palette = self.palette_for_copy
# if self.active_array is not None:
# palette = self.active_array.mypal
# elif self.active_res2d is not None:
# palette = self.active_res2d.mypal
if palette is not None:
if palette.values is not None:
bufpal = io.BytesIO()
palette.export_image(bufpal,'v')
try:
bufpal.seek(0)
#lecture du buffer et conversion en image avec PIL
impal = Image.open(bufpal)
except Exception as e:
text = _('Error while creating the colormap/palette image !')
text += '\n'
text += _('Please check if an array or a 2D result is active !')
logging.error(text)
self._dialogs.show_message(text, _('Error'), style=DialogStyles.OK)
return
impal = impal.resize((int(impal.size[0]*im.size[1]*.8/impal.size[1]),int(im.size[1]*.8)))
imnew = Image.new('RGB',(im.size[0]+impal.size[0], im.size[1]), (255,255,255))
# On colle l'image du buffer et la palette pour ne former qu'une seul image à copier dans le clipboard
imnew.paste(im.convert('RGB'),(0,0))
imnew.paste(impal.convert('RGB'),(im.size[0]-10, int((im.size[1]-impal.size[1])/3)))
im=imnew
bufpal.close()
else:
imnew = Image.new('RGB', (im.size[0], im.size[1]), (255,255,255))
# On colle l'image du buffer et la palette pour ne former qu'une seul image à copier dans le clipboard
imnew.paste(im.convert('RGB'),(0,0))
im=imnew
else:
imnew = Image.new('RGB', (im.size[0], im.size[1]), (255,255,255))
# On colle l'image du buffer et la palette pour ne former qu'une seul image à copier dans le clipboard
imnew.paste(im.convert('RGB'),(0,0))
im=imnew
#création d'un objet bitmap wx
wxbitmap = wx.Bitmap().FromBuffer(im.width,im.height,im.tobytes())
# objet wx exportable via le clipboard
dataobj = wx.BitmapDataObject()
dataobj.SetBitmap(wxbitmap)
wx.TheClipboard.SetData(dataobj)
wx.TheClipboard.Close()
fig.set_visible(False)
buf.close()
return fig, ax, im
else:
""" Création d'un objet bitmap wx sur base du canvas
et copie dans le clipboard
"""
# wxbitmap = wx.Bitmap().FromBuffer(myimage.width,myimage.height,myimage.tobytes())
wxbitmap = wx.Bitmap().FromBufferRGBA(myimage.width,myimage.height,myimage.tobytes())
# objet wx exportable via le clipboard
dataobj = wx.BitmapDataObject()
dataobj.SetBitmap(wxbitmap)
wx.TheClipboard.SetData(dataobj)
wx.TheClipboard.Close()
return myimage
else:
wx.MessageBox("Can't open the clipboard", "Error")
[docs]
def save_canvasogl(self,
fn:str='',
mpl:bool=True,
ds:float=0.,
dpi:int= 300,
add_title:bool = False,
figsizes= [10.,10.],
arrayid_as_title:bool = False,
resid_as_title:bool = False):
"""
Sauvegarde de la fenêtre d'affichage dans un fichier
:param fn: File name (.png or .jpg file)
:param mpl: Using Matplotlib as renderer
:param ds: Ticks interval
"""
# FIXME : SHOULD BE MERGEd WITH copy_canvasogl
fn = str(fn)
if fn == '':
chosen = self._dialogs.ask_file_save(
_('Choose file name'),
wildcard='PNG (*.png)|*.png|JPG (*.jpg)|*.jpg',
parent=self,
)
if chosen is None:
return
fn = chosen
elif not fn.endswith('.png'):
fn += '.png'
if self.SetCurrentContext():
self.Paint(ignore_overlays=True)
if mpl:
if ds == 0.:
_ds = self._dialogs.ask_integer(
_("xmin : {:.3f} \nxmax : {:.3f} \nymin : {:.3f} \nymax : {:.3f} \n\n dx : {:.3f}\n dy : {:.3f}").format(
self.xmin, self.xmax, self.ymin, self.ymax, self.xmax - self.xmin,
self.ymax - self.ymin),
_("Interval [m]"), _("Ticks interval ?"), 500, 1, 10000)
if _ds is None:
return
ds = float(_ds)
# Création d'un graphique Matplotlib
extent = (self.xmin, self.xmax, self.ymin, self.ymax)
fig, ax = plt.subplots(1, 1)
w, h = [self.width, self.height]
neww = figsizes[0]
newh = h/w * figsizes[0]
fig.set_size_inches(neww, newh)
pot_title = self.viewer_name
if arrayid_as_title:
pot_title = self.active_array.idx
if resid_as_title:
pot_title = self.active_res2d.idx
self.display_canvasogl(fig=fig,
ax=ax,
title=pot_title if add_title else '',
ds = ds)
#création d'un'buffers
buf = io.BytesIO()
#sauvegarde de la figure au format png
fig.savefig(buf, format='png')
#déplacement au début du buffer
buf.seek(0)
#lecture du buffer et conversion en image avec PIL
im = Image.open(buf)
if self.palette_for_copy.values is not None:
bufpal = io.BytesIO()
self.palette_for_copy.export_image(bufpal,'v')
bufpal.seek(0)
#lecture du buffer et conversion en image avec PIL
impal = Image.open(bufpal)
impal = impal.resize((int(impal.size[0]*im.size[1]*.8/impal.size[1]),int(im.size[1]*.8)))
imnew = Image.new('RGB',(im.size[0]+impal.size[0], im.size[1]), (255,255,255))
# On colle l'image du buffer et la palette pour ne former qu'une seul image à copier dans le clipboard
imnew.paste(im.convert('RGB'),(0,0))
imnew.paste(impal.convert('RGB'),(im.size[0]-10, int((im.size[1]-impal.size[1])/3)))
im=imnew
bufpal.close()
else:
imnew = Image.new('RGB', (im.size[0], im.size[1]), (255,255,255))
# On colle l'image du buffer et la palette pour ne former qu'une seul image à copier dans le clipboard
imnew.paste(im.convert('RGB'),(0,0))
im=imnew
im.save(fn, dpi=(dpi, dpi))
fig.set_visible(False)
buf.close()
else:
metadata = PngInfo()
metadata.add_text('xmin', str(self.xmin))
metadata.add_text('ymin', str(self.ymin))
metadata.add_text('xmax', str(self.xmax))
metadata.add_text('ymax', str(self.ymax))
myimage = self.get_canvas_as_image()
myimage.save(fn, pnginfo=metadata)
return fn, ds
else:
raise NameError(
'Opengl setcurrent -- maybe a conflict with an existing opengl32.dll file - please rename the opengl32.dll in the libs directory and retry')
[docs]
def reporting(self, dir=''):
""" First attempt to create a reporting.
!! Must be improved !!
"""
if dir == '':
dir = self._dialogs.ask_directory("Choose directory to store reporting", style=wx.FD_SAVE, parent=self)
if dir is None:
return
myppt = Presentation(__file__)
slide = myppt.slides.add_slide(0)
for curzone in self.myzones:
for curvec in curzone.myvectors:
curvec: vector
if curvec.nbvertices > 1:
oldwidth = curvec.myprop.width
curvec.myprop.width = 4
myname = curvec.myname
self.Activate_vector(curvec)
if self.linked:
for curview in self.linkedList:
title = curview.GetTitle()
curview.zoomon_activevector()
fn = path.join(dir, title + '_' + myname + '.png')
curview.save_canvasogl(fn)
else:
self.zoomon_activevector()
fn = path.join(dir, myname + '.png')
self.save_canvasogl(fn)
fn = path.join(dir, 'palette_v_' + myname + '.png')
self.active_array.mypal.export_image(fn, 'v')
fn = path.join(dir, 'palette_h_' + myname + '.png')
self.active_array.mypal.export_image(fn, 'h')
curvec.myprop.width = oldwidth
[docs]
def InitUI(self):
""" Initialisation de l'interface utilisateur """
self.Bind(wx.EVT_SIZE, self.OnSize)
self.Bind(wx.EVT_CLOSE, self.OnClose)
# self.canvas.Bind(wx.EVT_CONTEXT_MENU, self.OnShowPopup)
self.canvas.Bind(wx.EVT_PAINT, self.OnPaint)
self.canvas.Bind(wx.EVT_ERASE_BACKGROUND, lambda e: None) # suppress system background erase to prevent flickering
self.canvas.Bind(wx.EVT_CHAR_HOOK, self.OnHotKey)
self.canvas.Bind(wx.EVT_BUTTON, self.On_Mouse_Button)
self.canvas.Bind(wx.EVT_RIGHT_DCLICK, self.On_Right_Double_Clicks)
self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.On_Left_Double_Clicks)
self.canvas.Bind(wx.EVT_LEFT_DOWN, self.On_Mouse_Left_Down)
self.canvas.Bind(wx.EVT_LEFT_UP, self.On_Mouse_Left_Up)
self.canvas.Bind(wx.EVT_MIDDLE_DOWN, self.On_Mouse_Left_Down)
self.canvas.Bind(wx.EVT_RIGHT_DOWN, self.On_Mouse_Right_Down)
self.canvas.Bind(wx.EVT_RIGHT_UP, self.On_Mouse_Right_Up)
self.canvas.Bind(wx.EVT_MOTION, self.On_Mouse_Motion)
self.canvas.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.On_Mouse_Button)
self.treelist.Bind(dataview.EVT_TREELIST_ITEM_CHECKED, self.OnCheckItem)
self.treelist.Bind(dataview.EVT_TREELIST_ITEM_ACTIVATED, self.OnActivateTreeElem)
self.treelist.Bind(dataview.EVT_TREELIST_ITEM_CONTEXT_MENU, self.OntreeRight)
self.treelist.Bind(wx.EVT_CHAR_HOOK, self.OnHotKey)
self.treelist.Bind(dataview.EVT_TREELIST_SELECTION_CHANGED,self.OnSelectItem)
# Drag & drop reordering in the tree
self._tree_drag_init()
# dispo dans wxpython 4.1 self.Bind(wx.EVT_GESTURE_ZOOM,self.OnZoomGesture)
self.Centre()
self.mybc = []
self.myarrays = []
self.mypartsystems = []
self.myvectors = []
self.mytiles = []
self.myimagestiles = []
self.myclouds = []
self.mytri = []
self.myothers = []
self.myviews = []
self.mywmsback = []
self.mywmsfore = []
self.myres2D = []
self.myviewers3d = []
self.myviewerslaz = []
self.mylazdata = []
self.mypicturecollections = []
self.myinjectors = []
self.mymplfigs = []
self.sim_explorers = {}
# liste des éléments modifiable dans l'arbre
self.all_lists = [self.myarrays, self.myvectors, self.myclouds, self.mytri, self.myothers, self.myviews, self.myres2D, self.mytiles, self.myimagestiles, self.mypartsystems, self.myviewers3d, self.myviewerslaz, self._dike.mydikes, self._drowning.mydrownings, self.myinjectors]
self.menu_options = wx.Menu()
self._change_title = self.menu_options.Append(wx.ID_ANY, _('Change title'), _('Change title of the window'))
self.Bind(wx.EVT_MENU, self.OnChangeTitle, self._change_title)
if self.get_configuration() is not None:
# see PyGui.py if necessary
self.menubar.Append(self.menu_options, _('Options'))
self.option_global = self.menu_options.Append(wx.ID_ANY,_("Global"),_("Modify global options"))
self.Bind(wx.EVT_MENU, self.GlobalOptionsDialog, self.option_global)
self.menu_1to9 =self.menu_options.Append(wx.ID_ANY, _('Colors for selections 1->9'), _('Selections'))
self.Bind(wx.EVT_MENU, self.colors1to9.change_colors, self.menu_1to9)
self.menu_qdfidf()
self.Show(True)
[docs]
def OnChangeTitle(self, e):
"""
Change the title of the window
"""
new_title = self._dialogs.ask_text(_('Enter the new title'), _('Change title'), default=self.GetTitle())
if new_title is not None:
self.SetTitle(new_title)
[docs]
def OnSize(self, e):
"""
Redimensionnement de la fenêtre
"""
if self.regular:
# retrouve la taille de la fenêtre
width, height = self.GetClientSize()
# enlève la barre d'arbre
width -= self.treewidth
# définit la taille de la fenêtre graphique OpenGL et sa position (à droite de l'arbre)
self.canvas.SetSize(width, height)
self.canvas.SetPosition((self.treewidth, 0))
# calcule les limites visibles sur base de la taille de la fenêtre et des coefficients sx sy
self.setbounds()
# fixe la taille de l'arbre (notamment la hauteur)
# self.treelist.SetSize(self.treewidth,height)
e.Skip()
[docs]
def center_view_on(self, cx, cy):
"""
Center the view on the point of (map) coordinates (x,y)
"""
self._center_x, self._center_y = cx, cy
# retrouve la taille de la fenêtre OpenGL
width, height = self.canvas.GetSize()
# calcule la taille selon X et Y en coordonnées réelles
width = width / self.sx
height = height / self.sy
# retrouve les bornes min et max sur base de la valeur centrale qui est censée ne pas bouger
self.xmin = self._center_x - width / 2.
self.xmax = self.xmin + width
self.ymin = self._center_y - height / 2.
self.ymax = self.ymin + height
[docs]
def setbounds(self, updatescale=True):
"""
Calcule les limites visibles de la fenêtre graphique sur base des
facteurs d'échelle courants
"""
if updatescale:
self.updatescalefactors()
# retrouve la taille de la fenêtre OpenGL
width, height = self.canvas.GetSize()
self.canvaswidth = width
self.canvasheight = height
# calcule la taille selon X et Y en coordonnées réelles
width = width / self.sx
height = height / self.sy
# retrouve les bornes min et max sur base de la valeur centrale qui est censée ne pas bouger
self.xmin = self._center_x - width / 2.
self.xmax = self.xmin + width
self.ymin = self._center_y - height / 2.
self.ymax = self.ymin + height
self.width = width
self.height = height
self._center_x = self.xmin + width / 2.
self._center_y = self.ymin + height / 2.
self.updatescalefactors()
else:
# retrouve les bornes min et max sur base de la valeur centrale qui est censée ne pas bouger
self.xmin = self._center_x - self.width / 2.
self.xmax = self.xmin + self.width
self.ymin = self._center_y - self.height / 2.
self.ymax = self.ymin + self.height
self.mybackisloaded = False
self.myfrontisloaded = False
self.Refresh()
self.mimicme()
[docs]
def setsizecanvas(self,width,height):
""" Redimensionne la fenêtre graphique """
self.canvas.SetClientSize(width, height)
[docs]
def updatescalefactors(self):
""" Mise à jour des facteurs d'échelle
This one updates the scale factors based on the relative sizes
of the GLCanvas and the footprint that should fit in it.
"""
width, height = self.canvas.GetSize()
self.sx = 1
self.sy = 1
if self.width > 0 and width >0 :
self.sx = float(width) / self.width
if self.height > 0 and height > 0 :
self.sy = float(height) / self.height
self.sx = min(self.sx, self.sy)
self.sy = self.sx
[docs]
def add_viewer_and_link(self):
""" Ajout d'une nouvelle fenêtre de visualisation et liaison avec la fenêtre courante """
newcap = self._dialogs.ask_text(_('Enter a caption for the new window'), parent=self)
if newcap is None:
return
newview = WolfMapViewer(None, newcap, w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
newview.add_grid()
newview.add_WMS()
if self.linkedList is None:
self.linkedList = [self]
self.linkedList.append(newview)
for curview in self.linkedList:
curview.linked = True
curview.linkedList = self.linkedList
curview.link_shareopsvect = False
logging.info(_('New viewer added and linked'))
[docs]
def add_grid(self):
""" Ajout d'une grille """
mygrid = Grid(1000.)
self.add_object('vector', newobj=mygrid, ToCheck=False, id='Grid')
[docs]
def add_WMS(self):
""" Ajout de couches WMS """
xmin = 0
xmax = 0
ymin = 0
ymax = 0
orthos = {'IMAGERIE': {'Last one': 'ORTHO_LAST',
'1971': 'ORTHO_1971', '1994-2000': 'ORTHO_1994_2000',
'2006-2007': 'ORTHO_2006_2007',
'2009-2010': 'ORTHO_2009_2010',
'2012-2013': 'ORTHO_2012_2013',
'2015': 'ORTHO_2015',
'2016': 'ORTHO_2016',
'2017': 'ORTHO_2017',
'2018': 'ORTHO_2018',
'2019': 'ORTHO_2019',
'2020': 'ORTHO_2020',
'2021': 'ORTHO_2021',
'2022 printemps': 'ORTHO_2022_PRINTEMPS',
'2022 été': 'ORTHO_2022_ETE',
'2023 été': 'ORTHO_2023_ETE',
}}
data_2021 = {'EAU': {'IDW': 'ZONES_INONDEES_IDW',
'Emprise': 'ZONES_INONDEES',
'Emprise wo Alea': 'ZONES_INONDEES_wo_alea'}}
lifewatch = {'LW_ecotopes_lc_hr_raster': {'2006': '2006',
'2010': '2010',
'2015': '2015',
'2018': '2018',
'2019': '2019',
'2020': '2020',
'2021': '2021',
'2022': '2022',
}}
"""cat:Literal['orthoimage_coverage',
'orthoimage_coverage_2016',
'orthoimage_coverage_2017',
'orthoimage_coverage_2018',
'orthoimage_coverage_2019',
'orthoimage_coverage_2020',
'orthoimage_coverage_2021',
'orthoimage_coverage_2022']
"""
ign_belgique = {'Orthophotos': {'Last': 'orthoimage_coverage',
'2016': 'orthoimage_coverage_2016',
'2017': 'orthoimage_coverage_2017',
'2018': 'orthoimage_coverage_2018',
'2019': 'orthoimage_coverage_2019',
'2020': 'orthoimage_coverage_2020',
'2021': 'orthoimage_coverage_2021',
'2022': 'orthoimage_coverage_2022',}}
""" ['crossborder',
'crossborder_grey',
'overlay',
'topo',
'topo_grey']"""
ign_cartoweb = {'CartoWeb': {'Crossborder': 'crossborder',
'Crossborder Grey': 'crossborder_grey',
'Overlay': 'overlay',
'Topographic': 'topo',
'Topographic Grey': 'topo_grey',}}
ign_postflood2021 = {'PostFlood2021': {'Flood 2021': 'orthoimage_flood'}}
"""
['10_m_u__wind_component',
'10_m_v__wind_component',
'2_m_Max_temp_since_ppp',
'2_m_Min_temp_since_ppp',
'2_m_dewpoint_temperature',
'2_m_temperature',
'2m_Relative_humidity',
'Convective_rain',
'Convective_snow',
'Geopotential',
'Inst_flx_Conv_Cld_Cover',
'Inst_flx_High_Cld_Cover',
'Inst_flx_Low_Cld_Cover',
'Inst_flx_Medium_Cld_Cover',
'Inst_flx_Tot_Cld_cover',
'Large_scale_rain',
'Large_scale_snow',
'Mean_sea_level_pressure',
'Relative_humidity',
'Relative_humidity_isobaric',
'SBL_Meridian_gust',
'SBL_Zonal_gust',
'Specific_humidity',
'Surf_Solar_radiation',
'Surf_Thermal_radiation',
'Surface_CAPE',
'Surface_Temperature',
'Surface_orography',
'Temperature',
'Total_precipitation',
'U-velocity',
'V-velocity',
'Vertical_velocity',
'Wet_Bulb_Poten_Temper',
'freezing_level_zeroDegC_isotherm'],
"""
# alaro = {'ALARO': {'10m_u_wind_component': '10_m_u__wind_component',
# '10m_v_wind_component': '10_m_v__wind_component',
# '2m_Max_temp_since_ppp': '2_m_Max_temp_since_ppp',
# '2m_Min_temp_since_ppp': '2_m_Min_temp_since_ppp',
# '2m_dewpoint_temperature': '2_m_dewpoint_temperature',
# '2m_temperature': '2_m_temperature',
# '2m_Relative_humidity': '2m_Relative_humidity',
# 'Convective_rain': 'Convective_rain',
# 'Convective_snow': 'Convective_snow',
# 'Geopotential': 'Geopotential',
# 'Inst_flx_Conv_Cld_Cover': 'Inst_flx_Conv_Cld_Cover',
# 'Inst_flx_High_Cld_Cover': 'Inst_flx_High_Cld_Cover',
# 'Inst_flx_Low_Cld_Cover': 'Inst_flx_Low_Cld_Cover',
# 'Inst_flx_Medium_Cld_Cover': 'Inst_flx_Medium_Cld_Cover',
# 'Inst_flx_Tot_Cld_cover': 'Inst_flx_Tot_Cld_cover',
# 'Large_scale_rain': 'Large_scale_rain',
# 'Large_scale_snow': 'Large_scale_snow',
# 'Mean_sea_level_pressure': 'Mean_sea_level_pressure',
# 'Relative_humidity': 'Relative_humidity',
# 'Relative_humidity_isobaric': 'Relative_humidity_isobaric',
# 'SBL_Meridian_gust': 'SBL_Meridian_gust',
# 'SBL_Zonal_gust': 'SBL_Zonal_gust',
# 'Specific_humidity': 'Specific_humidity',
# 'Surf_Solar_radiation': 'Surf_Solar_radiation',
# 'Surf_Thermal_radiation': 'Surf_Thermal_radiation',
# 'Surface_CAPE': 'Surface_CAPE',
# 'Surface_Temperature': 'Surface_Temperature',
# 'Surface_orography': 'Surface_orography',
# 'Temperature': 'Temperature',
# 'Total_precipitation': 'Total_precipitation',
# 'U-velocity': 'U-velocity',
# 'V-velocity': 'V-velocity',
# 'Vertical_velocity': 'Vertical_velocity',
# 'Wet_Bulb_Poten_Temper': 'Wet_Bulb_Poten_Temper',
# 'freezing_level_zeroDegC_isotherm': 'freezing_level_zeroDegC_isotherm',}}
alaro = {'ALARO': {'2m_temperature': '2_m_temperature',
'Convective_rain': 'Convective_rain',
'Convective_snow': 'Convective_snow',
'Large_scale_rain': 'Large_scale_rain',
'Large_scale_snow': 'Large_scale_snow',
'Surface_Temperature': 'Surface_Temperature',
'Total_precipitation': 'Total_precipitation',}}
prioritary = {'Orthophotos': {'Last': ('orthoimage_coverage', 'IGN')},
'IMAGERIE': {'Last one': ('ORTHO_LAST', 'PPNC')},
'LW_ecotopes_lc_hr_raster': {'2022': ('2022', 'LifeWatch')},
'CartoWeb': {'Overlay': ('overlay', 'IGN_cartoweb')}
}
for idx, (k, item) in enumerate(prioritary.items()):
for kdx, (m, subitem) in enumerate(item.items()):
subitem, service = subitem
if service == 'IGN':
self.add_object(which='wmsback',
newobj=imagetexture('Orthos IGN', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024, IGN_Belgium=True),
ToCheck=False, id='Orthophotos IGN')
elif service == 'PPNC':
self.add_object(which='wmsback',
newobj=imagetexture('PPNC', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024),
ToCheck=False, id='Orthophotos Walonmap')
elif service == 'LifeWatch':
self.add_object(which='wmsback',
newobj=imagetexture('LanCover', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024, LifeWatch=True),
ToCheck=False, id='LifeWatch 2022')
elif service == 'IGN_cartoweb':
self.add_object(which='wmsback',
newobj=imagetexture('Cartoweb IGN', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024,
IGN_Cartoweb=True),
ToCheck=False, id='Overlay Cartoweb')
for idx, (k, item) in enumerate(orthos.items()):
for kdx, (m, subitem) in enumerate(item.items()):
self.add_object(which='wmsback',
newobj=imagetexture('PPNC', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024),
ToCheck=False, id='PPNC ' + m)
for idx, (k, item) in enumerate(data_2021.items()):
for kdx, (m, subitem) in enumerate(item.items()):
self.add_object(which='wmsback',
newobj=imagetexture('PPNC', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024),
ToCheck=False, id='Data 2021 ' + m)
for idx, (k, item) in enumerate(lifewatch.items()):
for kdx, (m, subitem) in enumerate(item.items()):
self.add_object(which='wmsback',
newobj=imagetexture('LanCover', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024, LifeWatch=True),
ToCheck=False, id='LifeWatch LC' + m)
for idx, (k, item) in enumerate(ign_belgique.items()):
for kdx, (m, subitem) in enumerate(item.items()):
self.add_object(which='wmsback',
newobj=imagetexture('Orthos IGN', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024, IGN_Belgium=True),
ToCheck=False, id='IGN ' + m)
for idx, (k, item) in enumerate(ign_cartoweb.items()):
for kdx, (m, subitem) in enumerate(item.items()):
self.add_object(which='wmsback',
newobj=imagetexture('Cartoweb IGN', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024, IGN_Cartoweb=True),
ToCheck=False, id='IGN ' + m)
for idx, (k, item) in enumerate(ign_postflood2021.items()):
for kdx, (m, subitem) in enumerate(item.items()):
self.add_object(which='wmsback',
newobj=imagetexture('IGN 2021', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024, postFlood2021=True),
ToCheck=False, id='orthos post2021')
self.add_object(which='wmsback',
newobj=imagetexture('PPNC', 'Orthos France', 'OI.OrthoimageCoverage.HR', '',
self, xmin, xmax, ymin, ymax, -99999, 1024, France=True, epsg='EPSG:2154'),
ToCheck=False, id='Orthos France')
forelist = {'EAU': {'Aqualim': 'RES_LIMNI_DGARNE', 'Alea': 'ALEA_INOND', 'Lidaxes': 'LIDAXES'},
'LIMITES': {'Secteurs Statistiques': 'LIMITES_QS_STATBEL',
'Limites administratives': 'LIMITES_ADMINISTRATIVES'},
'R3C': {'Limites Communes': 'Municipalities'},
# 'INSPIRE': {'Limites administratives': 'AU_wms'},
'PLAN_REGLEMENT': {'Plan Parcellaire 2021': 'CADMAP_2021_PARCELLES',
'Plan Parcellaire 2022': 'CADMAP_2022_PARCELLES',
'Plan Parcellaire 2023': 'CADMAP_2023_PARCELLES',
'Plan Parcellaire 2024': 'CADMAP_2024_PARCELLES'}}
for idx, (k, item) in enumerate(forelist.items()):
for kdx, (m, subitem) in enumerate(item.items()):
self.add_object(which='wmsfore',
newobj=imagetexture('PPNC', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024),
ToCheck=False, id=m)
for idx, (k, item) in enumerate(alaro.items()):
for kdx, (m, subitem) in enumerate(item.items()):
self.add_object(which='wmsfore',
newobj=imagetexture('ALARO', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024, Alaro=True),
ToCheck=False, id='ALARO ' + m)
for idx, (k, item) in enumerate(ign_cartoweb.items()):
for kdx, (m, subitem) in enumerate(item.items()):
self.add_object(which='wmsfore',
newobj=imagetexture('Cartoweb', m, k, subitem,
self, xmin, xmax, ymin, ymax, -99999, 1024, IGN_Cartoweb=True),
ToCheck=False, id='IGN_f ' + m)
# self.add_object(which='wmsfore',
# newobj=imagetexture('Cadastre Flandres', 'Plan Parcellaire 2024 (Flandres)', 'Adpf', '',
# self, xmin, xmax, ymin, ymax, -99999, 1024, Vlaanderen=True),
# ToCheck=False, id='Plan Parcellaire 2024 (Flandres)')
[docs]
def set_compare(self, ListArrays:list[WolfArray]=None, share_colormap:bool=True):
"""
Comparison of 2 arrays
:param ListArrays: List of 2 arrays to compare
:param share_colormap: Share the colormap between the 2 arrays
"""
# assert len(ListArrays) == 2, _('List of arrays must contain 2 and only 2 arrays - Here, you have provided {} arrays'.format(len(ListArrays)))
# Création de 3 fenêtres de visualisation basées sur la classe "WolfMapViewer"
first = self
second = WolfMapViewer(None, 'Comparison', w=600, h=600, wxlogging=self.wxlogging, wolfparent = self.wolfparent)
third = WolfMapViewer(None, 'Difference', w=600, h=600, wxlogging=self.wxlogging, wolfparent = self.wolfparent)
second.add_grid()
third.add_grid()
second.add_WMS()
third.add_WMS()
# Création d'une liste contenant les 3 instances d'objet "WolfMapViewer"
mylist:list[WolfMapViewer] = []
mylist.append(first)
mylist.append(second)
mylist.append(third)
# On indique que les objets sont liés en activant le Booléen et en pointant la liste précédente
for curlist in mylist:
curlist.linked = True
curlist.linkedList = mylist
if ListArrays is not None:
if len(ListArrays) == 2:
mnt = ListArrays[0]
mns = ListArrays[1]
if not mnt.is_like(mns):
logging.warning(_('The 2 arrays must have the same shape - Here, the 2 arrays have different shapes'))
return
else:
logging.warning(_('List of arrays must contain 2 and only 2 arrays - Here, you have provided {} arrays'.format(len(ListArrays))))
return
else:
logging.warning(_('You must fill the List of arrays with 2 and only 2 arrays - Here, the list is void'))
return
mns: WolfArray
mnt: WolfArray
diff: WolfArray
# Recherche d'un masque union des masques partiels
mns.mask_union(mnt)
# Création du différentiel -- Les opérateurs mathématiques sont surchargés
diff = mns - mnt
# on attribue une matrice par interface graphique
mnt.change_gui(first)
mns.change_gui(second)
diff.change_gui(third)
path = os.path.dirname(__file__)
fn = join(path, 'models\\diff16.pal')
# on partage la palette de couleurs
if share_colormap:
mns.add_crosslinked_array(mnt)
mns.share_palette()
# on dissocie la palette de la différence
diff.mypal = wolfpalette()
if isinstance(diff, WolfArrayMB):
diff.link_palette()
diff.mypal.readfile(fn)
diff.mypal.automatic = False
diff.myops.palauto.SetValue(0)
mnt.mypal.automatic = False
mnt.myops.palauto.SetValue(0)
if not share_colormap:
mns.mypal.automatic = False
mns.myops.palauto.SetValue(0)
mns.mypal.updatefrompalette(mnt.mypal)
# Ajout des matrices dans les fenêtres de visualisation
first.add_object('array', newobj=mnt, ToCheck=True, id='source')
second.add_object('array', newobj=mns, ToCheck=True, id='comp')
third.add_object('array', newobj=diff, ToCheck=True, id='diff=comp-source')
# Partage des vecteurs de la classe d'opérations
mnt.myops.myzones = mns.myops.myzones
diff.myops.myzones = mns.myops.myzones
first.active_array = mnt
second.active_array = mns
third.active_array = diff
mnt.reset_plot()
mns.reset_plot()
diff.reset_plot()
[docs]
def set_compare_all(self, ListArrays=None, names:list[str] = None):
""" Comparison of 2 or 3 arrays """
assert len(ListArrays) == 2 or len(ListArrays) == 3, _('List of arrays must contain 2 or 3 arrays - Here, you have provided {} arrays'.format(len(ListArrays)))
if names is not None:
assert len(names) == len(ListArrays)-1, _('List of names must contain the number of names as arrays minus one - Here, you have provided {} names for {} arrays'.format(len(names), len(ListArrays)))
else:
names = ['comp1', 'comp2']
# Création de 3 fenêtres de visualisation basées sur la classe "WolfMapViewer"
first = self
second = WolfMapViewer(None, 'Comparison {}'.format(names[0]), w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
third = WolfMapViewer(None, 'Difference {}'.format(names[0]), w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
if len(ListArrays) == 3:
fourth = WolfMapViewer(None, 'Comparison {}'.format(names[1]), w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
fifth = WolfMapViewer(None, 'Difference {}'.format(names[1]), w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
# Création d'une liste contenant les multiples instances d'objet "WolfMapViewer"
list = []
list.append(first)
list.append(second)
list.append(third)
if len(ListArrays) == 3:
list.append(fourth)
list.append(fifth)
for curview in list:
if curview is not self:
curview.add_grid()
curview.add_grid()
# On indique que les objets sont liés en actiavt le Booléen et en pointant la liste précédente
for curview in list:
curview.linked = True
curview.linkedList = list
comp2 = None
if ListArrays is not None:
if len(ListArrays) == 2:
src = ListArrays[0]
comp1 = ListArrays[1]
elif len(ListArrays) == 3:
src = ListArrays[0]
comp1 = ListArrays[1]
comp2 = ListArrays[2]
else:
return
else:
return
src: WolfArray
comp1: WolfArray
diff1: WolfArray
comp2: WolfArray
diff2: WolfArray
# Création du différentiel -- Les opérateurs mathématiques sont surchargés
diff1 = comp1 - src
comp1.copy_mask(src, True)
diff1.copy_mask(src, True)
src.change_gui(first)
comp1.change_gui(second)
diff1.change_gui(third)
src.mypal.automatic = False
comp1.mypal.automatic = False
src.myops.palauto.SetValue(0)
comp1.myops.palauto.SetValue(0)
src.mypal.isopop(src.array, src.nbnotnull)
comp1.mypal.updatefrompalette(src.mypal)
# Ajout des matrices dans les fenêtres de visualisation
first.add_object('array', newobj=src, ToCheck=True, id='source')
second.add_object('array', newobj=comp1, ToCheck=True, id='comp')
third.add_object('array', newobj=diff1, ToCheck=True, id='diff=comp-source')
comp1.myops.myzones = src.myops.myzones
diff1.myops.myzones = src.myops.myzones
first.active_array = src
second.active_array = comp1
third.active_array = diff1
if comp2 is not None:
diff2 = comp2 - src
comp2.copy_mask(src, True)
diff2.copy_mask(src, True)
comp2.change_gui(fourth)
diff2.change_gui(fifth)
comp2.mypal.automatic = False
comp2.myops.palauto.SetValue(0)
comp2.mypal.updatefrompalette(src.mypal)
# Ajout des matrices dans les fenêtres de visualisation
fourth.add_object('array', newobj=comp2, ToCheck=True, id='comp2')
fifth.add_object('array', newobj=diff2, ToCheck=True, id='diff2=comp2-source')
comp2.myops.myzones = src.myops.myzones
diff2.myops.myzones = src.myops.myzones
fourth.active_array = comp2
fifth.active_array = diff2
[docs]
def set_blender_sculpting(self):
"""
Mise en place de la structure nécessaire pour comparer la donnée de base avec la donnée sculptée sous Blender
La donnée de base est la matrice contenue dans la fenêtre actuelle
Fenêtres additionnelles :
- information sur les volumes de déblai/remblai et bilan
- matrice sculptée
- différentiel entre scultage - source
- gradient
- laplacien
- masque de modification
"""
myframe = wx.Frame(None, title=_('Excavation and backfill'))
sizergen = wx.BoxSizer(wx.VERTICAL)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer2 = wx.BoxSizer(wx.HORIZONTAL)
sizer3 = wx.BoxSizer(wx.HORIZONTAL)
sizergen.Add(sizer1)
sizergen.Add(sizer2)
sizergen.Add(sizer3)
labexc = wx.StaticText(myframe, label=_('Excavation : '))
labback = wx.StaticText(myframe, label=_('Backfill : '))
labbal = wx.StaticText(myframe, label=_('Balance : '))
sizer1.Add(labexc)
sizer2.Add(labback)
sizer3.Add(labbal)
font = wx.Font(18, wx.DECORATIVE, wx.NORMAL, wx.NORMAL)
Exc = wx.StaticText(myframe, label=' [m³]')
Back = wx.StaticText(myframe, label=' [m³]')
Bal = wx.StaticText(myframe, label=' [m³]')
labexc.SetFont(font)
labback.SetFont(font)
labbal.SetFont(font)
Exc.SetFont(font)
Back.SetFont(font)
Bal.SetFont(font)
sizer1.Add(Exc)
sizer2.Add(Back)
sizer3.Add(Bal)
myframe.SetSizer(sizergen)
myframe.Layout()
myframe.Centre(wx.BOTH)
myframe.Show()
if self.link_params is None:
self.link_params = {}
self.link_params['ExcavationBackfill'] = myframe
self.link_params['Excavation'] = Exc
self.link_params['Backfill'] = Back
self.link_params['Balance'] = Bal
# Création de fenêtres de visualisation basées sur la classe "WolfMapViewer"
first = self
second = WolfMapViewer(None, 'Sculpting', w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
third = WolfMapViewer(None, 'Difference', w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
fourth = WolfMapViewer(None, 'Gradient', w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
fifth = WolfMapViewer(None, 'Laplace', w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
sixth = WolfMapViewer(None, 'Unitary Mask', w=600, h=600, wxlogging=self.wxlogging, wolfparent=self.wolfparent)
# Création d'une liste contenant les 3 instances d'objet "WolfMapViewer"
list = []
list.append(first)
list.append(second)
list.append(third)
list.append(fourth)
list.append(fifth)
list.append(sixth)
for curlist in list:
curlist.add_grid()
curlist.add_WMS()
# On indique que les objets sont liés en actiavt le Booléen et en pointant la liste précédente
for curlist in list:
curlist.linked = True
curlist.linkedList = list
source: WolfArray
sourcenew: WolfArray
diff: WolfArray
grad: WolfArray
lap: WolfArray
unimask: WolfArray
source = self.active_array
sourcenew = WolfArray(mold=source)
# Création du différentiel -- Les opérateurs mathématiques sont surchargés
diff = source - source
grad = source.get_gradient_norm()
lap = source.get_laplace()
unimask = WolfArray(mold=diff)
np.divide(diff.array.data, abs(diff.array.data), out=unimask.array.data, where=diff.array.data != 0.)
grad.copy_mask(source, True)
lap.copy_mask(source, True)
diff.copy_mask(source, True)
unimask.copy_mask(source, True)
sourcenew.change_gui(second)
diff.change_gui(third)
grad.change_gui(fourth)
lap.change_gui(fifth)
unimask.change_gui(sixth)
path = os.path.dirname(__file__)
fn=join(path,'models\\diff16.pal')
if exists(fn):
diff.mypal.readfile(fn)
diff.mypal.automatic=False
diff.myops.palauto.SetValue(0)
fn=join(path,'models\\diff3.pal')
if exists(fn):
unimask.mypal.readfile(fn)
unimask.mypal.automatic=False
unimask.myops.palauto.SetValue(0)
# Ajout des matrices dans les fenêtres de visualisation
second.add_object('array', newobj=sourcenew, ToCheck=True, id='source_new')
third.add_object('array', newobj=diff, ToCheck=True, id='diff=comp-source')
fourth.add_object('array', newobj=grad, ToCheck=True, id='gradient')
fifth.add_object('array', newobj=lap, ToCheck=True, id='laplace')
sixth.add_object('array', newobj=unimask, ToCheck=True, id='unimask')
#pointage des vecteurs attachés à chaque matrice dans chaque GUI de façon à c que les modifications se répercutent partout
sourcenew.myops.myzones = source.myops.myzones
diff.myops.myzones = source.myops.myzones
grad.myops.myzones = source.myops.myzones
lap.myops.myzones = source.myops.myzones
unimask.myops.myzones = source.myops.myzones
second.active_array = sourcenew
third.active_array = diff
fourth.active_array = grad
fifth.active_array = lap
sixth.active_array = unimask
self.mimicme()
[docs]
def update_blender_sculpting(self):
""" Mise à jour des fenêtres de visualisation pour la comparaison avec Blender """
if not self.linked:
return
if len(self.linkedList) != 6:
return
# Création de fenêtres de visualisation basées sur la classe "WolfMapViewer"
first = self.linkedList[0]
second = self.linkedList[1]
third = self.linkedList[2]
fourth = self.linkedList[3]
fifth = self.linkedList[4]
sixth = self.linkedList[5]
source = first.active_array
sourcenew = second.active_array
diff = third.active_array
grad = fourth.active_array
lap = fifth.active_array
unimask = sixth.active_array
fn = ''
if self.link_params is not None:
if 'gltf file' in self.link_params.keys():
fn = self.link_params['gltf file']
fnpos = self.link_params['gltf pos']
if fn == '':
for curgui in self.linkedList:
if curgui.link_params is not None:
if 'gltf file' in curgui.link_params.keys():
fn = self.link_params['gltf file']
fnpos = self.link_params['gltf pos']
break
with self._dialogs.show_busy(_('Importing gltf/glb')):
sourcenew.import_from_gltf(fn, fnpos, 'scipy')
with self._dialogs.show_busy(_('Update plots')):
# Création du différentiel -- Les opérateurs mathématiques sont surchargés
diff.array = (sourcenew - source).array
grad.array = sourcenew.get_gradient_norm().array
lap.array = sourcenew.get_laplace().array
np.divide(diff.array.data, abs(diff.array.data), out=unimask.array.data, where=diff.array.data != 0.)
diff.copy_mask(sourcenew, True)
lap.copy_mask(sourcenew, True)
grad.copy_mask(sourcenew, True)
unimask.copy_mask(sourcenew, True)
first.Paint()
second.Paint()
third.Paint()
fourth.Paint()
fifth.Paint()
sixth.Paint()
Exc: wx.StaticText
Back: wx.StaticText
Bal: wx.StaticText
if not 'ExcavationBackfill' in self.link_params.keys():
for curgui in self.linkedList:
if curgui.link_params is not None:
if 'ExcavationBackfill' in curgui.link_params.keys():
myframe = curgui.link_params['ExcavationBackfill']
Exc = curgui.link_params['Excavation']
Back = curgui.link_params['Backfill']
Bal = curgui.link_params['Balance']
else:
myframe = self.link_params['ExcavationBackfill']
Exc = self.link_params['Excavation']
Back = self.link_params['Backfill']
Bal = self.link_params['Balance']
Exc.SetLabel("{:.2f}".format(np.sum(diff.array[diff.array < 0.])) + ' [m³]')
Back.SetLabel("{:.2f}".format(np.sum(diff.array[diff.array > 0.])) + ' [m³]')
Bal.SetLabel("{:.2f}".format(np.sum(diff.array)) + ' [m³]')
[docs]
def zoomon_activevector(self, size:float=500., forceupdate:bool=True):
"""
Zoom on active vector
:param size: size of the zoomed window
:param forceupdate: force the update of the window
"""
if self.active_vector is None:
logging.warning(_('No active vector'))
return
curvec = self.active_vector
if curvec.xmin == -99999:
curvec.find_minmax()
bounds = [curvec.xmin, curvec.xmax, curvec.ymin, curvec.ymax]
dx = bounds[1] - bounds[0]
dy = bounds[3] - bounds[2]
self._center_x = bounds[0] + dx / 2.
self._center_y = bounds[2] + dy / 2.
self.width = max(size, dx)
self.height = max(size, dy)
self.updatescalefactors()
self.setbounds()
self.mimicme()
if forceupdate:
self.update()
if self.linked:
for cur in self.linkedList:
if cur is not self:
cur.update()
[docs]
def zoomon_active_vertex(self, size:float = 20, forceupdate:bool = True):
"""
Zoom on active vertex.
:param size: size of the zoomed window
:param forceupdate: force the update of the window
"""
if self.active_vector is None:
logging.warning(_('No active vector'))
return
curvec = self.active_vector
if curvec.xmin == -99999:
curvec.find_minmax()
if self.active_vector is None:
logging.warning(_('No active vector'))
return
grid = self.active_zones.xls
row = grid.GetGridCursorRow()
x = float(grid.GetCellValue(row, 0))
y = float(grid.GetCellValue(row, 1))
z = float(grid.GetCellValue(row, 2))
curvert = wolfvertex(x, y, z)
self._center_x = curvert.x
self._center_y = curvert.y
self.width = size
self.height = size
self.updatescalefactors()
self.setbounds()
self.mimicme()
if forceupdate:
self.update()
if self.linked:
for cur in self.linkedList:
if cur is not self:
cur.update()
[docs]
def zoom_on_id(self, id:str, drawtype:draw_type = draw_type.ARRAYS, forceupdate=True, canvas_height=1024):
"""
Zoom on id
:param id: id of the object to zoom on
:param drawtype: type of object to zoom on - Different types elements can have the same id
"""
if drawtype not in [draw_type.ARRAYS, draw_type.VECTORS]:
logging.warning(_('Draw type must be either ARRAYS or VECTORS'))
return
obj = self.get_obj_from_id(id, drawtype)
if obj is None:
logging.warning(_('No object found with id {} and drawtype {}'.format(id, drawtype)))
return
if drawtype == draw_type.ARRAYS:
self.zoom_on_array(obj, forceupdate=forceupdate, canvas_height=canvas_height)
elif drawtype == draw_type.VECTORS:
self.zoom_on_vector(obj, forceupdate=forceupdate, canvas_height=canvas_height)
[docs]
def zoom_on_array(self, array:WolfArray, forceupdate=True, canvas_height=1024):
""" Zoom on array """
bounds = array.get_bounds()
center = [(bounds[0][1] + bounds[0][0]) / 2., (bounds[1][1] + bounds[1][0]) / 2.]
width = bounds[0][1] - bounds[0][0]
height = bounds[1][1] - bounds[1][0]
self.zoom_on({'center':center, 'width':width, 'height':height}, forceupdate=forceupdate, canvas_height=canvas_height)
[docs]
def zoom_on_vector(self, vector:vector, forceupdate=True, canvas_height=1024):
""" Zoom on vector """
if vector.xmin == -99999:
vector.find_minmax()
bounds = vector.get_bounds_xx_yy()
center = [(bounds[0][1] + bounds[0][0]) / 2., (bounds[1][1] + bounds[1][0]) / 2.]
width = bounds[0][1] - bounds[0][0]
height = bounds[1][1] - bounds[1][0]
self.zoom_on({'center':center, 'width':width, 'height':height}, forceupdate=forceupdate, canvas_height=canvas_height)
[docs]
def create_Zones_from_arrays(self, arrays:list[WolfArray], id:str = None, add_legend:bool=True) -> Zones:
"""
Create a Zones instance from list of WolfArrays
One zone per array.
One vector per zone with the masked contour.
:param arrays: list of WolfArrays
:param id: id of the Zones instance
:param add_legend: add legend to the vector -- centroid of the contour
"""
msg = _('This function will force a null border of 1 cell on each array to avoid issues with the contouring algorithm')
if not self._dialogs.ask_ok_cancel(msg, _('Warning'), DialogStyles.OK_CANCEL_WARNING, parent=self):
logging.info(_('Operation cancelled by user'))
return None
# création de l'instance de Zones
new_zones = Zones(idx = 'contour' if id is None else id.lower(), mapviewer=self)
for curarray in arrays:
if isinstance(curarray, WolfArray):
curarray.nullify_border(1)
curarray.reset_plot()
new_zone = zone(name = curarray.idx)
new_zones.add_zone(new_zone, forceparent=True)
sux, sux, curvect, interior = curarray.suxsuy_contour()
new_zone.add_vector(curvect, forceparent=True)
curvect.set_legend_to_centroid(curarray.idx)
curvect.myprop.width = 2
rectvect = vector(name = 'rect_boundary')
new_zone.add_vector(rectvect, forceparent=True)
bounds = curarray.get_bounds()
rectvect.add_vertex(wolfvertex(bounds[0][0], bounds[1][0]))
rectvect.add_vertex(wolfvertex(bounds[0][1], bounds[1][0]))
rectvect.add_vertex(wolfvertex(bounds[0][1], bounds[1][1]))
rectvect.add_vertex(wolfvertex(bounds[0][0], bounds[1][1]))
rectvect.close_force()
rectvect.myprop.color = getIfromRGB([255,0,0])
rectvect.myprop.width = 2
logging.info(_('{} treated'.format(curarray.idx)))
else:
logging.warning(_('All elements in the list must be of type WolfArray'))
new_zones.find_minmax(update=True)
return new_zones
[docs]
def zoom_on(self, zoom_dict = None, width = 500, height = 500, center = None, xll = None, yll = None, forceupdate=True, canvas_height=1024):
"""
Zoom on a specific area
It is possible to zoom on a specific area by providing the zoom parameters in :
- a dictionnary
- width and height of the zoomed window and the lower left corner coordinates
- width and height of the zoomed window and the center coordinates
:param zoom_dict: dictionnary containing the zoom parameters - possible keys : 'width', 'height', 'center', 'xmin', 'ymin', 'xmax', 'ymax'
:param width: width of the zoomed window [m]
:param height: height of the zoomed window [m]
:param center: center of the zoomed window [m] - tuple (x,y)
:param xll: lower left X coordinate of the zoomed window [m]
:param yll: lower left Y coordinate of the zoomed window [m]
:param forceupdate: force the update of the window
:param canvas_height: height of the canvas [pixels]
Examples :
- zoom_on(zoom_dict = {'center':(500,500), 'width':1000, 'height':1000})
- zoom_on(width = 1000, height = 1000, xll = 500, yll = 500)
- zoom_on(width = 1000, height = 1000, center = (500,500))
"""
if zoom_dict is not None:
width = 99999
height = 99999
xll = 99999
yll = 99999
xmax = 99999
ymax = 99999
if 'center' in zoom_dict.keys():
center = zoom_dict['center']
if 'width' in zoom_dict.keys():
width = zoom_dict['width']
if 'height' in zoom_dict.keys():
height = zoom_dict['height']
if 'xmin' in zoom_dict.keys():
xll = zoom_dict['xmin']
if 'ymin' in zoom_dict.keys():
yll = zoom_dict['ymin']
if 'xmax' in zoom_dict.keys():
xmax = zoom_dict['xmax']
if 'ymax' in zoom_dict.keys():
ymax = zoom_dict['ymax']
if width == 99999:
width = xmax-xll
if height == 99999:
height = ymax-yll
if center is not None and len(center)==2:
self._center_x = center[0]
self._center_y = center[1]
self.width = width
self.height = height
elif (xll is not None) and (yll is not None):
self._center_x = xll + width/2
self._center_y = yll + height/2
self.width = width
self.height = height
# fixe la taille de la fenêtre
v_height = canvas_height
v_width = int(v_height*(float(width)/float(height)))
self.SetClientSize(v_width + self.treewidth, v_height)
self.updatescalefactors()
self.mimicme()
if forceupdate:
self.update()
if self.linked:
for cur in self.linkedList:
if cur is not self:
cur.update()
[docs]
def zoom_on_active_profile(self, size:float=500., forceupdate:bool=True):
""" Zoom on active profile """
curvec = self.active_profile
if curvec.xmin == -99999:
curvec.find_minmax()
bounds = [curvec.xmin, curvec.xmax, curvec.ymin, curvec.ymax]
dx = bounds[1] - bounds[0]
dy = bounds[3] - bounds[2]
self._center_x = bounds[0] + dx / 2.
self._center_y = bounds[2] + dy / 2.
self.width = max(size, dx)
self.height = max(size, dy)
self.updatescalefactors()
self.setbounds()
self.mimicme()
if forceupdate:
self.update()
if self.linked:
for cur in self.linkedList:
if cur is not self:
cur.update()
[docs]
def read_project(self, fn):
"""
Projet WOLF GUI
Fichier de paramètres contenant les types et chemins d'accès aux données à ajouter
A compléter...
"""
curdir = Path(os.getcwd())
real_ids = {}
myproject = Wolf_Param(None, filename=fn, toShow=False)
def check_params(myproject, curgroup) -> bool:
check = True
pot_keys = list(PROJECT_GROUP_KEYS[curgroup].keys())
for curkey in pot_keys:
if 'mandatory' in PROJECT_GROUP_KEYS[curgroup][curkey]:
if not myproject.is_in(curgroup, curkey):
logging.warning(_('Missing key : ')+ curkey)
check = False
return check
def sanit_id(id:str, drawtype:draw_type) -> str:
existing_id = self.get_list_keys(drawtype, None)
while id in existing_id:
logging.warning(_('ID already exists - Changing it...'))
id = id + '_'
return id
# COMPLEX ACTIONS
curgroup = PROJECT_ACTION
if myproject.is_in(curgroup):
pot_keys = list(PROJECT_GROUP_KEYS[curgroup].keys())
for curkey in pot_keys:
which = myproject[(curgroup, curkey)]
pot_val = list(PROJECT_GROUP_KEYS[curgroup][curkey].keys())
if which in pot_val:
if which == 'compare_arrays':
# Comparaison de plusieurs matrices
logging.info(_('Compare action - Searching for arrays to compare...'))
ListCompare = []
if myproject.is_in('array'):
for curval in myproject.get_group('array').values():
curid = curval[key_Param.NAME]
logging.info(_('Array to compare : ')+ curid)
ListCompare.append(WolfArray(Path(myproject[('array', curid)])))
else:
logging.warning(_('No array to compare - Aborting !'))
return
logging.info(_('Setting compare...'))
self.set_compare(ListCompare)
logging.info(_('Compare set !'))
return
else:
logging.error(_('Bad parameter in project file - action : ')+ which)
# CROSS SECTIONS
curgroup = PROJECT_CS
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
for curval in myproject.get_group(curgroup).values():
curid = curval[key_Param.NAME]
if curid != 'format' and curid != 'dirlaz':
mycs = crosssections(myproject[(curgroup, curid)],
format = myproject[(curgroup, 'format')],
dirlaz = myproject[(curgroup, 'dirlaz')],
mapviewer = self)
locid = real_ids[(draw_type.VECTORS, curid)] = sanit_id(curid, draw_type.VECTORS)
self.add_object(curgroup, newobj=mycs, id=locid)
else:
logging.warning(_('Bad parameter in project file - cross_sections'))
# TILES
curgroup = PROJECT_TILES
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
curid = myproject.get_param(curgroup, 'id')
curfile = myproject.get_param(curgroup, 'tiles_file')
curdatadir = myproject.get_param(curgroup, 'data_dir')
curcompdir = myproject.get_param(curgroup, 'comp_dir')
if exists(curfile):
try:
mytiles = Tiles(filename= curfile, parent=self, linked_data_dir=curdatadir)
mytiles.set_comp_dir(curcompdir)
locid = real_ids[(draw_type.TILES, curid)] = sanit_id(curid, draw_type.TILES)
self.add_object(curgroup, newobj=mytiles, id=locid)
except Exception as e:
logging.error(_('Error in tiles import : ')+ str(e))
else:
logging.warning(_('File does not exist : ')+ str(curfile))
else:
logging.warning(_('Bad parameter in project file - tiles'))
# LAZ GRID
curgroup = PROJECT_LAZ
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
try:
self.init_laz_from_gridinfos(curdir / myproject[curgroup, 'data_dir'], myproject[(curgroup, 'classification')])
except Exception as e:
logging.error(_('Error in laz_grid import : ')+ str(e))
else:
logging.warning(_('Bad parameter in project file - laz_grid'))
# VECTOR DATA
curgroup = PROJECT_VECTOR
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
for curval in myproject.get_group(curgroup).values():
curid = curval[key_Param.NAME]
name = curval[key_Param.VALUE]
if exists(name):
try:
myvec = Zones(name, parent = self, mapviewer = self)
locid = real_ids[(draw_type.VECTORS, curid)] = sanit_id(curid, draw_type.VECTORS)
self.add_object(curgroup, newobj = myvec, id = locid)
except Exception as e:
logging.error(_('Error in vector import : ')+ str(e))
else:
logging.info(_('File does not exist : ') + str(name))
else:
logging.warning(_('Bad parameter in project file - vector'))
# ARRAY DATA
curgroup = PROJECT_ARRAY
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
for curval in myproject.get_group(curgroup).values():
curid = curval[key_Param.NAME]
name = curdir / Path(curval[key_Param.VALUE])
if exists(name):
try:
curarray = WolfArray(name, mapviewer = self)
locid = real_ids[(draw_type.ARRAYS, curid)] = sanit_id(curid, draw_type.ARRAYS)
self.add_object('array', newobj=curarray, id = locid)
except Exception as e:
logging.error(_('Error in array import : ')+ str(e))
else:
logging.info(_('File does not exist : ') + str(name))
else:
logging.warning(_('Bad parameter in project file - array'))
# CLOUD DATA
curgroup = PROJECT_CLOUD
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
for curval in myproject.get_group(curgroup).values():
curid = curval[key_Param.NAME]
name = curval[key_Param.VALUE]
if exists(name):
try:
mycloud = cloud_vertices(name, mapviewer = self)
locid = real_ids[(draw_type.CLOUD, curid)] = sanit_id(curid, draw_type.CLOUD)
self.add_object('cloud', newobj = mycloud, id = locid)
except Exception as e:
logging.error(_('Error in cloud import : ') + str(e))
else:
logging.info(_('File does not exist : ') + str(name))
else:
logging.warning(_('Bad parameter in project file - cloud'))
# 2D RESULTS
# CPU code
curgroup = PROJECT_WOLF2D
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
for curval in myproject.get_group(curgroup).values():
curid = curval[key_Param.NAME]
simdir = Path(curval[key_Param.VALUE])
if simdir.exists():
try:
curwolf = Wolfresults_2D(simdir, mapviewer = self)
locid = real_ids[(draw_type.RES2D, curid)] = sanit_id(curid, draw_type.RES2D)
self.add_object('res2d', newobj = curwolf, id = locid)
except Exception as e:
logging.error(_('Error in wolf2d import : ')+ str(e))
else:
logging.info(_('Directory does not exist ')) + str(simdir)
self.menu_wolf2d()
else:
logging.warning(_('Bad parameter in project file - wolf2d'))
# GPU code
curgroup = PROJECT_GPU2D
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
pgbar = self._dialogs.create_progress(
_('Loading GPU results'),
_('Loading GPU results'),
len(myproject.myparams[curgroup].keys()),
self,
style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
)
for idx, curval in enumerate(myproject.get_group(curgroup).values()):
curid = curval[key_Param.NAME]
simdir = Path(curval[key_Param.VALUE])
if simdir.exists():
try:
curwolf = wolfres2DGPU(curdir / simdir, mapviewer = self)
locid = real_ids[(draw_type.RES2D, curid)] = sanit_id(curid, draw_type.RES2D)
self.add_object('res2d', newobj = curwolf, id = locid)
except Exception as e:
logging.error(_('Error in gpu2d import : ')+ str(e))
else:
logging.info(_('Bad directory : ') + str(simdir))
pgbar.update(idx + 1)
pgbar.close()
self.menu_wolf2d()
self.menu_2dgpu()
else:
logging.warning(_('Bad parameter in project file - gpu2d'))
# PALETTE/COLORMAP
curgroup = PROJECT_PALETTE
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
self.project_pal = {}
for curval in myproject.get_group(curgroup).values():
curid = curval[key_Param.NAME]
name = Path(curval[key_Param.VALUE])
if name.exists():
if name.suffix == '.pal':
mypal = wolfpalette(None, '')
mypal.readfile(name)
mypal.automatic = False
self.project_pal[curid] = mypal
else:
logging.warning(_('Bad palette file : ')+ str(name))
else:
logging.info(_('Bad parameter in project file - palette : ')+ str(name))
else:
logging.warning(_('Bad parameter in project file - palette'))
# LINKS
curgroup = PROJECT_PALETTE_ARRAY
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
curarray: WolfArray
if self.project_pal is not None:
for curval in myproject.get_group(curgroup).keys():
id_array = curval[key_Param.NAME]
id_pal = curval[key_Param.VALUE]
if id_pal in self.project_pal.keys():
try:
curarray = self.getobj_from_id(real_ids[(draw_type.ARRAYS, id_array)])
if curarray is not None:
mypal:wolfpalette
mypal = self.project_pal[id_pal]
curarray.mypal = mypal
if mypal.automatic:
curarray.myops.palauto.SetValue(1)
else:
curarray.myops.palauto.SetValue(0)
curarray.updatepalette(0)
curarray.reset_plot()
else:
logging.warning(_('Bad parameter in project file - palette-array : ')+ str(id_array))
except Exception as e:
logging.error(_('Error in palette-array link : ')+ str(e))
else:
logging.warning(_('Bad parameter in project file - palette-array : ')+ str(id_pal))
else:
logging.warning(_('No palettes found in project file ! -- Add palette group in the .proj'))
else:
logging.warning(_('Bad parameter in project file - palette-array'))
curgroup = PROJECT_LINK_CS
if myproject.is_in(curgroup):
if self.active_cs is not None:
if check_params(myproject, curgroup):
idx = real_ids[(draw_type.VECTORS, myproject[(curgroup, 'linkzones')])]
curzones = self.get_obj_from_id(idx, draw_type.VECTORS)
if curzones is not None:
self.active_cs.link_external_zones(curzones)
zonename = myproject[(curgroup, 'sortzone')]
vecname = myproject[(curgroup, 'sortname')]
downfirst = myproject[(curgroup, 'downfirst')]
downfirst = False
if downfirst == 1 or str(downfirst).lower() == 'true':
downfirst = True
if zonename != '' and vecname != '':
curvec = curzones[(zonename, vecname)]
if curvec is not None:
try:
self.active_cs.sort_along(curvec.asshapely_ls(), curvec.myname, downfirst)
except Exception as e:
logging.error(_('Error in cross_sections_link sorting : ')+ str(e))
else:
logging.warning(_('Bad id for sorting vector in project file - cross_sections_link'))
else:
logging.warning(_('Bad parameter in project file - cross_sections_link'))
else:
logging.warning(_('No active cross section to link !'))
curgroup = PROJECT_LINK_VEC_ARRAY
# Useful to mask data outside of the linked contour
if myproject.is_in(curgroup):
if check_params(myproject, curgroup):
for curval in myproject.get_group(curgroup).keys():
id_array = real_ids[(draw_type.ARRAYS, curval[key_Param.NAME])]
id_zones = real_ids[(draw_type.VECTORS, curval[key_Param.VALUE])]
locarray:WolfArray
locvec:Zones
locarray = self.get_obj_from_id(id_array, draw_type.ARRAYS)
if locarray is None:
locarray = self.get_obj_from_id(id_array, draw_type.RES2D)
locvec = self.get_obj_from_id(id_zones, draw_type.VECTORS)
if locvec is not None and locarray is not None:
try:
if locvec.nbzones == 1:
if locvec.myzones[0].nbvectors == 1:
locarray.linkedvec = locvec.myzones[0].myvectors[0]
else:
logging.warning(_('In vec-array association, You must have only 1 zone and 1 polyline !'))
else:
logging.warning(_('In vec-array association, You must have only 1 zone and 1 polyline !'))
except Exception as e:
logging.error(_('Error in vector_array_link : ')+ str(e))
else:
logging.warning(_('Bad vec-array association in project file !'))
else:
logging.warning(_('Bad parameter in project file - vector_array_link'))
[docs]
def save_project(self, fn, absolute:bool = True):
""" Save project file """
dirproj = Path(fn).parent
def new_path(drawtype:draw_type, id:str) -> str:
logging.info(_('Empty path but I need a path !'))
path = ''
ext = 'All files (*.*)|*.*'
if drawtype == draw_type.ARRAYS:
ext += '|Binary files (*.bin)|*.bin|Tiff files (*.tif)|*.tif|Numpy files (*.npy)|*.npy'
elif drawtype == draw_type.VECTORS:
ext += '|VecZ files (*.vecz)|*.vecz|Vec files (*.vec)|*.vec'
elif drawtype == draw_type.CLOUD:
ext += '|Cloud files (*.xyz)|*.xyz'
chosen = self._dialogs.ask_file_save(
_('Choose a filename for ') + id,
wildcard=ext,
default_path=str(dirproj),
parent=self,
)
if chosen is not None:
path = Path(chosen)
return path
def sanit_path(path:Path, absolute:bool, drawtype:draw_type) -> str:
path = Path(path)
if not path.exists():
logging.info(_('Path does not exist : ')+ str(path))
if absolute:
return str(path)
else:
try:
return os.path.relpath(path, dirproj)
except:
logging.error(_('Error in relative path : ')+ str(path) + " - " + str(dirproj))
logging.info(_('Returning absolute path instead !'))
return str(path.absolute())
myproject = Wolf_Param(None, toShow=False, to_read=False, filename=fn, init_GUI=False)
# matrices
try:
curgroup = PROJECT_ARRAY
for curel in self.iterator_over_objects(draw_type.ARRAYS):
curel:WolfArray
if curel.filename == '':
newpath = new_path(draw_type.ARRAYS, curel.idx)
if newpath == '':
logging.warning(_('No path for array : ')+ curel.idx + _(' - Ignoring it !'))
continue
curel.write_all(newpath)
curpath = sanit_path(curel.filename, absolute, draw_type.ARRAYS)
myproject.add_param(curgroup, curel.idx, curpath)
except:
logging.error(_('Error in saving arrays'))
# résultats 2D
try:
curgroup = PROJECT_WOLF2D
for curel in self.iterator_over_objects(draw_type.RES2D):
if type(curel) == Wolfresults_2D:
myproject.add_param(curgroup, curel.idx, sanit_path(curel.filename, absolute, draw_type.RES2D))
curgroup = PROJECT_GPU2D
for curel in self.iterator_over_objects(draw_type.RES2D):
if type(curel) == wolfres2DGPU:
myproject.add_param(curgroup, curel.idx, sanit_path(curel.filename, absolute, draw_type.RES2D))
except:
logging.error(_('Error in saving 2D results'))
# vecteurs
try:
curgroup = PROJECT_VECTOR
for curel in self.iterator_over_objects(draw_type.VECTORS):
if isinstance(curel, crosssections):
continue
curel:Zones
if curel.filename == '':
newpath = new_path(draw_type.VECTORS, curel.idx)
if newpath == '':
logging.warning(_('No path for vector : ')+ curel.idx + _(' - Ignoring it !'))
continue
curel.saveas(newpath)
myproject.add_param(curgroup, curel.idx, sanit_path(curel.filename, absolute, draw_type.VECTORS))
except:
logging.error(_('Error in saving vectors'))
# cross sections
try:
curgroup = PROJECT_CS
for curel in self.iterator_over_objects(draw_type.VECTORS):
if isinstance(curel, crosssections):
myproject.add_param(curgroup, curel.idx, sanit_path(curel.filename, absolute, draw_type.VECTORS))
except:
logging.error(_('Error in saving cross sections'))
# nuages de points
try:
curgroup = PROJECT_CLOUD
for curel in self.iterator_over_objects(draw_type.CLOUD):
myproject.add_param(curgroup, curel.idx, sanit_path(curel.filename, absolute, draw_type.CLOUD))
except:
logging.error(_('Error in saving clouds'))
# palettes
try:
if self.project_pal is not None:
curgroup = PROJECT_PALETTE
for curel in self.project_pal.keys():
myproject.add_param(curgroup, curel, sanit_path(self.project_pal[curel].filename, absolute, draw_type.OTHER))
except:
logging.error(_('Error in saving palettes'))
# tiles
try:
curgroup = PROJECT_TILES
for curel in self.iterator_over_objects(draw_type.TILES):
myproject.add_param(curgroup, curel.idx, sanit_path(curel.filename, absolute, draw_type.OTHER))
myproject.add_param(curgroup, 'data_dir', sanit_path(curel.linked_data_dir, absolute, draw_type.OTHER))
myproject.add_param(curgroup, 'comp_dir', sanit_path(curel.linked_data_dir_comp, absolute, draw_type.OTHER))
except:
logging.error(_('Error in saving tiles'))
# LAZ GRID
try:
if self.mylazgrid is not None:
curgroup = PROJECT_LAZ
myproject.add_param(curgroup, 'data_dir', sanit_path(self.mylazgrid.dir, absolute, draw_type.OTHER))
myproject.add_param(curgroup, 'classification', self.mylazgrid.colors.class_name)
except:
logging.error(_('Error in saving laz grid'))
myproject.Save(fn)
[docs]
def help_project(self):
""" Help for project file.
Define which elements can be saved in a project file.
"""
logging.info(_('Project file help'))
logging.info(_('Project file is a file containing some information about the current project.'))
logging.info(_('It can contain the following informations :'))
logging.info(_(' - Arrays :'))
logging.info(_(' - id'))
logging.info(_(' - filename in relative or absolute path'))
logging.info(_(' - Cross sections :'))
logging.info(_(' - id'))
logging.info(_(' - filename in relative or absolute path'))
logging.info(_(' - Vectors :'))
logging.info(_(' - id'))
logging.info(_(' - filename in relative or absolute path'))
logging.info(_(' - Clouds :'))
logging.info(_(' - id'))
logging.info(_(' - filename in relative or absolute path'))
logging.info(_(' - Tiles :'))
logging.info(_(' - id'))
logging.info(_(' - filename in relative or absolute path'))
logging.info(_(' - LAZ grid :'))
logging.info(_(' - data_dir : directory containing the NUMPY grid'))
logging.info(_(' - classification : classification of the laz files'))
logging.info(_(' - Palettes :'))
logging.info(_(' - id'))
logging.info(_(' - filename in relative or absolute path'))
logging.info(_(' - Wolf2D CPU results :'))
logging.info(_(' - id'))
logging.info(_(' - filename in relative or absolute path'))
logging.info(_(' - Wolf2D GPU results :'))
logging.info(_(' - id'))
logging.info(_(' - filename in relative or absolute path'))
logging.info(_(' - Palette-Array links :'))
logging.info(_(' - id of the array'))
logging.info(_(' - id of the palette'))
logging.info(_(' - Vector-Array links :'))
logging.info(_(' - id of the array'))
logging.info(_(' - id of the vector (containing only 1 zone and 1 vector)'))
logging.info(_(' - Cross section links :'))
logging.info(_(' - id of the cross section'))
logging.info(_(' - id of the vector to sort along'))
logging.info(_(' - id of the zone to link'))
logging.info(_(' - downfirst : True or False'))
logging.info('')
logging.info(_('A tabulation is used to separate the value and the key.'))
logging.info('')
logging.info(_('Exemple :'))
logging.info('')
logging.info('array:')
logging.info('myid1\tmyfilename_array1')
logging.info('myid2\tmy../filename_array2')
logging.info('vector:')
logging.info('myvec1\tmy../../filename_vecz1')
logging.info('myvec2\tmyfilename_vecz2')
logging.info('laz_grid:')
logging.info('data_dir\tD:\\MODREC-Vesdre\\LAZ_Vesdre\\2023\\grids_flt32')
logging.info('classification\tSPW-Geofit 2023')
[docs]
def plot_laz_around_active_vec(self):
""" Plot laz data around active vector """
if self.active_vector is None:
logging.warning(_('Please activate a vector'))
return
if self.mylazgrid is None:
logging.warning(_('No laz grid'))
return
_val = self._dialogs.ask_integer(_('Enter the size of the window around the active vector [cm]'), _('Window size'), _('Window size'), 500, 0, 2000)
if _val is None:
return
value = _val / 100.
fig = self.mylazgrid.plot_laz_wx(self.active_vector.linestring, length_buffer=value, show=True)
if self.active_array is not None:
copy_vec = vector()
copy_vec.myvertices = self.active_vector.myvertices.copy()
copy_vec.split(abs(self.active_array.dx)/2., False)
copy_vec.get_values_on_vertices(self.active_array)
s,z = copy_vec.get_sz()
notmasked = np.where(z != -99999.)
fig.plot(s[notmasked],z[notmasked], c='black', linewidth=2.0)
[docs]
def clip_laz_gridded(self):
""" Clip laz grid on current zoom """
if self.mylazgrid is None:
logging.warning(_('No laz grid -- Please initialize it !'))
return
curbounds = [[self.xmin, self.xmin + self.width], [self.ymin, self.ymin + self.height]]
if self.active_laz is None:
newobj = Wolf_LAZ_Data()
newobj.classification = self.mylazgrid.colors
newobj.from_grid(self.mylazgrid, curbounds)
self.add_object('laz', newobj= newobj)
else:
if self._dialogs.ask_yes_no(_('Do you want to keep the current data ?'), _('Keep data ?'), wx.YES_NO | wx.ICON_QUESTION):
newobj = Wolf_LAZ_Data()
newobj.classification = self.mylazgrid.colors
newobj.from_grid(self.mylazgrid, curbounds)
self.add_object('laz', newobj= newobj)
else:
self.active_laz.from_grid(self.mylazgrid, curbounds)
logging.info(_('Clip LAZ grid on current zoom'))
logging.info(_('Bounds {}-{} {}-{}').format(curbounds[0][0],curbounds[0][1],curbounds[1][0],curbounds[1][1]))
logging.info(_('Nb points : {:_}').format(self.active_laz.num_points))
[docs]
def filter_active_laz(self):
""" Filter active laz data """
if self.active_laz is None:
logging.warning(_('No laz data'))
return
codes = self.active_laz.codes_unique()
names = [self.active_laz.classification.classification[curcode][0] for curcode in codes]
used_codes = self._dialogs.ask_multi_choice(_('Choose the codes to keep'), _('Codes'), names, parent=self)
if used_codes is None:
logging.info(_('Filter cancelled'))
return
used_codes = [codes[cur] for cur in used_codes]
self.active_laz.filter_data(used_codes)
logging.info(_('Filter done - Nb points : {:_}').format(self.active_laz.num_points))
[docs]
def descimate_laz_data(self, factor:int = 10):
""" Descimate data """
if self.active_laz is None:
logging.warning(_('No laz data'))
return
self.active_laz.descimate(factor)
[docs]
def select_active_array_from_laz(self, array:WolfArray = None, used_codes:list = None, chunk_size:float = 500.):
""" select some nodes from laz data
:param array: array to fill
:param used_codes: codes to use
"""
if self.mylazgrid is None:
logging.info(_('No laz grid - Aborting !'))
return
if array is None:
logging.error(_('No array'))
return
if used_codes is None:
keycode = [key for key,val in self.mylazgrid.colors.classification.items()]
names = [val[0] for key,val in self.mylazgrid.colors.classification.items()]
used_codes = self._dialogs.ask_multi_choice(_('Choose the codes to use'), _('Codes'), names, parent=self)
if used_codes is None:
return
used_codes = [float(keycode[cur]) for cur in used_codes]
curbounds = array.get_bounds()
# align bounds on chunk_size
curbounds[0][0] = curbounds[0][0] - curbounds[0][0] % chunk_size
curbounds[0][1] = curbounds[0][1] + chunk_size - curbounds[0][1] % chunk_size
curbounds[1][0] = curbounds[1][0] - curbounds[1][0] % chunk_size
curbounds[1][1] = curbounds[1][1] + chunk_size - curbounds[1][1] % chunk_size
chunck_x = np.arange(curbounds[0][0], curbounds[0][1], chunk_size)
chunck_y = np.arange(curbounds[1][0], curbounds[1][1], chunk_size)
for curx in tqdm(chunck_x, 'Chunks'):
for cury in chunck_y:
curbounds = [[curx, curx + chunk_size], [cury, cury + chunk_size]]
logging.info(_('Scan {}-{} {}-{}').format(curbounds[0][0],curbounds[0][1],curbounds[1][0],curbounds[1][1]))
mylazdata = self.mylazgrid.scan(curbounds)
# logging.info(_('Scan done'))
data = {}
for curcode in used_codes:
data[curcode] = mylazdata[mylazdata[:, 3] == curcode]
for curdata in data.values():
if curdata.shape[0] == 0:
continue
i,j = array.get_ij_from_xy(curdata[:, 0], curdata[:, 1])
keys = np.vstack((i,j)).T
# unique keys
keys = np.unique(keys, axis=0)
array.SelectionData._add_nodes_to_selectionij(keys, verif = False)
array.SelectionData.update_nb_nodes_selection()
self.Paint()
logging.info(_('Selection done'))
[docs]
def fill_active_array_from_laz(self, array:WolfArray = None, used_codes:list = [], operator:int = -1, chunk_size:float = 500.):
""" Fill active array with laz data
:param array: array to fill
:param used_codes: codes to use
:param operator: operator to use
"""
if self.mylazgrid is None:
logging.info(_('No laz grid - Aborting !'))
return
if array is None:
logging.error(_('No array'))
return
if len(used_codes) == 0 :
keycode = [key for key,val in self.mylazgrid.colors.classification.items()]
names = [val[0] for key,val in self.mylazgrid.colors.classification.items()]
used_codes = self._dialogs.ask_multi_choice(_('Choose the codes to use'), _('Codes'), names, parent=self)
if used_codes is None:
return
used_codes = [float(keycode[cur]) for cur in used_codes]
if operator == -1:
op = self._dialogs.ask_single_choice(_('Choose the operator'), _('Operator'), ['max', 'percentile 95', 'percentile 5', 'min', 'mean', 'median', 'sum'], parent=self)
if op is None:
return
if op == 'max':
operator = np.max
elif op == 'min':
operator = np.min
elif op == 'mean':
operator = np.mean
elif op == 'median':
operator = np.median
elif op == 'sum':
operator = np.sum
elif op == 'percentile 95':
operator = lambda x: np.percentile(x, 95)
elif op == 'percentile 5':
operator = lambda x: np.percentile(x, 5)
else:
return
minpoints = self._dialogs.ask_integer(_('Minimum number of points to operate'), _('Minimum'), _('Minimum points'), 1, 1, 20)
if minpoints is None:
return
logging.info(_('This could take some time for large area...\n Take a coffee and relax!'))
bounds = array.get_bounds()
# align bounds on chunk_size
bounds[0][0] = bounds[0][0] - bounds[0][0] % chunk_size
bounds[0][1] = bounds[0][1] + chunk_size - bounds[0][1] % chunk_size
bounds[1][0] = bounds[1][0] - bounds[1][0] % chunk_size
bounds[1][1] = bounds[1][1] + chunk_size - bounds[1][1] % chunk_size
chunks_x = np.arange(bounds[0][0], bounds[0][1], chunk_size)
chunks_y = np.arange(bounds[1][0], bounds[1][1], chunk_size)
for curx in tqdm(chunks_x, 'Chunks'):
for cury in chunks_y:
curbounds = [[curx, curx + chunk_size], [cury, cury + chunk_size]]
logging.info(_('Scan {}-{} {}-{}').format(curbounds[0][0],curbounds[0][1],curbounds[1][0],curbounds[1][1]))
mylazdata = self.mylazgrid.scan(curbounds)
# logging.info(_('Scan done'))
if len(mylazdata) == 0:
continue
# Test codes
data = {}
for curcode in used_codes:
data[curcode] = mylazdata[mylazdata[:, 3] == curcode]
# Treat data for each code
for curdata in data.values():
if curdata.shape[0] == 0:
continue
else:
logging.info(_('Code {} : {} points'.format(curdata[0,3], curdata.shape[0])))
# get i,j from x,y
i,j = array.get_ij_from_xy(curdata[:, 0], curdata[:, 1])
# keep only valid points -- inside the array
used = np.where((i >=0) & (i < array.nbx) & (j >=0) & (j < array.nby))[0]
if len(used) == 0:
continue
i = i[used]
j = j[used]
z = curdata[used, 2]
# create a key array
keys = np.vstack((i,j)).T
# find unique keys
keys = np.unique(keys, axis=0)
# create a ijz array
ijz = np.vstack((i, j, z)).T
# sort ijz array according to keys
#
# the most important indice is the last one enumerated in lexsort
# see : https://numpy.org/doc/stable/reference/generated/numpy.lexsort.html
ijz = ijz[np.lexsort((ijz[:,1], ijz[:,0]))]
# find first element of each key
idx = np.where(np.abs(np.diff(ijz[:,0])) + np.abs(np.diff(ijz[:,1])) != 0)[0]
# add last element
idx = np.concatenate((idx, [ijz.shape[0]]))
assert len(idx) == keys.shape[0], 'Error in filling'
logging.info(_('Cells to fill : {}'.format(len(idx))))
# apply operator
vals = {}
start_ii = 0
for ii, key in enumerate(keys):
end_ii = idx[ii]+1
if end_ii - start_ii >= minpoints:
vals[(key[0], key[1])] = operator(ijz[start_ii:end_ii,2])
start_ii = end_ii
if len(vals) > 0:
# create a new ijz array
newijz = np.asarray([[key[0], key[1], val] for key, val in vals.items()], dtype = np.float32)
array.fillin_from_ijz(newijz)
array.reset_plot()
self.Paint()
logging.info(_('Filling done !'))
[docs]
def count_active_array_from_laz(self, array:WolfArray = None, used_codes:list = [], chunk_size:float = 500.):
""" Fill active array with laz data
:param array: array to fill
:param used_codes: codes to use
:param operator: operator to use
"""
if self.mylazgrid is None:
logging.info(_('No laz grid - Aborting !'))
return
if array is None:
logging.error(_('No array'))
return
if len(used_codes) == 0 :
keycode = [key for key,val in self.mylazgrid.colors.classification.items()]
names = [val[0] for key,val in self.mylazgrid.colors.classification.items()]
used_codes = self._dialogs.ask_multi_choice(_('Choose the codes to use'), _('Codes'), names, parent=self)
if used_codes is None:
return
data = {}
used_codes = [float(keycode[cur]) for cur in used_codes]
bounds = array.get_bounds()
# align bounds on chunk_size
bounds[0][0] = bounds[0][0] - bounds[0][0] % chunk_size
bounds[0][1] = bounds[0][1] + chunk_size - bounds[0][1] % chunk_size
bounds[1][0] = bounds[1][0] - bounds[1][0] % chunk_size
bounds[1][1] = bounds[1][1] + chunk_size - bounds[1][1] % chunk_size
chunks_x = np.arange(bounds[0][0], bounds[0][1], chunk_size)
chunks_y = np.arange(bounds[1][0], bounds[1][1], chunk_size)
for curx in tqdm(chunks_x, 'Chunks'):
for cury in chunks_y:
curbounds = [[curx, curx + chunk_size], [cury, cury + chunk_size]]
logging.info(_('Scan {}-{} {}-{}').format(curbounds[0][0],curbounds[0][1],curbounds[1][0],curbounds[1][1]))
mylazdata = self.mylazgrid.scan(curbounds)
if len(mylazdata) == 0:
continue
# Test codes
data = {}
for curcode in used_codes:
data[curcode] = mylazdata[mylazdata[:, 3] == curcode]
# Treat data for each code
for curdata in data.values():
if curdata.shape[0] == 0:
continue
else:
logging.info(_('Code {} : {} points'.format(curdata[0,3], curdata.shape[0])))
# get i,j from x,y
i,j = array.get_ij_from_xy(curdata[:, 0], curdata[:, 1])
# keep only valid points -- inside the array
used = np.where((i >=0) & (i < array.nbx) & (j >=0) & (j < array.nby))[0]
if len(used) == 0:
continue
i = i[used]
j = j[used]
z = curdata[used, 2]
# create a key array
keys = np.vstack((i,j)).T
# find unique keys
keys = np.unique(keys, axis=0)
# create a ijz array
ijz = np.vstack((i, j, z)).T
# sort ijz array according to keys
#
# the most important indice is the last one enumerated in lexsort
# see : https://numpy.org/doc/stable/reference/generated/numpy.lexsort.html
ijz = ijz[np.lexsort((ijz[:,1], ijz[:,0]))]
# find first element of each key
idx = np.where(np.abs(np.diff(ijz[:,0])) + np.abs(np.diff(ijz[:,1])) != 0)[0]
# add last element
idx = np.concatenate((idx, [ijz.shape[0]]))
assert len(idx) == keys.shape[0], 'Error in filling'
logging.info(_('Cells to fill : {}'.format(len(idx))))
# apply operator
vals = {}
start_ii = 0
for ii, key in enumerate(keys):
end_ii = idx[ii]+1
vals[(key[0], key[1])] = end_ii - start_ii
start_ii = end_ii
if len(vals) > 0:
# create a new ijz array
newijz = np.asarray([[key[0], key[1], val] for key, val in vals.items()], dtype = np.float32)
array.fillin_from_ijz(newijz)
array.reset_plot()
self.Paint()
logging.info(_('Counting done'))
[docs]
def init_laz_from_lazlasnpz(self, fn=None):
""" Read LAZ data stored in one file
:param fn: filename (extension .laz, .las, .npz)
"""
if fn is None:
filternpz = "LAZ (*.laz)|*.laz|LAS (*.las)|*.las|npz (*.npz)|*.npz|all (*.*)|*.*"
fn = self._dialogs.ask_file_open(_('Choose a file containing LAS data'), wildcard=filternpz, parent=self)
if fn is None:
return
lazobj = Wolf_LAZ_Data()
lazobj.from_file(fn)
self.add_object('laz', newobj= lazobj)
logging.info(_('LAZ data read from file : ')+ fn)
logging.info(_('Stored in internal variable'))
logging.info(_('Nb points : {:_}').format(self.active_laz.num_points))
if self.linked:
if len(self.linkedList) > 0:
for curframe in self.linkedList:
if not curframe is self:
curframe.mylazdata.append(self.active_laz)
[docs]
def _choice_laz_classification(self):
classification = self._dialogs.ask_single_choice(_('Choose the classification'), _('Classification'), ['SPW-Geofit 2023', 'SPW 2013-2014'], parent=self)
return classification
[docs]
def init_laz_from_gridinfos(self, dirlaz:str = None, classification:Literal['SPW-Geofit 2023', 'SPW 2013-2014', 'SPW 2021-2022'] = 'SPW-Geofit 2023'):
if dirlaz is None:
dirlaz = self._dialogs.ask_directory(_('Choose directory where LAZ data/gridinfo are stored'), default_path=str(self.default_laz), parent=self)
if dirlaz is None:
return
self.mylazgrid = xyz_laz_grids(dirlaz)
if classification not in ['SPW-Geofit 2023', 'SPW 2013-2014']:
classification = self._choice_laz_classification()
if classification is None:
logging.warning(_('No classification chosen - Abort !'))
return
elif classification == 'SPW 2013-2014':
self.mylazgrid.colors.init_2013()
elif classification == "SPW 2021-2022":
self.mylazgrid.colors.init_2021_2022()
else:
self.mylazgrid.colors.init_2023()
if self.linked:
if len(self.linkedList) > 0:
for curframe in self.linkedList:
curframe.mylazgrid = self.mylazgrid
[docs]
def managebanks(self):
if self.notebookbanks is None:
self.notebookbanks = PlotNotebook(self)
self.mypagebanks = self.notebookbanks.add(_("Manager banks interpolator"), "ManagerInterp")
msg = ''
if self.active_cs is None:
msg += _(' The is no cross section. Please activate the desired object !')
if msg != '':
dlg = wx.MessageBox(msg, 'Required action')
return
if self.active_cs.linked_zones is None:
msg += _(' The active zones is None. Please link the desired object to the cross sections !\n')
# if self.active_zone is None:
# msg+=_(' The active zone is None. Please activate the desired object !\n')
if msg != '':
dlg = wx.MessageBox(msg, 'Required action')
return
self.mypagebanks.pointing(self, self.active_cs, self.active_vector)
self.notebookbanks.Show(True)
[docs]
def _set_fn_fnpos_gltf(self):
"""
Définition du nom de fichier GLTF/GLB à lire pour réaliser la comparaison
Utilisation d'une fenêtre de dialogue WX
Cette fonction n'est a priori appelée que depuis set_fn_fnpos_gltf
"""
fn = self._dialogs.ask_file_open(
_('Choose filename'),
wildcard='glb (*.glb)|*.glb|gltf2 (*.gltf)|*.gltf|All (*.*)|*.*',
parent=self,
)
if fn is None:
return
fnpos = self._dialogs.ask_file_open(
_('Choose pos filename'),
wildcard='pos (*.pos)|*.pos|All (*.*)|*.*',
parent=self,
)
if fnpos is None:
return
if self.link_params is None:
self.link_params = {}
self.link_params['gltf file'] = fn
self.link_params['gltf pos'] = fnpos
return fn
[docs]
def set_fn_fnpos_gltf(self):
"""
Définition ou récupération du nom de fichier GLTF/GLB à lire pour réaliser la comparaison
Le nom de fichier est stocké dans la liste des paramètres partagés de façon à ce que l'appel de mise à jour puisse s'effectuer dans n'importe quel frame
"""
fn = ''
fnpos = ''
if self.linked:
for curgui in self.linkedList:
if curgui.link_params is not None:
if 'gltf file' in curgui.link_params.keys():
fn = curgui.link_params['gltf file']
fnpos = curgui.link_params['gltf pos']
break
elif self.link_params is None:
self.link_params = {}
fn = self._set_fn_fnpos_gltf()
if fn == '':
self._set_fn_fnpos_gltf()
[docs]
def read_last_result(self):
"""Lecture du dernier résultat pour les modèles ajoutés et plottés"""
self.currently_readresults = True
pgbar = self._dialogs.create_progress(
_('Reading results'),
_('Reading results'),
len(self.myres2D),
self,
style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
)
for id, curmodel in enumerate(self.iterator_over_objects(draw_type.RES2D)):
curmodel: Wolfresults_2D
logging.info(_('Updating {} - Last result'.format(curmodel.idx)))
curmodel.read_oneresult()
curmodel.set_currentview()
self._update_sim_explorer(curmodel)
pgbar.update(id + 1, _('Reading results') + ' - ' + curmodel.idx)
pgbar.close()
self.Refresh()
self.currently_readresults = False
self._update_tooltip()
[docs]
def read_one_result(self, which:int):
"""
Lecture d'un résultat spécific pour les modèles ajoutés et plottés
:param which: result index (0-based) -- -1 for last result
0 = first result
"""
self.currently_readresults = True
for curmodel in self.iterator_over_objects(draw_type.RES2D):
curmodel: Wolfresults_2D
if curmodel.checked:
logging.info(_('Updating {} - Specific result {}'.format(curmodel.idx, which)))
curmodel.read_oneresult(which)
curmodel.set_currentview()
self._update_sim_explorer(curmodel)
self.Refresh()
self.currently_readresults = False
self._update_tooltip()
[docs]
def simul_previous_step(self):
"""
Mise à jour au pas précédent
"""
self.currently_readresults = True
for curmodel in self.iterator_over_objects(draw_type.RES2D):
curmodel: Wolfresults_2D
logging.info(_('Updating {} - Previous result'.format(curmodel.idx)))
curmodel.read_previous()
curmodel.set_currentview()
self._update_sim_explorer(curmodel)
self.Refresh()
self.currently_readresults = False
self._update_tooltip()
[docs]
def particle_next_step(self):
""" Mise à jour au pas suivant """
for curps in self.iterator_over_objects(draw_type.PARTICLE_SYSTEM):
curps: Particle_system
logging.info(_('Updating {} - Next result'.format(curps.idx)))
curps.next_step()
self._update_tooltip()
self.Refresh()
[docs]
def particle_previous_step(self):
""" Mise à jour au pas précédent """
for curps in self.iterator_over_objects(draw_type.PARTICLE_SYSTEM):
curps: Particle_system
logging.info(_('Updating {} - Next result'.format(curps.idx)))
curps.previous_step()
self._update_tooltip()
self.Refresh()
[docs]
def simul_next_step(self):
"""
Mise à jour au pas suivant
"""
self.currently_readresults = True
for curmodel in self.iterator_over_objects(draw_type.RES2D):
curmodel: Wolfresults_2D
logging.info(_('Updating {} - Next result'.format(curmodel.idx)))
curmodel.read_next()
curmodel.set_currentview()
self._update_sim_explorer(curmodel)
self.Refresh()
self.currently_readresults = False
self._update_tooltip()
[docs]
def _select_laz_source(self):
""" Select laz source """
if self.active_laz is None and self.mylazgrid is None:
logging.warning(_('No LAZ data loaded/initialized !'))
return None
elif self.active_laz is None:
# No active laz data
laz_source = self.mylazgrid
elif self.mylazgrid is None:
# No laz grid
laz_source = self.active_laz
else:
# We have both
choices = [_('From active LAZ data'), _('From newly extracted data')]
keys = self.get_list_keys(draw_type.LAZ, None)
if len(keys) > 1:
choices.append(_('From multiple LAZ data'))
source = self._dialogs.ask_single_choice(_("Pick a data source"), "Choices", choices, parent=self)
if source is None:
return None
idx = choices.index(source)
if idx == 0:
laz_source = self.active_laz
elif idx == 1:
laz_source = self.mylazgrid
else:
used_keys = self._dialogs.ask_multi_choice(
_('Choose the LAZ data to use\n\nIf multiple, a new one will be created !'),
_('LAZ data'),
keys,
parent=self,
)
if used_keys is None:
return None
used_keys = [keys[cur] for cur in used_keys]
laz_source = Wolf_LAZ_Data()
for curkey in used_keys:
laz_source.merge(self.get_obj_from_id(curkey, draw_type.LAZ))
self.add_object('laz', newobj=laz_source, id = 'Merged LAZ data')
return laz_source
[docs]
def _choice_laz_colormap(self) -> int:
choices, ass_values = choices_laz_colormap()
preselected = None
if self.active_laz is not None:
if self.active_laz.associated_color is not None:
preselected = ass_values.index(self.active_laz.associated_color)
colormap = self._dialogs.ask_single_choice(_("Pick a colormap"), "Choices", choices, preselected=preselected, parent=self)
if colormap is None:
return self.active_laz.associated_color
idx = choices.index(colormap)
return ass_values[idx]
# add_lidaxe / menu_lidaxe / Onmenulidaxe → delegators defined near line 2060
[docs]
def _run_compare_arrays(self, dlg):
""" Run the comparison of two arrays"""
from .ui.wolf_multiselection_collapsiblepane import Wolf_CompareArrays_Selection
assert isinstance(dlg, Wolf_CompareArrays_Selection), 'Dialog must be a wx.Dialog instance'
dlg: Wolf_CompareArrays_Selection
vals = dlg.get_values()
id1 = vals[_('Reference array')][0]
id2 = vals[_('Comparison array')][0]
min_area = dlg.get_min_area()
threshold = dlg.get_threshold()
nb_patches = dlg.get_max_patches()
ref:WolfArray
comp:WolfArray
ref = self.get_obj_from_id(id1, draw_type.ARRAYS)
comp = self.get_obj_from_id(id2, draw_type.ARRAYS)
if ref is None or comp is None:
logging.warning(_('You must select two arrays to compare !'))
return
assert isinstance(ref, WolfArray), 'Reference object must be a WolfArray instance'
assert isinstance(comp, WolfArray), 'Comparison object must be a WolfArray instance'
if not ref.is_like(comp):
logging.error(_('The two arrays must have the same shape and type !'))
return
# if only 2 arrays, we can use the CompareArrays_wx directly
from .report.compare_arrays import CompareArrays_wx
try:
newsheet = CompareArrays_wx(ref, comp,
size=(800, 600),
ignored_patche_area= min_area,
threshold=threshold,
nb_max_patches = nb_patches,)
newsheet.Show()
self.add_object('vector', newobj = newsheet.get_zones(), ToCheck = True, id = 'compare_arrays_{}'.format(ref.idx + comp.idx))
except:
logging.error(_('Error in comparing arrays\n'))
dlg.Destroy()
[docs]
def _compare_arrays(self):
""" Compare two arrays """
arrays = self.get_list_keys(draw_type.ARRAYS, checked_state = None)
if len(arrays) == 0:
logging.warning(_('No arrays to compare !'))
return
elif len(arrays) == 1:
logging.warning(_('Only one array to compare - Nothing to do !'))
return
from .ui.wolf_multiselection_collapsiblepane import Wolf_CompareArrays_Selection
dlg = Wolf_CompareArrays_Selection(parent = self,
title = _("Choose the arrays to compare"),
info = _("Select the reference and comparison arrays"),
values_dict = {_('Reference array'): arrays,
_('Comparison array'): arrays},
callback= self._run_compare_arrays,
destroyOK = True,
styles = [wx.LB_SINGLE, wx.LB_SINGLE]
)
dlg.ShowModal()
# ------------------------------------------------------------------
# Direct-bound menu handlers (wired in __init__ via Bind)
# ------------------------------------------------------------------
# ---- File --------------------------------------------------------
[docs]
def _on_open_project(self, event):
filterProject = "proj (*.proj)|*.proj|param (*.param)|*.param|all (*.*)|*.*"
filename = self._dialogs.ask_file_open("Choose file", wildcard=filterProject, parent=self)
if filename is None:
return
old_dir = os.getcwd()
os.chdir(os.path.dirname(filename))
self.read_project(filename)
os.chdir(old_dir)
self._autoscale_if_needed()
[docs]
def _on_save_project(self, event):
filterProject = "proj (*.proj)|*.proj|param (*.param)|*.param|all (*.*)|*.*"
filename = self._dialogs.ask_file_save("Name your file", wildcard=filterProject, parent=self)
if filename is None:
return
abspath = True
if not self._dialogs.ask_yes_no(_('Do you want to save the paths in absolute mode ?'), _('Relative paths'), style=DialogStyles.YES_NO):
abspath = False
self.save_project(filename, absolute=abspath)
[docs]
def _on_save_canvas(self, event):
fn, ds = self.save_canvasogl(mpl=True)
all_images = self.save_linked_canvas(fn[:-4], mpl=True, ds=ds, add_title=True)
self.assembly_images(all_images, mode=self.assembly_mode)
[docs]
def _on_copy_canvas(self, event):
self.copy_canvasogl()
# ---- GLTF --------------------------------------------------------
[docs]
def _on_gltf_export(self, event):
if self.active_array is None or self.active_vector is None:
msg = ''
if self.active_array is None:
msg += _('Active array is None\n')
if self.active_vector is None:
msg += _('Active vector is None\n')
wx.MessageBox(msg + _('\nRetry !\n'))
return
curarray = self.active_array
curvec = self.active_vector
curvec.find_minmax()
i1, j1 = curarray.get_ij_from_xy(curvec.xmin, curvec.ymin)
x1, y1 = curarray.get_xy_from_ij(i1, j1)
x1 -= curarray.dx / 2.
y1 -= curarray.dy / 2.
i2, j2 = curarray.get_ij_from_xy(curvec.xmax, curvec.ymax)
x2, y2 = curarray.get_xy_from_ij(i2, j2)
x2 += curarray.dx / 2.
y2 += curarray.dy / 2.
mybounds = [[x1, x2], [y1, y2]]
fn = self._dialogs.ask_file_save(
_('Choose filename'),
wildcard='glb (*.glb)|*.glb|gltf2 (*.gltf)|*.gltf|All (*.*)|*.*',
parent=self,
)
if fn is None:
return
with self._dialogs.show_busy(_('Export to gltf/glb')):
curarray.export_to_gltf(mybounds, fn)
[docs]
def _on_gltf_import(self, event):
if self.active_array is None:
wx.MessageBox(_('Active array is None\n\nRetry !\n'))
return
curarray = self.active_array
fn = self._dialogs.ask_file_open(
_('Choose filename'),
wildcard='glb (*.glb)|*.glb|gltf2 (*.gltf)|*.gltf|All (*.*)|*.*',
parent=self,
)
if fn is None:
return
fnpos = self._dialogs.ask_file_open(
_('Choose pos filename'),
wildcard='pos (*.pos)|*.pos|All (*.*)|*.*',
parent=self,
)
if fnpos is None:
return
choices = ["matplotlib", "scipy"]
method = self._dialogs.ask_single_choice(
_("Pick an interpolation method"),
_("Choices"),
choices,
parent=self,
)
if method is None:
return
with self._dialogs.show_busy(_('Importing gltf/glb')):
try:
curarray.import_from_gltf(fn, fnpos, method)
except Exception:
pass
[docs]
def _on_gltf_compare(self, event):
if self.active_array is None:
wx.MessageBox(_('Active array is None\n\nRetry !\n'))
return
self.set_blender_sculpting()
self.set_fn_fnpos_gltf()
self.update_blender_sculpting()
[docs]
def _on_gltf_update(self, event):
if self.active_array is None:
wx.MessageBox(_('Active array is None\n\nRetry !\n'))
return
self.set_fn_fnpos_gltf()
self.update_blender_sculpting()
# ---- Simulation --------------------------------------------------
[docs]
def _on_sim_create_mb(self, event):
self.create_2D_MB_model()
[docs]
def _on_sim_create_gpu(self, event):
self.create_2D_GPU_model()
[docs]
def _on_sim_create_1d(self, event):
self.frame_create1Dfrom2D = GuiNotebook1D(mapviewer=self)
logging.info(_('New window available - Wolf1D.'))
[docs]
def _on_sim_create_hydro(self, event):
self.open_hydrological_model()
[docs]
def _on_add_lidaxe(self, event):
self.add_lidaxe()
[docs]
def _on_set_comparison(self, event):
self.compare_results = Compare_Arrays_Results(self, share_cmap_array=True, share_cmap_diff=True)
add_elt = True
while add_elt:
add_elt = self.compare_results.add()
if len(self.compare_results.paths) < 2:
logging.warning(_('Not enough elements to compare !'))
self.compare_results = None
return
self.compare_results.bake()
self._autoscale_if_needed()
[docs]
def _on_multiviewer(self, event):
nb = self._dialogs.ask_integer(_("Additional viewers"), _("How many?"), _("How many additional viewers?"), 1, 0, 5)
if nb is None:
return
if nb > 0:
new_name = self._dialogs.ask_text(
None,
_('New name for the current viewer'),
_('Rename'),
default=self.viewer_name,
style=wx.OK | wx.CANCEL,
)
if new_name is not None:
self.viewer_name = new_name
self.SetName(self.viewer_name)
for i in range(nb):
self.add_viewer_and_link()
else:
logging.warning(_('No additional viewer !'))
[docs]
def _on_viewer3d(self, event):
self.active_viewer3d = Wolf_Viewer3D(self, _("3D Viewer"))
self.active_viewer3d.Show()
self.myviewers3d.append(self.active_viewer3d)
for curarray in self.iterator_over_objects(draw_type.ARRAYS):
curarray: WolfArray
if curarray.checked:
if curarray._array3d is None:
curarray.prepare_3D()
if self.active_viewer3d not in curarray.viewers3d:
curarray.viewers3d.append(self.active_viewer3d)
self.active_viewer3d.add_array(curarray.idx, curarray._array3d)
self.active_viewer3d.autoscale()
# ---- Create objects ----------------------------------------------
[docs]
def _on_create_array_xyz(self, event):
self.add_object(which='array_xyz', ToCheck=True)
self._autoscale_if_needed()
[docs]
def _on_create_array_lidar2002(self, event):
sel = self._dialogs.ask_single_choice(
_('What source of data?'),
_('Lidar 2002'),
[_('First echo'), _('Second echo')],
parent=self,
)
if sel == _('First echo'):
self.add_object(which='array_lidar_first', ToCheck=True)
elif sel == _('Second echo'):
self.add_object(which='array_lidar_second', ToCheck=True)
[docs]
def _on_create_view(self, event):
newview = WolfViews(mapviewer=self)
self.add_object('views', newobj=newview)
[docs]
def _on_create_array(self, event):
newarray = WolfArray(create=True, mapviewer=self)
self.add_object('array', newobj=newarray)
[docs]
def _on_create_vector(self, event):
newzones = Zones(parent=self)
self.add_object('vector', newobj=newzones)
[docs]
def _on_create_clouds(self, event):
newcloud = cloud_of_clouds()
self.add_object('clouds', newobj=newcloud)
[docs]
def _on_create_manager2d(self, event):
from .mesh2d.config_manager import config_manager_2D
config_manager_2D(mapviewer=self)
[docs]
def _on_create_scenario2d(self, event):
from .scenario.config_manager import Config_Manager_2D_GPU
Config_Manager_2D_GPU(mapviewer=self, create_ui_if_wx=True)
[docs]
def _on_create_acceptability(self, event):
from .acceptability.acceptability_gui import AcceptabilityGui
mgr = AcceptabilityGui()
mgr.mapviewer = self
mgr.Show()
[docs]
def _on_create_inbe(self, event):
from .insyde_be.INBE_gui import INBEGui
mgr = INBEGui()
mgr.mapviewer = self
mgr.Show()
[docs]
def _on_create_bc_manager(self, event):
if self.active_array is None:
return
choices = {'WOLF prev': 1, 'WOLF OO': 2, 'GPU': 3}
method = self._dialogs.ask_single_choice(
_("Which version of BC Manager"),
_("Version"),
['WOLF prev', 'WOLF OO', 'GPU'],
parent=self,
)
if method is None:
return
which_version = choices[method]
self.mybc.append(BcManager(self, linked_array=self.active_array, version=which_version,
DestroyAtClosing=False, Callback=self.pop_boundary_manager,
mapviewer=self))
ret = self.mybc[-1].FindBorders()
if ret == -1:
self.mybc.pop(-1)
return
self.active_bc = self.mybc[-1]
[docs]
def _on_create_particle_system(self, event):
self.active_particle_system = newpart = Particle_system()
self.add_object(which='particlesystem', newobj=newpart, ToCheck=True)
self.menu_particlesystem()
[docs]
def _on_create_drowning(self, event):
self.newdrowning(_('Create a drowning...'))
# ---- Add objects -------------------------------------------------
[docs]
def _on_add_array(self, event):
self.add_object(which='array', ToCheck=True)
[docs]
def _on_add_vector(self, event):
self.add_object(which='vector', ToCheck=True)
[docs]
def _on_add_cloud(self, event):
self.add_object(which='cloud', ToCheck=True)
[docs]
def _on_add_cross_sections(self, event):
self.add_object(which='cross_sections', ToCheck=True)
[docs]
def _on_add_array_crop(self, event):
self.add_object(which='array_crop', ToCheck=True)
self._autoscale_if_needed()
[docs]
def _on_add_picture_collection(self, event):
choice = self._dialogs.ask_single_choice(
_('Choose the type of picture collection'),
_('Picture Collection'),
[_('Pictures + shapefile'), _('Wolf vec format'),
_('Georeferenced pictures'), _('Pictures + Excel'), _('URL zip file')],
parent=self,
)
if choice is None:
return
if choice in [_('Pictures + shapefile'), _('Georeferenced pictures'), _('Pictures + Excel')]:
mydir = self._dialogs.ask_directory(_('Choose directory to scan for pictures'), parent=self)
if mydir is None:
return
elif choice == _('URL zip file'):
mydir = self._dialogs.ask_text(_('Enter the URL of the zip file containing the pictures'), _('URL zip file'), parent=self)
if mydir is None:
return
else:
mydir = self._dialogs.ask_file_open(
_('Choose shapefile'),
wildcard='Wolf vec (*.vec)| *.vec|Wolf vecz (*.vecz)|*.vecz|All files (*.*)|*.*',
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST,
parent=self,
)
if mydir is None:
return
if choice == _('Pictures + shapefile'):
newcollection = PictureCollection(parent=self, mapviewer=self)
newcollection.load_from_directory_with_shapefile(mydir)
elif choice == _('Georeferenced pictures'):
newcollection = PictureCollection(parent=self, mapviewer=self)
newcollection.load_from_directory_georef_pictures(mydir)
elif choice == _('Pictures + Excel'):
newcollection = PictureCollection(parent=self, mapviewer=self)
newcollection.load_from_directory_with_excel(mydir)
elif choice == _('Wolf vec format'):
newcollection = PictureCollection(filename=mydir, parent=self, mapviewer=self)
elif choice == _('URL zip file'):
newcollection = PictureCollection(parent=self, mapviewer=self)
newcollection.load_from_url_zipfile(mydir)
else:
return
count = sum(zone.nbvectors for zone in newcollection.myzones)
if count == 0:
logging.warning(_('No usable pictures found in the collection !'))
return
self.add_object('picture_collection', newobj=newcollection, ToCheck=True)
[docs]
def _on_add_tiles(self, event):
self.add_object(which='tiles', ToCheck=True)
[docs]
def _on_add_images_tiles(self, event):
self.add_object(which='imagestiles', ToCheck=True)
[docs]
def _on_add_tiles_comparator(self, event):
self.add_object(which='tilescomp', ToCheck=True)
[docs]
def _on_add_tiles_gpu(self, event):
self.add_object(which='array_tiles', ToCheck=True)
[docs]
def _on_add_clouds(self, event):
self.add_object(which='clouds', ToCheck=True)
[docs]
def _on_add_triangulation(self, event):
self.add_object(which='triangulation', ToCheck=True)
[docs]
def _on_add_wolf2d(self, event):
self.add_object(which='res2d', ToCheck=True)
self.menu_wolf2d()
[docs]
def _on_add_wolf2d_gpu(self, event):
self.add_object(which='res2d_gpu', ToCheck=True)
self.menu_wolf2d()
self.menu_2dgpu()
[docs]
def _on_add_particle_system(self, event):
self.add_object(which='particlesystem', ToCheck=True)
self.menu_particlesystem()
[docs]
def _on_add_bridges(self, event):
self.add_object(which='bridges', ToCheck=True)
[docs]
def _on_add_weirs(self, event):
self.add_object(which='weirs', ToCheck=True)
[docs]
def _on_add_view(self, event):
self.add_object(which='views', ToCheck=True)
[docs]
def _on_add_drowning(self, event):
self.newdrowning(_('Add a drowning result...'))
# ---- Tools -------------------------------------------------------
[docs]
def _on_contour_from_arrays(self, event):
newzones = self.create_Zones_from_arrays(self.get_list_objects(draw_type.ARRAYS, checked_state=True))
self.add_object('vector', newobj=newzones, ToCheck=True, id='Contours from arrays')
[docs]
def _on_calculator(self, event):
if self.calculator is None:
self.calculator = Calculator(mapviewer=self)
else:
try:
self.calculator.Show()
except Exception:
self.calculator = Calculator(mapviewer=self)
[docs]
def _on_memory_views(self, event):
if self.memory_views is None:
self.memory_views = Memory_Views()
self._memory_views_gui = Memory_Views_GUI(self, _('Memory view manager'), self.memory_views, mapviewer=self)
else:
if self._memory_views_gui is None:
self._memory_views_gui = Memory_Views_GUI(self, _('Memory view manager'), self.memory_views, mapviewer=self)
self._memory_views_gui.Show()
[docs]
def _on_memory_distances(self, event):
if self._distances is not None:
if self._distances[-1].nbvectors == 0:
logging.warning(_('No vector to show !'))
return
self._distances.showstructure(self)
[docs]
def _on_add_distances_viewer(self, event):
if self._distances is not None:
self.add_object('vector', newobj=self._distances, ToCheck=True, id='Distances')
[docs]
def _on_image_digitizer(self, event):
Digitizer()
[docs]
def _on_jupyter_kernel(self, event):
"""Start (or show info for) the embedded Jupyter kernel."""
from ._jupyter_kernel import EmbeddedKernel, show_connection_dialog
if not hasattr(self, '_embedded_kernel'):
self._embedded_kernel: EmbeddedKernel | None = None
if self._embedded_kernel is not None and self._embedded_kernel.is_running:
show_connection_dialog(self, self._embedded_kernel)
return
try:
kernel = EmbeddedKernel(self)
ok = kernel.start()
except RuntimeError as exc:
wx.MessageBox(str(exc), _('Jupyter kernel'), wx.OK | wx.ICON_ERROR, self)
return
if not ok:
wx.MessageBox(
_('The Jupyter kernel did not start within the timeout.\n'
'Check the application logs for details.'),
_('Jupyter kernel'),
wx.OK | wx.ICON_WARNING,
self,
)
return
self._embedded_kernel = kernel
show_connection_dialog(self, kernel)
# ---- Cross sections ----------------------------------------------
[docs]
def _on_cs_link_zones(self, event):
if self.active_cs is None:
wx.MessageBox(_('Active cross sections is None\nPlease activate the one desired\n\nRetry !\n'))
return
if self.active_zones is None:
wx.MessageBox(_('Active zone is None\nPlease activate the one desired\n\nRetry !\n'))
return
self.active_cs.link_external_zones(self.active_zones)
[docs]
def _on_cs_manage_banks(self, event):
if self.active_vector is None:
wx.MessageBox(_('Active vector is None\nPlease activate the one desired\n\nRetry !\n'))
return
self.managebanks()
[docs]
def _on_cs_create_banks(self, event):
self.active_cs.create_zone_from_banksbed()
self.active_cs.linked_zones.showstructure()
[docs]
def _on_cs_rename(self, event):
idxstart = self._dialogs.ask_text(
None,
_('Index to use (int)?\n\nWe will rename the cross sections with consecutive numbers starting from this point to downstream'),
_('Rename cross sections'), default='0')
if idxstart is None:
return
self.active_cs.rename(int(idxstart))
[docs]
def _on_cs_triangulate(self, event):
self.triangulate_cs()
[docs]
def _on_cs_export_gltf(self, event):
if self.active_cs is None:
wx.MessageBox(_('Active cross sections is None\nPlease activate the one desired\n\nRetry !\n'))
return
zmin = 0.
_zmin = self._dialogs.ask_float('Z minimum ?', 'Choose an elevation as base', default='')
if _zmin is not None:
zmin = _zmin
fn = self._dialogs.ask_file_save(
_('Choose filename'),
wildcard='glb (*.glb)|*.glb|gltf2 (*.gltf)|*.gltf|All (*.*)|*.*',
parent=self,
)
if fn is None:
return
self.active_cs.export_gltf(zmin, fn)
[docs]
def _on_cs_bridge_gltf(self, event):
if self.active_cs is None:
wx.MessageBox(_('Active cross sections is None\nPlease activate the one desired\n\nRetry !\n'))
return
self.start_action('bridge gltf', _('Create bridge and export gltf...'))
# ---- Cross section toggle actions --------------------------------
[docs]
def _on_select_cs(self, event: wx.MenuEvent) -> None:
if self.select_cs.IsChecked():
self.action = 'Select nearest profile'
else:
self.action = None
[docs]
def _on_plot_cs(self, event: wx.MenuEvent) -> None:
if self.plot_cs.IsChecked():
self.action = 'Plot cross section'
else:
self.action = None
[docs]
def _on_sort_along(self, event: wx.MenuEvent) -> None:
if self.active_cs is not None and self.active_vector is not None:
self.active_cs.sort_along(self.active_vector.linestring, self.active_vector.myname, False)
else:
msg = ''
if self.active_cs is None:
msg += _('Please select the active cross sections\n')
if self.active_vector is None:
msg += _('Please select the active supprt vector.\n\nFirst vertex is upstream, last vertex is downstream.\n')
self._dialogs.show_message(msg, _('Sort along'), parent=self)
[docs]
def _on_locminmax(self, event: wx.MenuEvent) -> None:
if not self.locminmax.IsChecked():
self.update_absolute_minmax = True
# ---- Colormap ----------------------------------------------------
[docs]
def _on_colormap_unique(self, event):
self.uniquecolormap()
[docs]
def _on_colormap_from_file(self, event):
self.uniquecolormap(True)
[docs]
def _on_colormap_linear(self, event):
self.uniforminparts_all(False)
# ---- Utility -----------------------------------------------------
[docs]
def _autoscale_if_needed(self):
"""Trigger Autoscale when the viewer has exactly 2 total objects (1 new)."""
total = (len(self.myarrays) + len(self.myvectors) + len(self.myclouds) +
len(self.mytri) + len(self.myres2D) + len(self.mytiles) +
len(self.myimagestiles) + len(self.mypartsystems) +
len(self._dike.mydikes) + len(self._drowning.mydrownings) +
len(self.myinjectors))
if total == 2:
self.Autoscale()
[docs]
def _on_save_all(self, event):
for obj in self.iterator_over_objects(draw_type.ARRAYS):
obj: WolfArray
if obj.filename == '':
filterArray = "bin (*.bin)|*.bin|Geotif (*.tif)|*.tif|Numpy (*.npy)|*.npy|all (*.*)|*.*"
chosen = self._dialogs.ask_file_save("Choose file", wildcard=filterArray, parent=self)
if chosen is None:
continue
obj.filename = chosen
obj.write_all()
for obj in self.iterator_over_objects(draw_type.VECTORS):
obj: Zones
obj.saveas()
[docs]
def _on_save_all_as(self, event):
for obj in self.iterator_over_objects(draw_type.ARRAYS):
obj: WolfArray
filterArray = "bin (*.bin)|*.bin|Geotif (*.tif)|*.tif|Numpy (*.npy)|*.npy|all (*.*)|*.*"
chosen = self._dialogs.ask_file_save("Choose file name for Array : " + obj.idx,
wildcard=filterArray, parent=self)
if chosen is not None:
obj.filename = chosen
obj.write_all()
for obj in self.iterator_over_objects(draw_type.VECTORS):
obj: Zones
if obj.idx == 'grid':
pass
else:
filterArray = "vec (*.vec)|*.vec|vecz (*.vecz)|*.vecz|Shapefile (*.shp)|*.shp|all (*.*)|*.*"
chosen = self._dialogs.ask_file_save("Choose file name for Vector :" + obj.idx,
wildcard=filterArray, parent=self)
if chosen is not None:
obj.saveas(chosen)
[docs]
def _on_recursive_scan(self, event):
from os.path import join, exists
from os import scandir
def addscandir(mydir):
for entry in scandir(mydir):
if entry.is_dir():
addscandir(entry)
elif entry.is_file():
if entry.name.endswith('.vec') or entry.name.endswith('.vecz'):
if self._dialogs.ask_confirmation(
_(entry.name + ' found in ' + mydir + '\n\n Is it a "cross sections" file?'),
default='no', parent=self):
self.add_object(which='vector', filename=join(mydir, entry.name),
ToCheck=True, id=join(mydir, entry.name))
else:
self.add_object(which='cross_sections', filename=join(mydir, entry.name),
ToCheck=True, id=join(mydir, entry.name))
elif entry.name.endswith(('.bin', '.tif', '.npy')):
self.add_object(which='array', filename=join(mydir, entry.name),
ToCheck=True, id=join(mydir, entry.name))
mydir = self._dialogs.ask_directory(_("Choose directory to scan"), parent=self)
if mydir is None:
return
if exists(mydir):
addscandir(mydir)
# ---- Dike (conditional on WOLFPYDIKE_AVAILABLE) ------------------
[docs]
def _on_create_dike(self, event):
self.new_dike(_('Create dike...'))
[docs]
def _on_add_dike(self, event):
self.new_dike(_('Add dike...'))
# ---- Precomputed DEM/DTM ----------------------------------------
[docs]
def _on_precomputed_dem(self, event):
dlg = Precomputed_DEM_DTM_Dialog(self, _('Precomputed DEM'), self.default_dem, self)
dlg.ShowModal()
[docs]
def _on_precomputed_dtm(self, event):
dlg = Precomputed_DEM_DTM_Dialog(self, _('Precomputed DTM'), self.default_dtm, self)
dlg.ShowModal()
# ---- Exit --------------------------------------------------------
[docs]
def _on_exit(self, event):
if self._dialogs.ask_yes_no(_('Do you really want to quit?'), style=DialogStyles.YES_NO_DEFAULT_NO):
wx.Exit()
# ---- Cross sections (lazy items created in set_interp_cs) -------
[docs]
def _on_viewer_interpcs(self, event):
if self.myinterp is not None:
self.myinterp.viewer_interpolator()
[docs]
def _on_interpcs(self, event):
if self.myinterp is not None:
self.interpolate_cs()
[docs]
def pop_boundary_manager(self, which:BcManager):
""" Pop a boundary condition manager after Destroying """
idx = self.mybc.index(which)
if self.active_bc is which:
self.active_bc = None
self.mybc.pop(idx)
self.Refresh()
[docs]
def get_boundary_manager(self, which:WolfArray):
""" Get a boundary manager """
for curbc in self.mybc:
if curbc.linked_array is which:
return curbc
return None
[docs]
def uniquecolormap(self, loadfromfile = False):
""" Compute unique colormap from all (arrays, 2D results) and apply it to all """
workingarray=[]
nbnotnull=0
newpal = wolfpalette(self)
if loadfromfile :
newpal.readfile()
if not newpal.is_valid():
logging.warning(_('Palette not valid !'))
return
else:
nb = len(self.myarrays) + len(self.myres2D)
pgbar = self._dialogs.create_progress(
_('Compute unique colormap'),
_('Compute unique colormap from all arrays'),
nb,
self,
style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
)
curarray:WolfArray
curres2d:Wolfresults_2D
step = 0
for curarray in self.myarrays:
if curarray.plotted:
workingarray.append(curarray.get_working_array())
nbnotnull+=curarray.nbnotnull
step += 1
pgbar.update(step, _('Compute unique colormap from array : ') + curarray.idx)
for curres2d in self.myres2D:
if curres2d.plotted:
workingarray.append(curres2d.get_working_array())
nbnotnull+=curres2d.nbnotnull
step += 1
pgbar.update(step, _('Compute unique colormap from 2D result : ') + curres2d.idx)
pgbar.close()
workingarray = np.concatenate(workingarray)
newpal.default16()
newpal.isopop(workingarray, nbnotnull)
nb = len(self.myarrays) + len(self.myres2D)
pgbar = self._dialogs.create_progress(
_('Applying colormap'),
_('Applying colormap to all arrays'),
nb,
self,
style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE,
)
step = 0
for curarray in self.myarrays:
if curarray.plotted:
curarray.mypal.automatic = False
curarray.myops.palauto.SetValue(0)
curarray.mypal.values = newpal.values.copy()
curarray.mypal.colors = newpal.colors.copy()
curarray.mypal.fill_segmentdata()
curarray.reset_plot()
step += 1
pgbar.update(step, _('Applying colormap to array : ') + curarray.idx)
for curres2d in self.myres2D:
if curres2d.plotted:
curres2d.mypal.automatic = False
curres2d.mypal.nb = newpal.nb
curres2d.mypal.values = newpal.values.copy()
curres2d.mypal.colors = newpal.colors.copy()
curres2d.mypal.fill_segmentdata()
curres2d.reset_plot()
step += 1
pgbar.update(step, _('Applying colormap to 2D result : ') + curres2d.idx)
pgbar.close()
[docs]
def loadnap_and_apply(self):
if not self._dialogs.ask_yes_no(_('Load mask for all?'), style=DialogStyles.YES_NO_DEFAULT_YES, parent=self):
return
with self._dialogs.show_busy(_('Loading masks')):
curarray:WolfArray
for curarray in self.myarrays:
if curarray.plotted:
curarray.loadnap_and_apply()
[docs]
def filter_inundation(self):
_bound = self._dialogs.ask_float(_('Upper bound \n\n All values strictly lower than the bound will not be extracted !'), default='.0005')
if _bound is None:
return
bound = _bound
logging.info(_('Filtering results'))
curarray:WolfArray
for curarray in self.myarrays:
if curarray.plotted:
curarray.filter_inundation(epsilon = bound)
curarray.filter_independent_zones(n_largest = 1)
curarray:Wolfresults_2D
for curarray in self.myres2D:
if curarray.plotted:
curarray.filter_inundation(eps = bound)
curarray.filter_independent_zones(n_largest = 1)
logging.info(_('Filtering done !'))
[docs]
def export_results_as(self, which:Literal['geotiff','shape','numpy'] = None, multiband:bool = None):
"""
Export des résultats WOLF2D vers différents formats.
Au moins un résultat doit être chargé pour pouvoir être exporté.
"""
outdir = self._dialogs.ask_directory(_('Choose output directory'), style=wx.DD_DIR_MUST_EXIST, parent=self)
if outdir is None:
logging.warning(_('Abort!'))
return
if which not in ['geotiff','shape','numpy']:
selected = self._dialogs.ask_single_choice(_('Choose output format'), _('Format'), ['Geotiff','Shape file','Numpy array'], parent=self)
if selected is None:
logging.warning(_('Abort!'))
return
if selected == 'Geotiff':
which = 'geotiff'
elif selected == 'Shape file':
which = 'shape'
else:
which = 'numpy'
if which == 'geotiff':
if multiband is None:
selected = self._dialogs.ask_single_choice(
_('Choose output format'),
_('Format'),
['Multiband (single file)', 'Single band (multiple files)'],
parent=self,
)
if selected is None:
logging.warning(_('Abort!'))
return
if selected == 'Single band (multiple files)':
multiband = False
else:
multiband = True
logging.info(_('Exporting results -- Be patient !'))
loaded_res = self.get_list_keys(drawing_type= draw_type.RES2D, checked_state=None)
idx = self._dialogs.ask_multi_choice(
_('Choose results to export'),
_('Results'),
choices=loaded_res,
preselected=[idx for idx, res in enumerate(loaded_res) if self.get_obj_from_id(res, drawing_type=draw_type.RES2D).plotted],
parent=self,
)
if idx is None:
logging.warning(_('Abort!'))
return
sel_res = [self.get_obj_from_id(loaded_res[cursel], drawing_type=draw_type.RES2D) for cursel in idx]
if len(idx) == 0:
logging.warning(_('No results selected for export'))
return
fields = [(views_2D.TOPOGRAPHY, True),
(views_2D.WATERDEPTH, True),
(views_2D.QX, True),
(views_2D.QY, True),
(views_2D.UNORM, True),
(views_2D.FROUDE, True),
(views_2D.HEAD, True),
(views_2D.CRITICAL_DIAMETER_SHIELDS, False),
(views_2D.CRITICAL_DIAMETER_IZBACH, False),
(views_2D.QNORM, False),
(views_2D.WATERLEVEL, False),
(views_2D.CRITICAL_DIAMETER_SUSPENSION_50, False),
(views_2D.CRITICAL_DIAMETER_SUSPENSION_100, False),]
idx = self._dialogs.ask_multi_choice(
_('Choose fields to export'),
_('Fields'),
choices=[str(field[0]) for field in fields],
preselected=[idx for idx, field in enumerate(fields) if field[1]],
parent=self,
)
if idx is None:
logging.warning(_('Abort!'))
return
sel_fields = idx
if len(sel_fields) == 0:
logging.warning(_('No fields selected for export'))
return
# Get the views_2D values associated with the selected field names
fields = [fields[cursel][0] for cursel in sel_fields]
for cur_res in tqdm(sel_res):
cur_res:Wolfresults_2D
cur_res.export_as(outdir, fields, which, multiband)
logging.info(_('Export done -- Thanks for your patience !'))
[docs]
def export_shape(self, outdir:str= '', fn:str = '', myarrays:list[WolfArray]= [], descr:list[str]= [], mask:WolfArray=None):
""" Export multiple arrays to shapefile
:param outdir: output directory
:param fn: filename -- .shp will be added if not present
:param myarrays: list of Wolfarrays to export
:param descr: list of descriptions
:param mask: mask array -- export only where mask > 0
"""
if len(myarrays)==0:
logging.warning(_('No arrays provided for shapefile export'))
return
if mask is None:
logging.warning(_('No mask provided for shapefile export'))
return
from osgeo import gdal, osr, gdalconst,ogr
# create the spatial reference system, Lambert72
srs = osr.SpatialReference()
srs.ImportFromEPSG(self.epsg)
# create the data source
driver: ogr.Driver
driver = ogr.GetDriverByName("ESRI Shapefile")
# create the data source
filename = join(outdir,fn)
if not filename.endswith('.shp'):
filename+='.shp'
ds = driver.CreateDataSource(filename)
# create one layer
layer = ds.CreateLayer("results", srs, ogr.wkbPolygon)
# Add ID fields
idFields=[]
for curlab in descr:
idFields.append(ogr.FieldDefn(curlab, ogr.OFTReal))
layer.CreateField(idFields[-1])
# Create the feature and set values
featureDefn = layer.GetLayerDefn()
feature = ogr.Feature(featureDefn)
usednodes = np.argwhere(mask.array>0.)
for i,j in usednodes:
x,y = mask.get_xy_from_ij(i,j)
# Creating a line geometry
ring = ogr.Geometry(ogr.wkbLinearRing)
ring.AddPoint(x-mask.dx/2,y-mask.dy/2)
ring.AddPoint(x+mask.dx/2,y-mask.dy/2)
ring.AddPoint(x+mask.dx/2,y+mask.dy/2)
ring.AddPoint(x-mask.dx/2,y+mask.dy/2)
ring.AddPoint(x-mask.dx/2,y-mask.dy/2)
# Create polygon
poly = ogr.Geometry(ogr.wkbPolygon)
poly.AddGeometry(ring)
feature.SetGeometry(poly)
for arr, id in zip(myarrays,descr):
feature.SetField(id, float(arr.array[i,j]))
layer.CreateFeature(feature)
feature = None
# Save and close DataSource
ds = None
[docs]
def export_geotif(self, outdir:str= '', fn:str = '', myarrays:list[WolfArray]= [], descr:list[str]= [], multiband:bool= True):
""" Export multiple arrays to geotiff
:param outdir: output directory
:param fn: filename -- .tif will be added if not present
:param myarrays: list of Wolfarrays to export
:param descr: list of descriptions -- Bands names
"""
if len(myarrays)==0:
logging.warning(_('No arrays provided for geotiff export'))
return
from osgeo import gdal, osr, gdalconst
srs = osr.SpatialReference()
srs.ImportFromEPSG(self.epsg)
driver: gdal.Driver
out_ds: gdal.Dataset
band: gdal.Band
driver = gdal.GetDriverByName("GTiff")
if multiband:
filename = join(outdir,fn)
if not filename.endswith('.tif'):
filename+='.tif'
arr = myarrays[0]
out_ds = driver.Create(filename, arr.shape[0], arr.shape[1], len(myarrays), arr.dtype_gdal, options=['COMPRESS=LZW'])
out_ds.SetProjection(srs.ExportToWkt())
out_ds.SetGeoTransform([myarrays[0].origx+myarrays[0].translx,
myarrays[0].dx,
0.,
myarrays[0].origy+myarrays[0].transly,
0.,
myarrays[0].dy])
k=1
for arr, name in zip(myarrays,descr):
band = out_ds.GetRasterBand(k)
band.SetNoDataValue(0.)
band.SetDescription(name)
band.WriteArray(arr.array.transpose())
band.FlushCache()
band.ComputeStatistics(True)
k+=1
out_ds = None
else:
for arr, name in zip(myarrays,descr):
if filename.endswith('.tif'):
filename = filename[:-4]
filename = join(outdir,fn+'_'+name)
filename += '.tif'
out_ds = driver.Create(filename, arr.shape[0], arr.shape[1], 1, arr.dtype_gdal, options=['COMPRESS=LZW'])
out_ds.SetProjection(srs.ExportToWkt())
out_ds.SetGeoTransform([myarrays[0].origx+myarrays[0].translx,
myarrays[0].dx,
0.,
myarrays[0].origy+myarrays[0].transly,
0.,
myarrays[0].dy])
band = out_ds.GetRasterBand(1)
band.SetNoDataValue(0.)
band.SetDescription(name)
band.WriteArray(arr.array.transpose())
band.FlushCache()
band.ComputeStatistics(True)
out_ds = None
[docs]
def get_linked_arrays(self, linked:bool = True) -> dict:
""" Get all arrays in the viewer and linked viewers """
linkedarrays = {}
if self.linked and linked:
all_dicts = [curviewer.get_linked_arrays(linked = False) for curviewer in self.linkedList]
for curdict in all_dicts:
linkedarrays.update(curdict)
else:
for locarray in self.iterator_over_objects(draw_type.ARRAYS):
linkedarrays[locarray.idx] = locarray
for locarray in self.iterator_over_objects(draw_type.RES2D):
linkedarrays[locarray.idx] = locarray
return linkedarrays
[docs]
def save_linked_canvas(self, fn:str, mpl:bool= True, ds:float= 0., add_title:bool= True) -> tuple[(str, float), str]:
""" Save canvas of all linked viewers
:param fn: filename without extension -- '.png' will be added
:param mpl: save as matplotlib image
:param ds: Ticks size for matplotlib image
:return: list of tuple ((filename, ds), viewer_name)
"""
fn = str(fn)
ret = []
if self.linked:
for idx, curel in enumerate(self.linkedList):
ret.append((curel.save_canvasogl(fn + '_' + str(idx) + '.png', mpl, ds, add_title= add_title), self.viewer_name))
return ret
[docs]
def save_arrays_indep(self, fn:str, mpl:bool= True, ds:float= 0., add_title:bool= True) -> tuple[(str, float), str]:
""" Save each array in a separate file
:param fn: filename without extension -- '.png' will be added
:param mpl: save as matplotlib image
:param ds: Ticks size for matplotlib image
:return: list of tuple ((filename, ds), viewer_name)
"""
# Get all checked arrays
checked_arrays = self.get_list_keys(drawing_type= draw_type.ARRAYS, checked_state= True)
checked_results = self.get_list_keys(drawing_type= draw_type.RES2D, checked_state= True)
old_active = self.active_array
old_res2d = self.active_res2d
if len(checked_arrays) + len(checked_results) == 0:
logging.warning(_('No arrays checked for export'))
return []
def uncheck_all():
# uncheck arrays
for curarray in checked_arrays:
self.uncheck_id(curarray, unload= False, forceresetOGL= False)
for curres in checked_results:
self.uncheck_id(curres, unload= False, forceresetOGL= False)
fn = str(fn)
ret = []
for idx, curel in enumerate(checked_arrays):
uncheck_all()
self.check_id(curel)
self.active_array = self.get_obj_from_id(curel, drawing_type= draw_type.ARRAYS)
ret.append((self.save_canvasogl(fn + '_' + str(idx) + '.png', mpl, ds, add_title= add_title, arrayid_as_title=True), curel))
for idx, curel in enumerate(checked_results):
uncheck_all()
self.check_id(curel)
self.active_res2d = self.get_obj_from_id(curel, drawing_type= draw_type.RES2D)
ret.append((self.save_canvasogl(fn + '_' + str(idx + len(checked_arrays)) + '.png', mpl, ds, add_title= add_title, resid_as_title=True), curel))
self.active_array = old_active
self.active_res2d = old_res2d
for curarray in checked_arrays:
self.check_id(curarray)
for curres in checked_results:
self.check_id(curres)
self.Refresh()
return ret
[docs]
def assembly_images(self, all_images, mode:Literal['horizontal', 'vertical', 'square']= 'square'):
""" Assembly images
Every image has the same size (width, height)
:param all_images: list of tuple (filename, viewer_name)
:param mode: 'horizontal', 'vertical', 'square'
"""
assert mode in ['horizontal', 'vertical', 'square', 0, 1, 2], 'Mode not recognized'
from PIL import Image
images = [Image.open(fn) for (fn, ds), viewername in all_images]
if len(images) in [1,2] and (mode == 'square' or mode == 2):
mode = 'horizontal'
widths, heights = zip(*(i.size for i in images))
if mode == 'horizontal' or mode==0:
total_width = sum(widths)
max_height = max(heights)
new_im = Image.new('RGB', (total_width, max_height), color=(255,255,255))
x_offset = 0
for im in images:
new_im.paste(im, (x_offset,0))
x_offset += im.size[0]
new_im.save(all_images[0][0][0][:-4] + '_assembly.png')
elif mode == 'vertical' or mode==1:
total_height = sum(heights)
max_width = max(widths)
new_im = Image.new('RGB', (max_width, total_height), color=(255,255,255))
y_offset = 0
for im in images:
new_im.paste(im, (0, y_offset))
y_offset += im.size[1]
new_im.save(all_images[0][0][0][:-4] + '_assembly.png')
elif mode == 'square' or mode==2:
max_width = max(widths)
max_height = max(heights)
nb_hor = int(np.ceil(np.sqrt(len(images))))
new_im = Image.new('RGB', (max_width*nb_hor, max_height*nb_hor), color=(255,255,255))
x_offset = 0
y_offset = 0
for idx, im in enumerate(images):
new_im.paste(im, (x_offset, y_offset))
x_offset += im.size[0]
if (idx+1) % nb_hor == 0:
y_offset += im.size[1]
x_offset = 0
new_im.save(all_images[0][0][0][:-4] + '_assembly.png')
return new_im
[docs]
def thread_update_blender(self):
print("Update blender")
if self.SetCurrentContext():
self.update_blender_sculpting()
t = threading.Timer(10.0, self.thread_update_blender)
t.start()
# ----------------------------------------------------------------
# add_object — file-dialog wildcard constants (class level)
# ----------------------------------------------------------------
[docs]
_ADD_FILTER_ARRAY = "All supported formats|*.bin;*.tif;*.tiff;*.top;*.flt;*.npy;*.npz;*.vrt|bin (*.bin)|*.bin|Elevation WOLF2D (*.top)|*.top|Geotif (*.tif)|*.tif|Float ESRI (*.flt)|*.flt|Numpy (*.npy)|*.npy|Numpy named arrays(*.npz)|*.npz|all (*.*)|*.*"
[docs]
_ADD_FILTER_JSON = "json (*.json)|*.json|all (*.*)|*.*"
[docs]
_ADD_FILTER_ALL = "all (*.*)|*.*"
[docs]
_ADD_FILTER_VECTOR = "All supported formats|*.vec;*.vecz;*.dxf;*.shp|vec (*.vec)|*.vec|vecz (*.vecz)|*.vecz|dxf (*.dxf)|*.dxf|shp (*.shp)|*.shp|all (*.*)|*.*"
[docs]
_ADD_FILTER_CLOUD = "All supported formats|*.xyz;*.laz;*.las;*.json|xyz (*.xyz)|*.xyz|laz (*.laz)|*.laz|las (*.las)|*.las|dxf (*.dxf)|*.dxf|JSON (*.json)|*.json|text (*.txt)|*.txt|shp (*.shp)|*.shp|all (*.*)|*.*"
[docs]
_ADD_FILTER_LAZ = "laz (*.laz)|*.laz|las (*.las)|*.las|Numpy (*.npz)|*.npz|all (*.*)|*.*"
[docs]
_ADD_FILTER_TRI = "tri (*.tri)|*.tri|text (*.txt)|*.txt|dxf (*.dxf)|*.dxf|gltf (*.gltf)|*.gltf|gltf binary (*.glb)|*.glb|*.*'all (*.*)|*.*"
[docs]
_ADD_FILTER_CS = "vecz WOLF (*.vecz)|*.vecz|txt 2022 (*.txt)|*.txt|WOLF (*.sxy)|*.sxy|text 2000 (*.txt)|*.txt|xlsx 2025 (*.xlsx)|*.xlsx|xlsx ISL(*.xlsx)|*.xlsx|all (*.*)|*.*"
[docs]
_ADD_FILTER_IMAGE = "Geotif (*.tif)|*.tif|all (*.*)|*.*"
# Map: which → (dialog_class, title, filter_attr_name_or_None)
# DirDialog entries have None for the filter since wx.DirDialog has no wildcard parameter.
[docs]
_ADD_DIALOG_SPECS: dict = {
'array': ('FileDialog', "Choose file", '_ADD_FILTER_ARRAY'),
'array_crop': ('FileDialog', "Choose file", '_ADD_FILTER_ARRAY'),
'imagestiles': ('DirDialog', "Choose directory containing images", None),
'particlesystem': ('FileDialog', "Choose file", '_ADD_FILTER_JSON'),
'array_lidar_first': ('DirDialog', "Choose directory containing Lidar data", None),
'array_lidar_second': ('DirDialog', "Choose directory containing Lidar data", None),
'array_xyz': ('DirDialog', "Choose directory containing XYZ files", None),
'array_tiles': ('DirDialog', "Choose directory containing GPU results", None),
'bridges': ('DirDialog', "Choose directory containing bridges", None),
'weirs': ('DirDialog', "Choose directory containing weirs", None),
'vector': ('FileDialog', "Choose file", '_ADD_FILTER_VECTOR'),
'tiles': ('FileDialog', "Choose file", '_ADD_FILTER_VECTOR'),
'tilescomp': ('FileDialog', "Choose file", '_ADD_FILTER_VECTOR'),
'cloud': ('FileDialog', "Choose file", '_ADD_FILTER_CLOUD'),
'clouds': ('FileDialog', "Choose file", '_ADD_FILTER_CLOUD'),
'laz': ('FileDialog', "Choose file", '_ADD_FILTER_LAZ'),
'triangulation': ('FileDialog', "Choose file", '_ADD_FILTER_TRI'),
'cross_sections': ('FileDialog', "Choose file", '_ADD_FILTER_CS'),
'other': ('FileDialog', "Choose file", '_ADD_FILTER_ALL'),
'views': ('FileDialog', "Choose file", '_ADD_FILTER_ALL'),
'res2d': ('FileDialog', "Choose file", '_ADD_FILTER_ALL'),
'res2d_gpu': ('DirDialog', "Choose directory containing WolfGPU results", None),
'drowning': ('DirDialog', "Choose directory containing the drowning", None),
'dike': ('DirDialog', "Choose directory", None),
'picture_collection': ('DirDialog', "Choose directory containing pictures", None),
'wmsback': ('FileDialog', "Choose file", '_ADD_FILTER_IMAGE'),
'wmsfore': ('FileDialog', "Choose file", '_ADD_FILTER_IMAGE'),
# 'injector' intentionally absent — always requires a pre-built newobj
}
# Map: which → handler method name
[docs]
_ADD_HANDLERS: dict = {
'array': '_add_array',
'array_crop': '_add_array',
'array_xyz': '_add_array_xyz',
'array_tiles': '_add_array_tiles',
'array_lidar_first': '_add_array_lidar',
'array_lidar_second': '_add_array_lidar',
'picture_collection': '_add_picture_collection',
'imagestiles': '_add_imagestiles',
'bridges': '_add_bridges',
'weirs': '_add_weirs',
'tiles': '_add_tiles',
'tilescomp': '_add_tiles',
'res2d': '_add_res2d',
'res2d_gpu': '_add_res2d_gpu',
'vector': '_add_vector',
'cross_sections': '_add_cross_sections',
'laz': '_add_laz',
'cloud': '_add_cloud',
'clouds': '_add_clouds',
'triangulation': '_add_triangulation',
'other': '_add_other',
'views': '_add_views',
'wmsback': '_add_wmsback',
'wmsfore': '_add_wmsfore',
'particlesystem': '_add_particlesystem',
'drowning': '_add_drowning',
'dike': '_add_dike',
'injector': '_add_injector',
}
[docs]
def add_object(self,
which: Literal['array', 'array_lidar_first', 'array_lidar_second', 'array_xyz', 'array_tiles',
'bridges', 'weirs', 'vector', 'tiles', 'tilescomp',
'cloud', 'laz', 'clouds', 'triangulation', 'cross_sections',
'other', 'views', 'res2d', 'res2d_gpu', 'particlesystem',
'wmsback', 'wmsfore', 'drowning', 'imagestiles',
'dike', 'injector', 'picture_collection'] = 'array',
filename='',
newobj=None,
ToCheck=True,
id=''):
"""Add object to current Frame/Drawing area."""
which = which.lower()
if which not in self._ADD_HANDLERS:
logging.error(f'Unknown object type for add_object: {which!r}')
return -1
# Phase 1 — file dialog
curfilter = 0
if filename == '' and newobj is None:
result = self._prompt_file_for_type(which)
if result is None:
return -1
filename, curfilter = result
# Phase 2 — existence check
if filename != '' and not os.path.exists(filename):
logging.warning("Warning : the following file is not present here : " + filename)
return -1
# Phase 3 — pre-collect IDs before adding the new object
all_ids = self.get_list_keys(None, checked_state=None)
# Phase 4 — type dispatch
# Handler contract:
# (curtree, newobj, id) → proceed to ID resolution + tree insertion
# None → handler managed everything; return 0
# -1 → error / user cancel; return -1
handler_result = getattr(self, self._ADD_HANDLERS[which])(
which, filename, newobj, curfilter, ToCheck, id)
if handler_result is None:
return 0
if handler_result == -1:
return -1
curtree, newobj, id = handler_result
if newobj is None:
return -1
# Phase 5 — ID resolution
id = self._resolve_object_id(id, filename, all_ids)
# Phase 6 — tree registration + post-init side-effects
self._register_object_in_tree(newobj, curtree, id, ToCheck, filename)
return 0
[docs]
def _prompt_file_for_type(self, which: str):
"""Show a file or directory dialog for *which*. Returns (filename, curfilter) or None on cancel."""
spec = self._ADD_DIALOG_SPECS.get(which)
if spec is None:
return ('', 0)
dialog_kind, title, filter_attr = spec
if dialog_kind == 'FileDialog':
wildcard = getattr(self.__class__, filter_attr)
result = self._dialogs.ask_file_open_with_filter(title, wildcard=wildcard)
if result is None:
return None
return result
filename = self._dialogs.ask_directory(title)
if filename is None:
return None
return filename, 0
[docs]
def _resolve_object_id(self, id: str, filename: str, all_ids: list) -> str:
"""Prompt for ID when blank; ensure the result is unique across all loaded objects."""
if id == '':
base_default = (Path(filename).with_suffix('')).name if filename != '' else ''
endid = 1
while id == '' or id.lower() in all_ids:
default = base_default if endid == 1 and base_default != '' else str(endid).zfill(3)
selected = self._dialogs.ask_text(
'ID ? (case insensitive)',
'Choose an identifier',
default=default,
)
if selected is None or selected == '':
id = str(endid).zfill(3)
else:
id = selected
endid += 1
if id.lower() in all_ids:
endid = 1
while (id + str(endid).zfill(3)).lower() in all_ids:
endid += 1
id = id + str(endid).zfill(3)
return id
[docs]
def _register_object_in_tree(self, newobj, curtree, id: str, ToCheck: bool, filename: str) -> None:
"""Assign idx, insert into treelist, and handle post-registration side-effects."""
newobj.idx = id.lower()
if curtree is not None:
myitem = self.treelist.AppendItem(curtree, id, data=newobj)
if ToCheck:
self.treelist.CheckItem(myitem)
self.treelist.CheckItem(self.treelist.GetItemParent(myitem))
newobj.check_plot()
else:
logging.info(f'No tree item for this object {newobj.idx}')
if filename != '':
newobj._filename_vector = Path(filename).name.lower() # FIXME useful ??
newobj.checked = ToCheck
if isinstance(newobj, crosssections):
self.add_object('cloud', newobj=newobj.cloud, id=newobj.idx + '_intersect', ToCheck=False)
self.add_object('cloud', newobj=newobj.cloud_all, id=newobj.idx + '_all', ToCheck=False)
elif type(newobj) == WolfArray:
if self.active_cs is None:
self.active_cs = self.get_cross_sections()
# ----------------------------------------------------------------
# add_object type handlers
# Signature: (self, which, filename, newobj, curfilter, ToCheck, id)
# Returns: (curtree, newobj, id) | None | -1
# ----------------------------------------------------------------
[docs]
def _add_array(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
if str(filename).endswith('.npz'):
wait = wx.BusyCursor()
logging.info(_('Start of importing arrays from npz file'))
with np.load(filename) as data:
if 'header' in data.keys():
header = data['header']
if len(header) == 6:
logging.info(_('Header found in npz file'))
origx, origy, dx, dy, nbx, nby = header
logging.info(_('Origin X : ') + str(origx))
logging.info(_('Origin Y : ') + str(origy))
logging.info(_('dx : ') + str(dx))
logging.info(_('dy : ') + str(dy))
logging.info(_('nbx : ') + str(nbx))
logging.info(_('nby : ') + str(nby))
nbx, nby = int(nbx), int(nby)
else:
logging.warning(_('Header found in npz file but not complete -- Only {} values found - Must be 6').format(len(header)))
for key, curarray in data.items():
if isinstance(curarray, np.ndarray):
if curarray.shape == (nby, nbx):
logging.info("Importing array : " + key)
curhead = header_wolf()
curhead.origx, curhead.origy, curhead.dx, curhead.dy, curhead.nbx, curhead.nby = origx, origy, dx, dy, nbx, nby
newobj = WolfArray(srcheader=curhead, idx=key)
newobj.set_array_from_numpy(curarray)
self.add_object('array', newobj=newobj, id=key)
else:
origx, origy, dx, dy, nbx, nby = 0., 0., 1, 1., 1, 1
for key, curarray in data.items():
if isinstance(curarray, np.ndarray):
logging.info(_('No header found in npz file - Using default values for header'))
logging.info("Importing array : " + key)
curhead = header_wolf()
curhead.origx, curhead.origy, curhead.dx, curhead.dy, curhead.nbx, curhead.nby = 0., 0., 1., 1., curarray.shape[0], curarray.shape[1]
newobj = WolfArray(srcheader=curhead, idx=key)
newobj.set_array_from_numpy(curarray)
self.add_object('array', newobj=newobj, id=key)
logging.info(_('End of importing arrays from npz file'))
del wait
return None # all sub-arrays handled recursively
else:
testobj = WolfArray()
testobj.filename = filename
testobj.read_txt_header()
if testobj.wolftype in WOLF_ARRAY_MB:
newobj = WolfArrayMB(filename, mapviewer=self)
else:
if which == 'array_crop':
newobj = WolfArray(filename, mapviewer=self, crop='newcrop')
else:
newobj = WolfArray(filename, mapviewer=self)
if newobj is not None and (newobj.dx == 0. or newobj.dy == 0.):
dlg_pos = CropDialog(None)
dlg_pos.SetTitle(_('Choose informations'))
dlg_pos.ox.SetValue('99999.')
dlg_pos.oy.SetValue('99999.')
dlg_pos.ex.Hide()
dlg_pos.ey.Hide()
badvalues = True
while badvalues:
badvalues = False
ret = dlg_pos.ShowModal()
if ret == wx.ID_CANCEL:
dlg_pos.Destroy()
return -1
else:
cropini = [[float(dlg_pos.ox.Value), float(dlg_pos.ex.Value)],
[float(dlg_pos.oy.Value), float(dlg_pos.ey.Value)]]
tmpdx = float(dlg_pos.dx.Value)
tmpdy = float(dlg_pos.dy.Value)
if tmpdx == 0. or tmpdy == 0.:
badvalues = True
dlg_pos.Destroy()
newobj.dx = tmpdx
newobj.dy = tmpdy
if cropini[0][0] != 99999. and cropini[1][0] != 99999.:
newobj.origx = cropini[0][0]
newobj.origy = cropini[1][0]
if newobj.epsg is None:
logging.info(_('Array EPSG not defined -- Setting it to viewer EPSG ({})').format(self.epsg))
newobj.epsg = self.epsg
else:
if newobj.epsg != self.epsg:
logging.error(_('Array EPSG ({}) different from viewer EPSG ({}) -- Reproject the array before adding it to the viewer').format(newobj.epsg, self.epsg))
if self._show_dialog_wx:
self._dialogs.show_message(_('Array EPSG ({}) different from viewer EPSG ({}) -- Reproject the array before adding it to the viewer').format(newobj.epsg, self.epsg), style=DialogStyles.OK | DialogStyles.ICON_ERROR, parent=self)
return -1
newobj.updatepalette(0)
self.myarrays.append(newobj)
newobj.change_gui(self)
self.active_array = newobj
self._refresh_hillshade_panel_for_active()
self._set_active_bc()
return self.myitemsarray, newobj, id
[docs]
def _add_picture_collection(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
newobj = PictureCollection(parent=self, mapviewer=self)
newobj.load_from_directory_with_shapefile(filename)
self.mypicturecollections.append(newobj)
self.active_picturecollection = newobj
self.menu_pictcollection()
return self.myitemspictcollection, newobj, id
[docs]
def _add_array_tiles(self, which, filename, newobj, curfilter, ToCheck, id):
res = wolfres2DGPU(filename, plotted=False)
tilesmap = res._result_store._tile_packer.tile_indirection_map()
if tilesmap is None:
logging.warning(_('No tile map found in the simulation'))
return None
header = header_wolf()
res_header = res[0].get_header()
header.origx = res_header.origx
header.origy = res_header.origy
header.dx = res_header.dx * 16.
header.dy = res_header.dy * 16.
header.nbx = tilesmap.shape[1]
header.nby = tilesmap.shape[0]
newobj_i = WolfArray(mapviewer=self, srcheader=header, idx='tils_i')
newobj_j = WolfArray(mapviewer=self, srcheader=header, idx='tils_j')
newobj_i.array = np.ma.asarray(tilesmap[:, :, 0].T.astype(np.float32))
newobj_j.array = np.ma.asarray(tilesmap[:, :, 1].T.astype(np.float32))
newobj_i.mask_data(0.)
newobj_j.mask_data(0.)
self.add_object('array', newobj=newobj_i, id=newobj_i.idx)
self.add_object('array', newobj=newobj_j, id=newobj_j.idx)
return None
[docs]
def _add_imagestiles(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
newobj = ImagesTiles('', parent=self, mapviewer=self)
newobj.scan_dir(Path(filename))
self.myimagestiles.append(newobj)
self.active_imagestiles = newobj
self.menu_imagestiles()
return self.myitemsvector, newobj, id
[docs]
def _add_bridges(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
with self._dialogs.show_busy(_('Importing files')):
newobj = Bridges(filename, mapviewer=self)
self.myvectors.append(newobj)
self.active_bridges = newobj
self.menu_bridges()
return self.myitemsvector, newobj, id
[docs]
def _add_weirs(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
with self._dialogs.show_busy(_('Importing files')):
newobj = Weirs(filename, mapviewer=self)
self.myvectors.append(newobj)
self.active_weirs = newobj
self.menu_weirs()
return self.myitemsvector, newobj, id
[docs]
def _add_tiles(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
dirname = self._dialogs.ask_directory("Choose directory containing data", parent=self)
if dirname is None:
return -1
if which == 'tilescomp':
dirname_comp = self._dialogs.ask_directory("Choose directory containing comparison data", parent=self)
if dirname_comp is None:
return -1
with self._dialogs.show_busy(_('Importing files')):
newobj = Tiles(filename, parent=self, linked_data_dir=dirname, mapviewer=self)
if which == 'tilescomp':
newobj.linked_data_dir_comp = dirname_comp
self.mytiles.append(newobj)
self.active_tile = newobj
self.menu_tiles()
return self.myitemsvector, newobj, id
[docs]
def _add_array_xyz(self, which, filename, newobj, curfilter, ToCheck, id):
if self._dialogs.ask_yes_no(_('Do you want to crop the data?'), style=DialogStyles.YES_NO_DEFAULT_YES, parent=self):
newcrop = CropDialog(None)
badvalues = True
while badvalues:
badvalues = False
ret = newcrop.ShowModal()
if ret == wx.ID_CANCEL:
newcrop.Destroy()
return -1
else:
cropini = [[float(newcrop.ox.Value), float(newcrop.ex.Value)],
[float(newcrop.oy.Value), float(newcrop.ey.Value)]]
tmpdx = float(newcrop.dx.Value)
tmpdy = float(newcrop.dy.Value)
newcrop.Destroy()
myxyz = xyz_scandir(filename, cropini)
myhead = newcrop.get_header()
else:
_dx = self._dialogs.ask_float(_('Spatial step size (assuming dx == dy) ?'), default='1')
if _dx is None:
return -1
tmpdx = _dx
dy = tmpdx
myxyz = xyz_scandir(filename, None)
myhead = header_wolf()
myhead.origx = np.min(myxyz[:, 0]) - tmpdx / 2.
myhead.origy = np.min(myxyz[:, 1]) - dy / 2.
myhead.dx = tmpdx
myhead.dy = dy
myhead.nbx = int(np.max(myxyz[:, 0]) - myhead.origx) + 1
myhead.nby = int(np.max(myxyz[:, 1]) - myhead.origy) + 1
if len(myxyz) == 0:
return -1
newobj = WolfArray()
newobj.init_from_header(myhead)
newobj.nullvalue = -99999.
newobj.array.data[:, :] = -99999.
newobj.fillin_from_xyz(myxyz)
newobj.mask_data(newobj.nullvalue)
newobj.change_gui(self)
newobj.updatepalette(0)
self.myarrays.append(newobj)
self.active_array = newobj
self._refresh_hillshade_panel_for_active()
self._set_active_bc()
return self.myitemsarray, newobj, id
[docs]
def _add_array_lidar(self, which, filename, newobj, curfilter, ToCheck, id):
newcrop = CropDialog(None)
badvalues = True
while badvalues:
badvalues = False
ret = newcrop.ShowModal()
if ret == wx.ID_CANCEL:
newcrop.Destroy()
return -1
else:
cropini = [[float(newcrop.ox.Value), float(newcrop.ex.Value)],
[float(newcrop.oy.Value), float(newcrop.ey.Value)]]
tmpdx = float(newcrop.dx.Value)
tmpdy = float(newcrop.dy.Value)
newcrop.Destroy()
first, sec = Lidar2002.lidar_scandir(filename, cropini)
if which == 'array_lidar_first':
if len(first) == 0:
return -1
newobj = Lidar2002.create_wolfarray(first, bounds=cropini)
id = 'lidar2002_firstecho'
else:
if len(sec) == 0:
return -1
newobj = Lidar2002.create_wolfarray(sec, bounds=cropini)
id = 'lidar2002_secondecho'
if min(tmpdx, tmpdy) != 1.:
newobj.rebin(min(tmpdx, tmpdy))
newobj.change_gui(self)
newobj.updatepalette(0)
self.myarrays.append(newobj)
self.active_array = newobj
self._refresh_hillshade_panel_for_active()
self._set_active_bc()
return self.myitemsarray, newobj, id
[docs]
def _add_res2d(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
with self._dialogs.show_busy(_('Importing 2D model')):
newobj = Wolfresults_2D(filename, mapviewer=self)
newobj.get_nbresults(True)
newobj.updatepalette()
self.myres2D.append(newobj)
self.active_res2d = newobj
return self.myitemsres2d, newobj, id
[docs]
def _add_res2d_gpu(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
newobj = wolfres2DGPU(filename, mapviewer=self)
if newobj is None:
logging.warning(_('Error while importing GPU results'))
return -1
res = newobj.get_nbresults(True)
if res is None:
logging.error(_('Error while importing GPU results - No results found'))
return -1
newobj.read_oneresult(-1)
if newobj.loaded:
newobj.updatepalette()
self.myres2D.append(newobj)
self.active_res2d = newobj
return self.myitemsres2d, newobj, id
[docs]
def _add_vector(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
with self._dialogs.show_busy(_('Importing file')):
newobj = Zones(filename)
self.myvectors.append(newobj)
newobj.change_gui(self)
return self.myitemsvector, newobj, id
[docs]
def _add_cross_sections(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
dirlaz = ''
if self._dialogs.ask_confirmation('Load LAZ data?', default='no', parent=self):
if self.mylazgrid is not None:
if self._dialogs.ask_confirmation('Gridded LAZ data exist - use them ?', default='yes', parent=self):
dirlaz = self.mylazgrid
else:
_path = self._dialogs.ask_directory('If exist, where are the LAZ data?', parent=self)
if _path is not None:
dirlaz = _path
else:
_path = self._dialogs.ask_directory('If exist, where are the LAZ data?', parent=self)
if _path is not None:
dirlaz = _path
_cs_formats = {0: 'vecz', 1: '2022', 2: 'sxy', 3: '2000', 4: '2025_xlsx', 5: 'ISLDNT_xlsx'}
fmt = _cs_formats.get(curfilter, '2000')
with self._dialogs.show_busy(_('Importing cross sections')):
newobj = crosssections(filename, format=fmt, dirlaz=dirlaz, mapviewer=self)
self.myvectors.append(newobj)
newobj.mapviewer = self
return self.myitemsvector, newobj, id
[docs]
def _add_laz(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
newobj = Wolf_LAZ_Data(mapviewer=self)
newobj.from_file(filename)
self.mylazdata.append(newobj)
self.active_laz = newobj
newobj.set_mapviewer(self)
return self.myitemslaz, newobj, id
[docs]
def _add_cloud(self, which, filename, newobj, curfilter, ToCheck, id):
curtree = self.myitemscloud
if newobj is None:
try:
loadhead = False
if filename.endswith('.json'):
newobj = cloud_vertices.load_json(filename)
elif not filename.endswith('.dxf') and not filename.endswith('.shp'):
with open(filename, 'r') as f:
text = f.read().splitlines()
tmphead = ''
for i in range(min(4, len(text))):
tmphead += text[i].replace('\t', '\\t') + '\n'
if self._dialogs.ask_yes_no(_('Is there a file header (one upper line containing column names)?') + '\n\n' + tmphead, style=DialogStyles.YES_NO_DEFAULT_NO):
loadhead = True
newobj = cloud_vertices(filename, header=loadhead, mapviewer=self)
elif filename.endswith('.dxf'):
types = ['POLYLINE', 'LWPOLYLINE', 'LINE', 'MTEXT', 'INSERT']
_sel = self._dialogs.ask_multi_choice(_('Choose the types of entities to import'), _('Choose entities'), types, parent=self)
if _sel is None:
return -1
types = [types[i] for i in _sel]
newobj = cloud_vertices(filename, header=loadhead, mapviewer=self, dxf_imported_elts=types)
elif filename.endswith('.shp'):
if Path(filename).stem == 'Vesdre_Bridges':
data = gpd.read_file(filename)
clogged = data[data['Clogging'] == 'Yes']
unclogged = data[data['Clogging'] == 'No']
notsure = data[data['Clogging'] == 'No information']
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmpdirname:
clogged.to_file(tmpdirname + '/clogged.shp')
unclogged.to_file(tmpdirname + '/unclogged.shp')
notsure.to_file(tmpdirname + '/notsure.shp')
newobj = cloud_vertices(tmpdirname + '/unclogged.shp', header=loadhead, mapviewer=self, idx='unclogged')
self.myclouds.append(newobj)
newobj.set_mapviewer(self)
newobj.myprop.color = (0, 255, 0)
newobj.myprop.size = 10
myitem = self.treelist.AppendItem(curtree, newobj.idx, data=newobj)
self.treelist.CheckItem(myitem)
self.treelist.CheckItem(self.treelist.GetItemParent(myitem))
newobj.check_plot()
newobj = cloud_vertices(tmpdirname + '/notsure.shp', header=loadhead, mapviewer=self, idx='notsure')
self.myclouds.append(newobj)
newobj.set_mapviewer(self)
newobj.myprop.color = (0, 0, 255)
newobj.myprop.size = 10
myitem = self.treelist.AppendItem(curtree, newobj.idx, data=newobj)
self.treelist.CheckItem(myitem)
self.treelist.CheckItem(self.treelist.GetItemParent(myitem))
newobj.check_plot()
newobj = cloud_vertices(tmpdirname + '/clogged.shp', header=loadhead, mapviewer=self, idx='clogged')
newobj.myprop.color = (255, 0, 0)
newobj.myprop.size = 15
id = 'clogged'
else:
newobj = cloud_vertices(filename, header=loadhead, mapviewer=self)
except Exception as e:
logging.warning(_('Error while importing cloud vertices -- Check the file format and content -- Error message : ') + str(e))
return -1
self.myclouds.append(newobj)
self.active_cloud = newobj
newobj.set_mapviewer(self)
self.create_cloud_menu()
return curtree, newobj, id
[docs]
def _add_clouds(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
if not filename.endswith('.json'):
logging.warning(_('For cloud of clouds, only json files are supported -- Please provide a json file containing the cloud of clouds'))
return None
newobj = cloud_of_clouds.load_json(filename)
self.myclouds.append(newobj)
self.active_cloud = newobj
newobj.set_mapviewer(self)
self.create_cloud_menu()
return self.myitemscloud, newobj, id
[docs]
def _add_triangulation(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
with self._dialogs.show_busy(_('Importing triangulation')):
newobj = Triangulation(filename, mapviewer=self)
self.mytri.append(newobj)
self.active_tri = newobj
self.create_triangles_menu()
return self.myitemstri, newobj, id
[docs]
def _add_other(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
logging.warning(_('No object to add in "Other" category -- Please provide an object to add or check your code'))
return None
self.myothers.append(newobj)
newobj.mapviewer = self
return self.myitemsothers, newobj, id
[docs]
def _add_views(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
newobj = WolfViews(plotted=ToCheck, mapviewer=self)
newobj.read_from_file(filename)
self.myviews.append(newobj)
return self.myitemsviews, newobj, id
[docs]
def _add_wmsback(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
logging.warning(_('No object to add in "WMS background" category -- Please provide an object to add or check your code'))
return None
self.mywmsback.append(newobj)
return self.myitemswmsback, newobj, id
[docs]
def _add_wmsfore(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
logging.warning(_('No object to add in "WMS foreground" category -- Please provide an object to add or check your code'))
return None
self.mywmsfore.append(newobj)
return self.myitemswmsfore, newobj, id
[docs]
def _add_particlesystem(self, which, filename, newobj, curfilter, ToCheck, id):
if newobj is None:
newobj = Particle_system(mapviewer=self)
newobj.load(filename)
self.mypartsystems.append(newobj)
self.active_particle_system = newobj
return self.myitemsps, newobj, id
[docs]
def _add_drowning(self, which, filename, newobj, curfilter, ToCheck, id):
self._drowning.register(newobj)
return self.myitemsdrowning, newobj, id
[docs]
def _add_dike(self, which, filename, newobj, curfilter, ToCheck, id):
if not WOLFPYDIKE_AVAILABLE:
logging.error('WolfPyDike module not available - cannot add dike')
return -1
self._dike.register(newobj)
return self.myitemsdike, newobj, id
[docs]
def _add_injector(self, which, filename, newobj, curfilter, ToCheck, id):
self.myinjectors.append(newobj)
self.active_injector = newobj
return self.myitemsinjector, newobj, id
[docs]
def add_pie_asset(self, *args, **kwargs) -> 'Zones | PieZonesController':
return self._assets.add_pie_asset(*args, **kwargs)
[docs]
def _bind_pie_controller_to_zones(self, controller, zones) -> None:
self._assets._bind_pie_controller_to_zones(controller, zones)
[docs]
def _iter_pie_controllers(self) -> list:
return self._assets._iter_pie_controllers()
[docs]
def _get_pie_controller(self, pie_id: str):
return self._assets._get_pie_controller(pie_id)
[docs]
def OnCreatePieChart(self, event: wx.Event) -> None:
self._assets.OnCreatePieChart(event)
[docs]
def OnEditPieChart(self, event: wx.Event) -> None:
self._assets.OnEditPieChart(event)
[docs]
def OnLoadPieChartJSON(self, event: wx.Event) -> None:
self._assets.OnLoadPieChartJSON(event)
[docs]
def add_bar_asset(self, *args, **kwargs) -> 'Zones | BarZonesController':
return self._assets.add_bar_asset(*args, **kwargs)
[docs]
def _bind_bar_controller_to_zones(self, controller, zones) -> None:
self._assets._bind_bar_controller_to_zones(controller, zones)
[docs]
def _iter_bar_controllers(self) -> list:
return self._assets._iter_bar_controllers()
[docs]
def _get_bar_controller(self, bar_id: str):
return self._assets._get_bar_controller(bar_id)
[docs]
def OnCreateBarChart(self, event: wx.Event) -> None:
self._assets.OnCreateBarChart(event)
[docs]
def OnEditBarChart(self, event: wx.Event) -> None:
self._assets.OnEditBarChart(event)
[docs]
def OnLoadBarChartJSON(self, event: wx.Event) -> None:
self._assets.OnLoadBarChartJSON(event)
[docs]
def add_curve_asset(self, *args, **kwargs) -> 'Zones | CurveZonesController':
return self._assets.add_curve_asset(*args, **kwargs)
[docs]
def _bind_curve_controller_to_zones(self, controller, zones) -> None:
self._assets._bind_curve_controller_to_zones(controller, zones)
[docs]
def _iter_curve_controllers(self) -> list:
return self._assets._iter_curve_controllers()
[docs]
def _get_curve_controller(self, curve_id: str):
return self._assets._get_curve_controller(curve_id)
[docs]
def OnCreateCurveChart(self, event: wx.Event) -> None:
self._assets.OnCreateCurveChart(event)
[docs]
def OnEditCurveChart(self, event: wx.Event) -> None:
self._assets.OnEditCurveChart(event)
[docs]
def OnLoadCurveChartJSON(self, event: wx.Event) -> None:
self._assets.OnLoadCurveChartJSON(event)
[docs]
def OnCreateBoxplot(self, event: wx.Event) -> None:
self._assets.OnCreateBoxplot(event)
[docs]
def OnEditBoxplot(self, event: wx.Event) -> None:
self._assets.OnEditBoxplot(event)
[docs]
def OnLoadBoxplotJSON(self, event: wx.Event) -> None:
self._assets.OnLoadBoxplotJSON(event)
[docs]
def replace_object(self, id: str, newobj, drawing_type: draw_type = None):
""" Replace an object in the list of objects of type drawing_type """
if drawing_type is None:
for curdict in draw_type:
keys = self.get_list_keys(curdict, checked_state=None)
if id.lower() in [k.lower() for k in keys]:
# The object exists in the current dictionary
obj = self.get_obj_from_id(id, drawing_type=curdict)
obj.reset_listogl()
# Searching the object in all lists
if obj is not None:
curlist = self._get_list(drawing_type=curdict)
if obj in curlist:
pos = curlist.index(obj)
if isinstance(newobj, curlist[pos].__class__):
# Updating the tree item
self.treelist.SetItemData(self.get_treeitem_from_obj(obj), newobj)
curlist[pos] = newobj
newobj.idx = id.lower()
else:
logging.error(f'Cannot replace {id} with {newobj.idx} - Different type of object')
else:
logging.error(f'Object {id} not found in list')
else:
logging.error(f'Object {id} not found in list')
else:
logging.error(f'Object {id} not found in dictionary {curdict}')
else:
keys = self.get_list_keys(drawing_type, checked_state=None)
if id.lower() in [k.lower() for k in keys]:
# The object exists in the current dictionary
obj = self.get_obj_from_id(id, drawing_type=drawing_type)
obj.reset_listogl()
# Searching the object in all lists
if obj is not None:
curlist = self._get_list(drawing_type=drawing_type)
if obj in curlist:
pos = curlist.index(obj)
if isinstance(newobj, curlist[pos].__class__):
# Updating the tree item
self.treelist.SetItemData(self.get_treeitem_from_obj(obj), newobj)
curlist[pos] = newobj
newobj.idx = id.lower()
else:
logging.error(f'Cannot replace {id} with {newobj.idx} - Different type of object')
else:
logging.error(f'Object {id} not found in list')
else:
logging.error(f'Object {id} not found in list')
else:
logging.error(f'Object {id} not found in dictionary {drawing_type}')
obj = self.get_obj_from_id(id, drawing_type=drawing_type)
obj_from_tree = self.get_obj_from_treeitem(self.get_treeitem_from_id(id, drawing_type=drawing_type))
if obj is not None and obj_from_tree is not None:
if obj is obj_from_tree:
logging.debug(f'Object {id} replaced successfully in the list and tree item')
else:
logging.error(f'Object {id} replaced in the list but not in the tree item - {obj} != {obj_from_tree}')
else:
logging.error(f'Object {id} not found in the list or tree item after replacement')
[docs]
def get_obj_from_treeitem(self, treeitem):
""" Find the object associated with treeitem """
return self.treelist.GetItemData(treeitem)
[docs]
def get_treeitem_from_id(self, id: str, drawing_type: draw_type = None):
""" Find the tree item associated with id """
obj = self.get_obj_from_id(id, drawing_type=drawing_type)
if obj is not None:
return self.get_treeitem_from_obj(obj)
return None
[docs]
def get_treeitem_from_obj(self, obj):
""" Find the tree item associated with obj.
Alias for "gettreeitem".
"""
return self.gettreeitem(obj)
[docs]
def getobj_from_id(self, id: str, drawing_type: draw_type = None):
""" Find the object associated with id """
if drawing_type is None:
for curdict in draw_type:
keys = self.get_list_keys(curdict, checked_state=None)
keys_lower = [k.lower() for k in keys]
if id.lower() in keys_lower:
try:
idx = keys_lower.index(id.lower())
return self.get_list_objects(curdict, checked_state=None)[idx]
except:
return None
else:
keys = self.get_list_keys(drawing_type, checked_state=None)
keys_lower = [k.lower() for k in keys]
if id.lower() in keys_lower:
try:
idx = keys_lower.index(id.lower())
return self.get_list_objects(drawing_type, checked_state=None)[idx]
except:
return None
[docs]
def get_obj_from_id(self, id: str, drawing_type: draw_type = None):
""" Find the object associated with id in a specifid drawtype
If you want to search in all drawtypes, use getobj_from_id instead.
:param id: str : id of the object
:param drawtype: draw_type : type of object to search
"""
keys = self.get_list_keys(drawing_type, checked_state=None)
keys_lower = [k.lower() for k in keys]
if id.lower() in keys_lower:
try:
idx = keys_lower.index(id.lower())
return self.get_list_objects(drawing_type, checked_state=None)[idx]
except:
return None
[docs]
def _get_list(self, drawing_type:draw_type = None):
""" return the list of objects of type drawing_type """
# ARRAYS = 'arrays'
# BRIDGES= 'bridges'
# WEIRS = 'weirs'
# VECTORS = 'vectors'
# CLOUD = 'clouds'
# TRIANGULATION = 'triangulations'
# PARTICLE_SYSTEM = 'particle systems'
# CROSS_SECTIONS = 'cross_sections'
# OTHER = 'others'
# VIEWS = 'views'
# RES2D = 'wolf2d'
# WMSBACK = 'wms-background'
# WMSFORE = 'wms-foreground'
# PICTURE_COLLECTION = 'picture collections'
if drawing_type is None:
# return all_lists
return self.myarrays + self.myvectors + self.myclouds + self.mytri + self.mypartsystems + self.myothers + self.myviews + self.myres2D + self.mydikes + self.mydrownings + self.myinjectors + self.mypicturecollections
if drawing_type == draw_type.ARRAYS:
return self.myarrays
elif drawing_type == draw_type.VECTORS or drawing_type == draw_type.BRIDGES or drawing_type == draw_type.WEIRS or drawing_type == draw_type.CROSS_SECTIONS :
return self.myvectors
elif drawing_type == draw_type.TILES:
return self.mytiles
elif drawing_type == draw_type.CLOUD:
return self.myclouds
elif drawing_type == draw_type.TRIANGULATION:
return self.mytri
elif drawing_type == draw_type.RES2D:
return self.myres2D
elif drawing_type == draw_type.PARTICLE_SYSTEM:
return self.mypartsystems
elif drawing_type == draw_type.OTHER:
return self.myothers
elif drawing_type == draw_type.VIEWS:
return self.myviews
elif drawing_type == draw_type.WMSBACK:
return self.mywmsback
elif drawing_type == draw_type.WMSFORE:
return self.mywmsfore
elif drawing_type == draw_type.IMAGESTILES:
return self.myimagestiles
elif drawing_type == draw_type.LAZ:
return self.mylazdata
elif drawing_type == draw_type.DROWNING:
return self.mydrownings
elif drawing_type == draw_type.DIKE:
return self.mydikes
elif drawing_type == draw_type.INJECTOR:
return self.myinjectors
elif drawing_type == draw_type.PICTURECOLLECTION:
return self.mypicturecollections
else:
logging.error('Unknown drawing type : ' + drawing_type)
return None
[docs]
def get_list_keys(self, drawing_type:draw_type = None, checked_state:bool=True):
""" Create a list of keys of type draw_type.
Return a list of keys (idx) in LOWER CASE of objects of type draw_type.
:param drawing_type: type of object to search - If None, return all objects
:param checked_state: if True/False, return only keys of objects that are plotted or not. None return all objects.
"""
if checked_state is None:
return [curobj.idx for curobj in self._get_list(drawing_type)]
else:
return [curobj.idx for curobj in self._get_list(drawing_type) if curobj.plotted == checked_state]
[docs]
def get_list_ids(self, drawing_type:draw_type = None, checked_state:bool=True):
""" Alias for get_list_keys """
return self.get_list_keys(drawing_type, checked_state)
[docs]
def get_list_objects(self, drawing_type:draw_type = None, checked_state:bool=True):
""" Create a list of objects of type draw_type.
Return a list of keys (idx) in LOWER CASE of objects of type draw_type.
:param drawing_type: type of object to search -- If None, return all objects.
:param checked_state: if True/False, return only objects that are plotted or not. None return all objects.
"""
if checked_state is None:
return [curobj for curobj in self._get_list(drawing_type)]
else:
return [curobj for curobj in self._get_list(drawing_type) if curobj.plotted == checked_state]
[docs]
def single_choice_key(self, draw_type:draw_type, checked_state:bool=True, message:str=_('Make a choice'), title:str=_('Choice')):
""" Create wx dialog to choose a key object of type draw_type """
keys = self.get_list_keys(draw_type, checked_state)
return self._dialogs.ask_single_choice(message, title, keys)
[docs]
def single_choice_object(self, draw_type:draw_type, checked_state:bool=True, message:str=_('Make a choice'), title:str=_('Choice')):
""" Create wx dialog to choose an object of type draw_type """
keys = self.get_list_keys(draw_type, checked_state)
obj = self.get_list_objects
selected = self._dialogs.ask_single_choice(message, title, keys, parent=self)
if selected is None:
return None
idx = keys.index(selected)
return obj[idx]
[docs]
def multiple_choice_key(self, draw_type:draw_type, checked_state:bool=True, message:str=_('Make a choice'), title:str=_('Choice')):
""" Create wx dialog to choose multiple keys object of type draw_type """
keys = self.get_list_keys(draw_type, checked_state)
idx = self._dialogs.ask_multi_choice(message, title, keys)
if idx is None:
return None
return [keys[i] for i in idx]
[docs]
def multiple_choice_object(self, draw_type:draw_type, checked_state:bool=True, message:str=_('Make a choice'), title:str=_('Choice')):
""" Create wx dialog to choose multiple objects of type draw_type """
keys = self.get_list_keys(draw_type, checked_state)
obj = self.get_list_objects
idx = self._dialogs.ask_multi_choice(message, title, keys, parent=self)
if idx is None:
return None
return [obj[i] for i in idx]
[docs]
def iterator_over_objects(self, drawing_type:draw_type, checked_state:bool=True):
""" Create iterator over objects of type draw_type """
for obj in self.get_list_objects(drawing_type, checked_state):
yield obj
[docs]
def gettreeitem(self, obj):
""" Find the tree item associated with obj """
up = self.treelist.GetFirstItem()
updata = self.treelist.GetItemData(up)
while updata is not obj:
up = self.treelist.GetNextItem(up)
updata = self.treelist.GetItemData(up)
return up
[docs]
def removeobj(self):
"""Remove selected item from general tree"""
if self.selected_treeitem is None:
return
id = self.treelist.GetItemText(self.selected_treeitem).lower()
self.removeobj_from_id(id)
[docs]
def checkuncheckobj(self):
""" Check/uncheck selected item from general tree """
if self.selected_treeitem is None:
return
id = self.treelist.GetItemText(self.selected_treeitem).lower()
current_check = self.treelist.GetCheckedState(self.selected_treeitem)
myobj = self.getobj_from_id(id)
if myobj is not None:
if current_check == 0:
self.treelist.CheckItem(self.selected_treeitem)
myobj.check_plot()
else:
self.treelist.CheckItem(self.selected_treeitem, False)
myobj.uncheck_plot()
[docs]
def removeobj_from_id(self, id:str, draw_type:draw_type = None):
""" Remove object from id """
myobj = self.getobj_from_id(id)
if myobj is not None:
self.treelist.DeleteItem(self.gettreeitem(myobj))
for curlist in self.all_lists:
if myobj in curlist:
curlist.pop(curlist.index(myobj))
myobj.hide_properties()
if self.clear_active_if_is(myobj):
self.set_label_selecteditem('')
# ----------------------------------------------------------------
# Tree drag & drop reordering
# ----------------------------------------------------------------
[docs]
_tree_drag_active: bool = False
[docs]
_tree_drag_source_item = None # TreeListItem being dragged
[docs]
_tree_drag_source_obj = None # Python object being dragged
[docs]
_tree_drag_start_pos = None # wx.Point of initial click
[docs]
_tree_drag_drop_before: bool = False # True = insert before target
[docs]
_tree_drag_overlay = None # wx.Overlay for drop indicator
[docs]
_TREE_DRAG_THRESHOLD: int = 6 # pixels before drag starts
[docs]
def _tree_drag_init(self):
"""Bind mouse events on the DataView's main window for DnD."""
dv = self.treelist.GetDataView()
mw = dv.GetMainWindow()
mw.Bind(wx.EVT_MOTION, self._on_tree_motion)
mw.Bind(wx.EVT_LEFT_UP, self._on_tree_left_up)
mw.Bind(wx.EVT_LEFT_DOWN, self._on_tree_left_down)
[docs]
def _on_tree_left_down(self, e: wx.MouseEvent):
"""Record potential drag start position."""
e.Skip() # Allow normal selection processing
self._tree_drag_start_pos = e.GetPosition()
self._tree_drag_active = False
[docs]
def _on_tree_motion(self, e: wx.MouseEvent):
"""Detect drag start, update cursor and draw drop indicator."""
e.Skip() # Allow normal processing
if not e.LeftIsDown() or self._tree_drag_start_pos is None:
if self._tree_drag_active:
self._tree_drag_cancel()
return
if self.selected_treeitem is None or self.selected_object is None:
return
pos = e.GetPosition() # MainWindow client coords
if not self._tree_drag_active:
dx = abs(pos.x - self._tree_drag_start_pos.x)
dy = abs(pos.y - self._tree_drag_start_pos.y)
if dx + dy < self._TREE_DRAG_THRESHOLD:
return
# start drag — capture source ONCE
parent = self.treelist.GetItemParent(self.selected_treeitem)
if parent == self.root or not parent.IsOk():
return # don't drag category headers
self._tree_drag_source_item = self.selected_treeitem
self._tree_drag_source_obj = self.selected_object
self._tree_drag_active = True
self._tree_drag_overlay = wx.Overlay()
dv = self.treelist.GetDataView()
mw = dv.GetMainWindow()
mw.SetCursor(wx.Cursor(wx.CURSOR_HAND))
# Convert MainWindow coords → DataViewCtrl coords for HitTest
pos_screen = mw.ClientToScreen(pos)
pos_dv = dv.ScreenToClient(pos_screen)
dvi, _col = dv.HitTest(pos_dv)
if dvi.IsOk():
rect = dv.GetItemRect(dvi) # in dv client coords
# Convert rect origin to MainWindow coords
rect_screen = dv.ClientToScreen(wx.Point(rect.x, rect.y))
rect_mw = mw.ScreenToClient(rect_screen)
mid_y = rect_mw.y + rect.height // 2
self._tree_drag_drop_before = pos.y < mid_y
indicator_y = rect_mw.y if self._tree_drag_drop_before else rect_mw.y + rect.height
# Draw drop indicator line via overlay
dc = wx.ClientDC(mw)
odc = wx.DCOverlay(self._tree_drag_overlay, dc)
odc.Clear()
dc.SetPen(wx.Pen(wx.Colour(0, 120, 215), 2, wx.PENSTYLE_SOLID))
dc.DrawLine(0, indicator_y, mw.GetSize().width, indicator_y)
del odc
else:
# Clear indicator when not over a valid item
if self._tree_drag_overlay:
dc = wx.ClientDC(mw)
odc = wx.DCOverlay(self._tree_drag_overlay, dc)
odc.Clear()
del odc
[docs]
def _on_tree_left_up(self, e: wx.MouseEvent):
"""Perform drop if drag was active."""
e.Skip()
if not self._tree_drag_active:
self._tree_drag_start_pos = None
return
dv = self.treelist.GetDataView()
mw = dv.GetMainWindow()
# Convert MainWindow coords → DataViewCtrl coords for HitTest
pos = e.GetPosition()
pos_screen = mw.ClientToScreen(pos)
pos_dv = dv.ScreenToClient(pos_screen)
dvi, _col = dv.HitTest(pos_dv)
drop_ok = False
drop_item = None # TreeListItem
insert_before = False
if dvi.IsOk():
# Determine above / below from mouse position vs item center
rect = dv.GetItemRect(dvi)
rect_screen = dv.ClientToScreen(wx.Point(rect.x, rect.y))
rect_mw = mw.ScreenToClient(rect_screen)
mid_y = rect_mw.y + rect.height // 2
insert_before = pos.y < mid_y
# Get the text of the drop target via the DataView model
model = dv.GetModel()
drop_text = model.GetValue(dvi, 0).GetText()
# Walk children of source's parent to find matching TreeListItem
src_parent = self.treelist.GetItemParent(self._tree_drag_source_item)
child = self.treelist.GetFirstChild(src_parent)
while child.IsOk():
if self.treelist.GetItemText(child) == drop_text:
child_data = self.treelist.GetItemData(child)
if child_data is not self._tree_drag_source_obj:
drop_item = child
break
child = self.treelist.GetNextSibling(child)
if drop_item is not None:
drop_ok = True
if drop_ok:
self._move_obj_to(self._tree_drag_source_item,
self._tree_drag_source_obj,
drop_item,
before=insert_before)
self._tree_drag_cancel()
self.Refresh()
[docs]
def _tree_drag_cancel(self):
"""Reset drag state, cursor and overlay."""
self._tree_drag_active = False
self._tree_drag_source_item = None
self._tree_drag_source_obj = None
self._tree_drag_start_pos = None
self._tree_drag_drop_before = False
dv = self.treelist.GetDataView()
mw = dv.GetMainWindow()
mw.SetCursor(wx.NullCursor)
if self._tree_drag_overlay:
self._tree_drag_overlay.Reset()
self._tree_drag_overlay = None
[docs]
def _move_obj_to(self, src_item, src_obj, target_item, before=False):
"""Move *src_item* before or after *target_item* in the tree and lists.
Both items must share the same parent (category).
"""
parent = self.treelist.GetItemParent(src_item)
src_text = self.treelist.GetItemText(src_item)
src_checked = self.treelist.GetCheckedState(src_item)
# Delete source first so sibling traversal is clean
self.treelist.DeleteItem(src_item)
if before:
# Find the predecessor of target_item
prev = None
child = self.treelist.GetFirstChild(parent)
while child.IsOk():
if child == target_item:
break
prev = child
child = self.treelist.GetNextSibling(child)
if prev is None:
new_item = self.treelist.PrependItem(parent, src_text, data=src_obj)
else:
new_item = self.treelist.InsertItem(parent, prev, src_text, data=src_obj)
else:
new_item = self.treelist.InsertItem(parent, target_item,
src_text, data=src_obj)
self.treelist.CheckItem(new_item, src_checked)
self.selected_treeitem = new_item
# Synchronize the Python list order to match the new tree order
for curlist in self.all_lists:
if src_obj in curlist:
# Rebuild list order from the actual tree
ordered = []
child = self.treelist.GetFirstChild(parent)
while child.IsOk():
data = self.treelist.GetItemData(child)
if data in curlist:
ordered.append(data)
child = self.treelist.GetNextSibling(child)
# Replace list contents with the new order
# (preserve any items not visible in this category)
remaining = [o for o in curlist if o not in ordered]
curlist[:] = ordered + remaining
break
[docs]
def upobj(self):
"""Up selected item into general tree"""
if self.selected_treeitem is None:
return
id:str
id = self.treelist.GetItemText(self.selected_treeitem).lower()
myobj = self.getobj_from_id(id)
ischecked = self.treelist.GetCheckedState(self.selected_treeitem)
assert self.selected_object is myobj, 'selected_object is not myobj'
if myobj is not None:
down = self.treelist.GetNextItem(self.selected_treeitem)
up = self.treelist.GetFirstItem()
up2 = up
while self.treelist.GetNextItem(up) != self.selected_treeitem:
up2= up
up = self.treelist.GetNextItem(up)
parent = self.treelist.GetItemParent(self.selected_treeitem)
parentup = self.treelist.GetItemParent(up)
parentup2 = self.treelist.GetItemParent(up2)
if parent == parentup2:
# up n'est pas le premier élément de la liste
myitem = self.treelist.InsertItem(parent,up2,id,data=myobj)
self.treelist.CheckItem(myitem,ischecked)
elif parentup == parent:
# up est le premier élément de la liste
myitem = self.treelist.PrependItem(parent,id,data=myobj)
self.treelist.CheckItem(myitem,ischecked)
else:
# nothing to do
return
self.treelist.DeleteItem(self.selected_treeitem)
self.selected_treeitem = myitem
# mouvement dans les listes pour garder l'ordre identique à l'arbre
for curlist in self.all_lists:
if myobj in curlist:
idx = curlist.index(myobj)
if idx>0:
curlist.pop(idx)
curlist.insert(idx-1,myobj)
self.Refresh()
[docs]
def downobj(self):
"""Down selected item into general tree"""
if self.selected_treeitem is None:
return
id = self.treelist.GetItemText(self.selected_treeitem).lower()
myobj = self.getobj_from_id(id)
ischecked = self.treelist.GetCheckedState(self.selected_treeitem)
if myobj is not None:
down = self.treelist.GetNextItem(self.selected_treeitem)
down2 = self.treelist.GetNextItem(down)
parent = self.treelist.GetItemParent(self.selected_treeitem)
parentdown = self.treelist.GetItemParent(down)
parentdown2 = self.treelist.GetItemParent(down2)
if parent == parentdown:
# on n'est pas sur le dernoier élément
myitem = self.treelist.InsertItem(parent,down,id,data=myobj)
self.treelist.CheckItem(myitem,ischecked)
else:
# nothing to do
return
self.treelist.DeleteItem(self.selected_treeitem)
self.selected_treeitem = myitem
for curlist in self.all_lists:
if myobj in curlist:
if len(curlist)>1:
idx = curlist.index(myobj)
if idx == len(curlist)-1:
# dernier --> rien à faire
pass
elif idx==len(curlist)-2:
# avant-dernier --> passage en dernier
curlist.append(myobj)
curlist.pop(idx)
elif idx<len(curlist)-2:
curlist.insert(idx+2,myobj)
curlist.pop(idx)
self.Refresh()
# ---- Popup item handlers -------------------------------------------------
[docs]
def _popup_clip_h_band(self) -> None:
from .array_core.clipping import ClipZoneMixin as _CZM
if isinstance(self.selected_object, _CZM):
cz, sliders = self.selected_object.setup_horizontal_band()
self.Refresh()
[docs]
def _popup_clip_v_band(self) -> None:
from .array_core.clipping import ClipZoneMixin as _CZM
if isinstance(self.selected_object, _CZM):
cz, sliders = self.selected_object.setup_vertical_band()
self.Refresh()
[docs]
def OnClose(self, event):
""" Close the application """
if getattr(self, 'anim_clock', None) is not None:
self.anim_clock.destroy()
nb = 0
if self.linked:
if self.linkedList is not None:
if self in self.linkedList:
id = self.linkedList.index(self)
self.linkedList.pop(id)
nb = len(self.linkedList)
if nb == 0:
if self.wxlogging is not None:
if self._dialogs.ask_yes_no(_('Do you want to quit Wolf ?'), _('Quit Wolf'), wx.YES_NO | wx.NO_DEFAULT):
self.Destroy()
#FIXME : It is not a really proper way to quit the application
wx.Exit()
return
else:
return
self.Destroy()
[docs]
def OnSelectItem(self, event):
""" Select the item in the tree list """
ctrl = wx.GetKeyState(wx.WXK_CONTROL)
alt = wx.GetKeyState(wx.WXK_ALT)
myitem = event.GetItem()
nameitem = self.treelist.GetItemText(myitem).lower()
curobj = self.getobj_from_id(nameitem)
myobj = self.treelist.GetItemData(myitem)
if curobj is not myobj:
logging.error(_('Bad association between object and tree item'))
logging.error(_('Do you have 2 objects with the same id ?'))
logging.error(_('It could be the case if you have drag/drop an object in the viewer...'))
logging.error(_('I will continue but it is not normal...'))
self.treelist.SetToolTip(self.treelist.GetItemText(myitem))
# myparent = self.treelist.GetItemParent(myitem)
# check = self.treelist.GetCheckedState(myitem)
# if myparent is not None:
# nameparent = self.treelist.GetItemText(myparent).lower()
self.selected_object = curobj
self.selected_treeitem = myitem
[docs]
def OnCheckItem(self, event:TreeListEvent):
""" Check the item in the tree list """
myitem = event.GetItem()
myparent = self.treelist.GetItemParent(myitem)
check = self.treelist.GetCheckedState(myitem)
nameparent = self.treelist.GetItemText(myparent).lower()
nameitem = self.treelist.GetItemText(myitem).lower()
ctrl = wx.GetKeyState(wx.WXK_CONTROL)
shiftdown = wx.GetKeyState(wx.WXK_SHIFT)
# ctrl = event.ControlDown()
if nameparent != '':
curobj = self.getobj_from_id(nameitem)
if curobj is None:
return
if bool(check):
try:
curobj.check_plot()
if isinstance(curobj, PlansTerrier):
if curobj.initialized:
self.menu_landmaps()
logging.info(_('Landmap initialized'))
else:
logging.warning(_('Landmap not initialized'))
elif isinstance(curobj, Ouvrages):
if curobj.initialized:
self.menu_pictcollection()
logging.info(_('Ouvrages collection initialized'))
else:
logging.warning(_('Ouvrages collection not initialized'))
elif isinstance(curobj, Particularites):
if curobj.initialized:
self.menu_pictcollection()
logging.info(_('Particularites collection initialized'))
else:
logging.warning(_('Particularites collection not initialized'))
elif isinstance(curobj, Enquetes):
if curobj.initialized:
self.menu_pictcollection()
logging.info(_('Enquetes collection initialized'))
else:
logging.warning(_('Enquetes collection not initialized'))
elif isinstance(curobj, Profils):
if curobj.initialized:
self.menu_pictcollection()
logging.info(_('Profils collection initialized'))
else:
logging.warning(_('Profils collection not initialized'))
except Exception as ex:
wx.LogMessage(str(ex))
wx.MessageBox(str(ex), _("Error"), wx.ICON_ERROR)
else:
if issubclass(type(curobj), WolfArray):
curobj.uncheck_plot(not ctrl,ctrl)
elif isinstance(curobj, Picc_data):
curobj.uncheck_plot(ctrl, shiftdown)
else:
curobj.uncheck_plot()
# if nameparent == 'vectors' or nameparent == 'cross_sections':
# if wx.GetKeyState(wx.WXK_CONTROL):
# curobj.showstructure(self)
if curobj.idx == 'grid' and check:
_size = self._dialogs.ask_float('Size of the Grid ? (float)', 'Choose an size', default='1000.')
size = _size if _size is not None else 1000.
curobj.creategrid(size, self.xmin, self.ymin, self.xmax, self.ymax)
if 'alaro' in curobj.idx and check:
if self.alaro_navigator is None:
self.alaro_navigator = Alaro_Navigator(self, curobj.idx, 'Alaro')
self.alaro_navigator.Show()
self.Refresh()
[docs]
def _alaro_update_time(self):
""" Update the time of the alaro navigator """
objs = self.get_list_objects(drawing_type=draw_type.WMSFORE, checked_state=True)
for obj in objs:
obj.time = self.alaro_navigator.time_str
obj.alpha = self.alaro_navigator.alpha
obj.force_alpha = True
self._update_foreground()
self.Paint()
[docs]
def _alaro_legends(self):
""" Show images of the checked alaro layers"""
objs = self.get_list_objects(drawing_type=draw_type.WMSFORE, checked_state=True)
for obj in objs:
if obj.category == 'ALARO':
img = Image.open(get_Alaro_legend(obj.idx.replace('alaro ', '')))
img.show()
[docs]
def getXY(self, pospix):
width, height = self.canvas.GetSize()
X = float(pospix[0]) / self.sx + self.xmin
Y = float(height - pospix[1]) / self.sy + self.ymin
return X, Y
[docs]
def OnZoomGesture(self, e):
pass
[docs]
def OnLeave(self, e):
if e.ControlDown():
# Do not hide the tooltip when leaving the canvas in CTRL mode:
# the tooltip is intentionally following the mouse and must stay visible.
pass
[docs]
def get_cross_sections(self):
"""
Récupération du premier objet crosssections disponible
"""
for obj in self.iterator_over_objects(draw_type.VECTORS):
if isinstance(obj,crosssections):
return obj
return None
[docs]
def set_active_profile(self, active_profile: profile):
"""
This method sets the active profile in Pydraw (useful for interfaces communication).
"""
self.active_profile = active_profile
[docs]
def set_active_vector(self, active_vector: vector):
"""
This method sets the active vector in Pydraw (useful for interfaces communication).
"""
self.active_vector = active_vector
[docs]
def get_active_profile(self):
"""
This methods returns the active profile in pydraw (useful for interfaces communication).
"""
return self.active_profile
[docs]
def plot_cross(self, x:float, y:float):
# Search for cross sections (List of profiles)
if self.active_cs is None:
self.active_cs = self.get_cross_sections()
if self.active_cs is None:
logging.warning(_('No cross sections available -- Please load a file or create data !'))
return
# Initialisation of the notebook where the active profile is plotted.
if self.notebookprof is None:
self.notebookprof = ProfileNotebook(mapviewer=self)
# self.myfigprof = self.notebookprof.add('Figure 1', which= "all")
self.myfigprof = self.notebookprof.add('Reference') # FIXME Updated add method
else:
try:
self.notebookprof.Show()
except:
self.notebookprof = ProfileNotebook(mapviewer=self)
# self.myfigprof = self.notebookprof.add('Figure 1', which= "all")
self.myfigprof = self.notebookprof.add('Reference') # FIXME updated add method
# Initialisation of the active profile
# 1. We uncolor the active profile in wolf GUI.
self.active_profile: profile
if self.active_profile is not None:
self.active_profile.uncolor_active_profile()
# 2. We select the closest profile corresponding to the user's right click in the GUI.
self.active_profile = self.active_cs.select_profile(x, y)
#Finally, we set the profile and the cross section (list of profiles) in the notebook.
#FIXME Iden establishes the communications between pydraw and the notebook (to avoid circular information).
self.myfigprof.cs_setter(mycross = self.active_cs, active_profile= self.active_profile, mapviewer = self)
sims = self.get_list_objects(drawing_type=draw_type.RES2D, checked_state=True)
if len(sims) > 0:
self.myfigprof.reset_models()
for sim in sims:
sim:Wolfresults_2D
id = sim.idx
vals = sim.get_all_values_underpoly(self.active_profile, integrate_q=True)
if vals.shape[0] == 0:
logging.warning(_('No data found along the profile for simulation {}').format(id))
continue
discharge = float(sim.get_q_alongpoly(self.active_profile))
waterdepthmax = float(np.max([val[0][0] for val in vals]))
waterlevelmax = float(np.max([val[0][7] for val in vals]))
self.myfigprof.add_model([id, discharge, waterdepthmax, waterlevelmax])
[docs]
def _bridge_gltf_dialog(self, x: float, y: float) -> None:
"""wx dialog workflow for BRIDGE_GLTF (called from the mouse handler)."""
self.bridgepar = (x, y)
zmax = 0.
_zmax = self._dialogs.ask_float('Z maximum ?', 'Choose an elevation as top', default='')
if _zmax is not None:
zmax = _zmax
fn = self._dialogs.ask_file_save(
_('Choose filename'),
wildcard='glb (*.glb)|*.glb|gltf2 (*.gltf)|*.gltf|All (*.*)|*.*',
parent=self,
)
if fn is None:
return
points, triangles = self.active_vector.triangulation_ponts(
self.bridgepar[0], self.bridgepar[1], zmax)
self.active_cs.export_gltf_gen(points, triangles, fn)
self.start_action('', 'None')
[docs]
def _ensure_notebookcs(self) -> None:
"""Create or show the cross-section notebook (shared by SET_1D_PROFILE and SELECT_NEAREST_PROFILE)."""
if self.notebookcs is None:
self.notebookcs = PlotNotebook()
self.myfigcs = self.notebookcs.add(_("Cross section"), "CS")
else:
try:
self.notebookcs.Show()
except Exception:
self.notebookcs = PlotNotebook()
self.myfigcs = self.notebookcs.add(_("Cross section"), "CS")
[docs]
def _set_1d_profile_rdown(self, x: float, y: float) -> None:
"""Logic for SET_1D_PROFILE right-click (called from the mouse handler)."""
if self.active_cs is None:
self.active_cs = self.get_cross_sections()
if self.active_cs is None:
logging.warning(_('No cross sections available -- Please load a file or create data !'))
return
self._ensure_notebookcs()
self.active_profile: profile
if self.active_profile is not None:
self.active_profile.uncolor_active_profile()
if self.myfigcs.mycs is not None:
self.myfigcs.mycs.uncolor_active_profile()
self.active_profile = self.frame_create1Dfrom2D.active_profile
self.myfigcs.set_linked_arrays(self.get_linked_arrays())
self.myfigcs.set_cs(self.active_profile)
self.active_profile.color_active_profile()
self.zoom_on_active_profile()
self.Paint()
[docs]
def _select_nearest_profile_rdown(self, x: float, y: float) -> None:
"""Logic for SELECT_NEAREST_PROFILE right-click (called from the mouse handler)."""
if self.active_cs is None:
self.active_cs = self.get_cross_sections()
if self.active_cs is None:
logging.warning(_('No cross sections available -- Please load a file or create data !'))
return
self._ensure_notebookcs()
self.active_profile: profile
if self.active_profile is not None:
self.active_profile.uncolor_active_profile()
if self.myfigcs.mycs is not None:
self.myfigcs.mycs.uncolor_active_profile()
self.active_profile = self.active_cs.select_profile(x, y)
self.myfigcs.set_linked_arrays(self.get_linked_arrays())
self.myfigcs.set_cs(self.active_profile)
self.active_profile.color_active_profile()
self.Refresh()
[docs]
def _wx_mouse_context(self, e: wx.MouseEvent) -> MouseContext:
pos = e.GetPosition()
x, y = self.getXY(pos)
alt = e.AltDown()
ctrl = e.ControlDown()
shiftdown = e.ShiftDown()
x_snap, y_snap = self._snap_xy_on_grid(x, y, do_snap=alt)
# Stylus pressure: WinTab (Wacom) → wx → 1.0
pressure = 1.0
wintab = self._wintab
if wintab is not None:
p = wintab.get_pressure()
if 0.0 <= p <= 1.0:
pressure = p
else:
try:
p = e.GetPressure()
if 0.0 < p <= 1.0:
pressure = float(p)
except AttributeError:
pass
# Poll all letter keys (A–Z) + SPACE at event time.
held = frozenset(k for k in _POLLED_KEYS if wx.GetKeyState(k))
keyboard = KeyboardSnapshot(ctrl=ctrl, shift=shiftdown, alt=alt, held=held)
return MouseContext(x=x, y=y, x_snap=x_snap, y_snap=y_snap,
x_pixel=pos[0], y_pixel=pos[1],
keyboard=keyboard,
left_down=e.LeftIsDown(),
middle_down=e.MiddleIsDown(),
right_down=e.RightIsDown(),
pressure=pressure,
wheel_rotation=e.GetWheelRotation(),
wheel_delta=e.GetWheelDelta())
[docs]
def _wx_keyboard_snapshot(self, e: wx.KeyEvent) -> KeyboardSnapshot:
"""Build a :class:`KeyboardSnapshot` from a raw *wx.KeyEvent*."""
return KeyboardSnapshot(
key_code=e.GetKeyCode(),
ctrl=e.ControlDown(),
shift=e.ShiftDown(),
alt=e.AltDown(),
is_down=e.GetEventType() == wx.wxEVT_KEY_DOWN,
)
[docs]
def schedule_once(self, delay_ms: int, fn) -> None:
"""Run *fn()* once after *delay_ms* milliseconds (wx.CallLater wrapper)."""
wx.CallLater(delay_ms, fn)
[docs]
def post_idle(self, fn) -> None:
"""Defer *fn()* to the next event-loop idle (wx.CallAfter wrapper)."""
wx.CallAfter(fn)
[docs]
def On_Mouse_Motion(self, e: wx.MouseEvent):
""" Mouse move event """
self._mouse_context = self._wx_mouse_context(e)
_ctx = self._wx_mouse_context(e)
# Hillshade overlay drag
if (self._hillshade_overlay is not None
and self._hillshade_overlay._dragging is not None
and e.LeftIsDown()):
self._hillshade_overlay.on_drag(_ctx.x_pixel, _ctx.y_pixel)
return
# Palette overlay drag
if (self._palette_overlay is not None
and self._palette_overlay._dragging is not None
and e.LeftIsDown()):
self._palette_overlay.on_drag(_ctx.x_pixel, _ctx.y_pixel)
return
# Toolbar overlay hover tracking
if self._toolbar_overlay is not None:
if self._toolbar_overlay.on_mouse_move(_ctx.x_pixel, _ctx.y_pixel):
return
# Palette overlay hover tracking
if self._palette_overlay is not None:
self._palette_overlay.on_mouse_move(_ctx.x_pixel, _ctx.y_pixel)
# ---
# ASSETS
if (
self.action == ActionKind.TRANSFORM_ASSET_BOUNDS
and e.LeftIsDown()
and self._assets.drag_handle is not None
):
if self._apply_asset_transform_drag(
_ctx.x,
_ctx.y,
keep_ratio=_ctx.shift,
resize_from_center=_ctx.ctrl,
do_snap=_ctx.alt,
):
self.Refresh()
return
self._update_asset_transform_cursor(_ctx.x, _ctx.y)
# ---
# CLIP SLIDER
# Handle active clip slider drag (intercepts before pan)
if e.LeftIsDown() and self._update_clip_slider_drag(_ctx.x, _ctx.y):
self.Refresh()
return
# ---
# Guardian - Sculpt / profile motion handling
if self._sculpt.on_motion(_ctx):
return
# Show slider label in status bar when hovering near a clip slider
if not e.LeftIsDown():
slider_label = self._get_nearest_clip_slider(_ctx.x, _ctx.y)
if slider_label:
self.set_statusbar_text(slider_label)
# --- Other mouse motion handling (pan, vertex move, etc.)
if e.LeftIsDown() or e.MiddleIsDown():
# Left mouse button or middle mouse button is pressed
#
# Moving the map relative to the position where the mouse was clicked
# the first time
if self._mouse_context_down is None: # only if the mouse was clicked before
self._mouse_context_down = self._wx_mouse_context(e)
if _ctx.shift:
if self.active_vector is None:
logging.warning(_('Shift key pressed but no active vector -- Please select a vector first !'))
return
if self.active_vector.myprop.textureimage is None:
logging.warning(_('Shift key pressed but no image texture -- Please select a vector with an image first !'))
return
# We move the image texture
delta_x = self._mouse_context.x - self._mouse_context_down.x
delta_y = self._mouse_context.y - self._mouse_context_down.y
self.active_vector.myprop._offset_image_texture(delta_x, delta_y)
self.active_vector.myprop.update_myprops()
self.active_vector.myprop.update_image_texture()
self.Refresh()
return
self._center_x -= self._mouse_context.x - self._mouse_context_down.x
self._center_y -= self._mouse_context.y - self._mouse_context_down.y
self.setbounds(updatescale = False)
return
elif e.RightIsDown():
# Right mouse button is pressed
if self.action == ActionKind.SELECT_BC:
if self.active_vector is None:
self.end_action(_('None because no active vector'))
return
self.active_vector.myvertices=[wolfvertex(self._mouse_context_rightdown.x,self._mouse_context_rightdown.y),
wolfvertex(self._mouse_context_rightdown.x,self._mouse_context.y),
wolfvertex(self._mouse_context.x,self._mouse_context.y),
wolfvertex(self._mouse_context.x,self._mouse_context_rightdown.y),
wolfvertex(self._mouse_context_rightdown.x,self._mouse_context_rightdown.y)]
else:
# No mouse button is pressed
self._mouse_context_down = None
# ---
# ACTIONS
if self.action is not None:
if self.action in POLYGON_VERTEX_ACTIONS:
if self.active_vector is not None and self.active_vector.nbvertices > 0:
self.active_vector.myvertices[-1].x = _ctx.x
self.active_vector.myvertices[-1].y = _ctx.y
self.active_vector.on_vertices_changed()
# self.active_vector.reset_linestring()
# if self.active_vector.parentzone is not None:
# self.active_vector.parentzone.reset_listogl()
if self.action in (ActionKind.MODIFY_VERTICES, ActionKind.INSERT_VERTICES):
if self.active_vertex is not None:
if _ctx.shift:
# Shift key is pressed
# We move/Insert the vertex along the segment linking the first and last vertices of the active vector
ox = self.active_vector.myvertices[0].x
oy = self.active_vector.myvertices[0].y
dirx = self.active_vector.myvertices[-1].x - ox
diry = self.active_vector.myvertices[-1].y - oy
normdir = np.sqrt(dirx ** 2. + diry ** 2.)
if normdir == 0:
logging.warning(_('Cannot move vertex along the segment because the first and last vertices of the vector are at the same position'))
return
dirx /= normdir
diry /= normdir
vecx = _ctx.x_snap - ox
vecy = _ctx.y_snap - oy
# norm = np.sqrt(vecx ** 2. + vecy ** 2.)
self.active_vertex.x = ox + np.inner([dirx, diry], [vecx, vecy]) * dirx
self.active_vertex.y = oy + np.inner([dirx, diry], [vecx, vecy]) * diry
else:
self.active_vertex.x = _ctx.x_snap
self.active_vertex.y = _ctx.y_snap
self.active_vertex.limit2bounds(self.active_vector._mylimits)
# Invalidate all cached geometries (Shapely, VBO, display list)
self.active_vector._on_vertices_changed()
elif self.action in self._custom_motion_handlers:
self._custom_motion_handlers[self.action](self, _ctx)
elif self.action in ACTION_MOTION_HANDLERS:
ACTION_MOTION_HANDLERS[self.action](self, _ctx)
elif self.action == ActionKind.DISTANCE_ALONG_VECTOR:
if self._tmp_vector_distance is not None:
self._tmp_vector_distance.myvertices[-1].x = _ctx.x
self._tmp_vector_distance.myvertices[-1].y = _ctx.y
if self._tmp_vector_distance.nbvertices ==2:
self._tmp_vector_distance.myvertices[0].x = _ctx.x
self._tmp_vector_distance.myvertices[0].y = _ctx.y
self.Paint()
elif self.has_tracking_labels:
# No action active, but tracking labels need continuous refresh
self.Refresh()
# ---
if self.active_vector is not None:
if self.active_vector.myprop.textureimage is not None:
self.active_vector.myprop._reset_cached_offset()
# ---
# TOOLTIP
# Update the tooltip with the values of the active arrays and results at position x,y
self._update_tooltip_position()
self._update_tooltip()
[docs]
def On_Mouse_Right_Down(self, e: wx.MouseEvent):
"""
Event when the right button of the mouse is pressed.
We use this event to manage "action" set by others objects.
"""
self._mouse_context = self._wx_mouse_context(e)
# Palette overlay: right-click context menu
if self._palette_overlay is not None:
if self._palette_overlay.on_right_click(self._mouse_context.x_pixel, self._mouse_context.y_pixel):
return
if self.action is None:
if self.active_bc is not None:
self.start_action('select bc', _('Select a boundary condition'))
tmpvec = vector()
self.last_active_vector = self.active_vector
self.active_vector = tmpvec
tmpvec.add_vertex(wolfvertex(self._mouse_context.x, self._mouse_context.y))
self._mouse_context_rightdown = self._mouse_context
elif self.action in self._custom_rdown_handlers:
self._custom_rdown_handlers[self.action](self, self._mouse_context)
elif self.action in ACTION_RDOWN_HANDLERS:
ACTION_RDOWN_HANDLERS[self.action](self, self._mouse_context)
else:
self._mouse_context_rightdown = self._mouse_context
[docs]
def On_Mouse_Right_Up(self, e):
self._mouse_context = self._wx_mouse_context(e)
x, y = self._mouse_context.x, self._mouse_context.y
if self.active_bc is not None:
if self.action == ActionKind.SELECT_BC:
try:
minx = min(self._mouse_context_rightdown.x, x)
miny = min(self._mouse_context_rightdown.y, y)
maxx = max(self._mouse_context_rightdown.x, x)
maxy = max(self._mouse_context_rightdown.y, y)
if minx != maxx and maxy != miny:
self.active_bc.ray_tracing_numpy([[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy]], 'X')
self.active_bc.ray_tracing_numpy([[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy]], 'Y')
else:
self.active_bc.query_kdtree((x, y))
self.active_bc.update_selection()
self.Refresh()
self.active_vector = self.last_active_vector
self.end_action(_('End selection BC'))
except:
pass
[docs]
def On_Right_Double_Clicks(self, e):
self._endactions()
[docs]
def On_Left_Double_Clicks(self, e:wx.MouseEvent):
_ctx = self._wx_mouse_context(e)
# Palette overlay: double-click to input exact value
if self._palette_overlay is not None:
if self._palette_overlay.on_double_click(_ctx.x_pixel, _ctx.y_pixel):
return
if self._mouse_context_down is None:
# Manage the case when the user double click rapidly on the map
# self.mousedown can not be set in On_Mouse_Left_Down
self._mouse_context_down = self._wx_mouse_context(e)
self._center_x, self._center_y = self._mouse_context_down.x, self._mouse_context_down.y
self.oneclick = False
self.setbounds()
if _ctx.shift:
if self.active_array is not None:
if self.active_viewer3d is not None:
self.active_viewer3d.force_view(self._center_x, self._center_y, self.active_array.get_value(self._center_x, self._center_y))
self.Refresh()
if self.active_laz is not None:
if self.active_laz.viewer is not None:
self.active_laz.force_view(self._center_x, self._center_y, self.active_array.get_value(self._center_x, self._center_y))
else:
if self.active_viewer3d is not None:
self.active_viewer3d.force_view(self._center_x, self._center_y)
self.Refresh()
if self.active_laz is not None:
if self.active_laz.viewer is not None:
self.active_laz.force_view(self._center_x, self._center_y)
[docs]
def On_Mouse_Left_Down(self, e):
""" Event when the left button of the mouse is pressed """
_ctx = self._wx_mouse_context(e)
# Toolbar overlay click
if self._toolbar_overlay is not None:
if self._toolbar_overlay.on_click(_ctx.x_pixel, _ctx.y_pixel):
return
# Hillshade overlay drag start
if self._hillshade_overlay is not None:
hit = self._hillshade_overlay.hit_test(_ctx.x_pixel, _ctx.y_pixel)
if hit is not None:
self._hillshade_overlay._dragging = hit
self._hillshade_overlay.on_drag(_ctx.x_pixel, _ctx.y_pixel)
return
# Palette overlay drag start
if self._palette_overlay is not None:
hit = self._palette_overlay.hit_test(_ctx.x_pixel, _ctx.y_pixel)
if hit is not None:
self._palette_overlay._dragging = hit
self._palette_overlay.on_drag(_ctx.x_pixel, _ctx.y_pixel)
return
x, y = _ctx.x, _ctx.y
self._mouse_context_down = self._wx_mouse_context(e)
# Sculpt / profile left-click handling
if self._sculpt.on_left_down(_ctx):
return
if self.action == ActionKind.TRANSFORM_ASSET_BOUNDS:
if self._assets.on_left_down(x, y):
return
# Check clip sliders on all visible arrays
if self._try_start_clip_slider_drag(x, y):
return
# Plugin left-down hook
if self.action in self._custom_ldown_handlers:
self._custom_ldown_handlers[self.action](self, _ctx)
[docs]
def On_Mouse_Left_Up(self, e):
""" Event when the left button of the mouse is released """
_ctx = self._wx_mouse_context(e)
# End hillshade overlay drag
if self._hillshade_overlay is not None and self._hillshade_overlay._dragging is not None:
self._hillshade_overlay._dragging = None
return
# End palette overlay drag
if self._palette_overlay is not None and self._palette_overlay._dragging is not None:
self._palette_overlay._dragging = None
return
self._end_clip_slider_drag()
x, y = _ctx.x, _ctx.y
self._assets.on_left_up(x, y)
self._update_asset_transform_cursor(x, y)
# ----------------------------------------------------------------
# Clip-slider interaction helpers
# ----------------------------------------------------------------
[docs]
_active_clip_slider = None # currently dragged ClipSlider, if any
[docs]
def _iter_clip_sliders(self):
"""Yield all (clip_zone, slider) from checked arrays, 2D results and vectors."""
from .array_core.clipping import ClipSlider, ClipZoneMixin
try:
for dtype in (draw_type.ARRAYS, draw_type.RES2D, draw_type.VECTORS):
for obj in self.iterator_over_objects(dtype, checked_state=True):
if isinstance(obj, ClipZoneMixin):
for cz in obj.clip_zones:
if cz.active:
for s in cz.sliders:
yield cz, s
except Exception:
return
[docs]
def _get_nearest_clip_slider(self, world_x: float, world_y: float):
"""Return the nearest slider label if cursor is close, else ''."""
for _cz, slider in self._iter_clip_sliders():
if slider.hit_test(world_x, world_y, self.sx, self.sy):
return slider.label
return ''
[docs]
def _try_start_clip_slider_drag(self, world_x: float, world_y: float) -> bool:
"""If a slider bar is under the cursor, start dragging it.
:return: ``True`` if a drag was started (caller should skip normal handling).
"""
for _cz, slider in self._iter_clip_sliders():
if slider.hit_test(world_x, world_y, self.sx, self.sy):
slider.start_drag()
self._active_clip_slider = slider
return True
return False
[docs]
def _update_clip_slider_drag(self, world_x: float, world_y: float) -> bool:
"""Update the active slider position during a drag.
:return: ``True`` if a drag is in progress.
"""
s = self._active_clip_slider
if s is not None and s.is_dragging:
s.update_drag(world_x, world_y)
return True
return False
[docs]
def _end_clip_slider_drag(self):
"""Finish any active slider drag."""
s = self._active_clip_slider
if s is not None and s.is_dragging:
s.end_drag()
self._active_clip_slider = None
self.Refresh()
# ----------------------------------------------------------------
# Asset transform interaction helpers
# ----------------------------------------------------------------
@staticmethod
[docs]
def _compute_asset_handles(bounds):
return AssetManager._compute_asset_handles(bounds)
[docs]
def _asset_handle_hit_test(self, x: float, y: float):
return self._assets._asset_handle_hit_test(x, y)
@staticmethod
[docs]
def _cursor_for_asset_handle(handle):
return AssetManager._cursor_for_asset_handle(handle)
@staticmethod
[docs]
def _nice_step(raw_step: float) -> float:
return AssetManager._nice_step(raw_step)
[docs]
def _ensure_snap_grid_origin(self) -> tuple[float, float]:
return self._assets._ensure_snap_grid_origin()
[docs]
def _adaptive_snap_step_from_span(self, span: float,
target_divisions: float = 20.0) -> float:
return self._assets._adaptive_snap_step_from_span(span, target_divisions)
@staticmethod
[docs]
def _snap_value(v: float, step: float, origin: float = 0.0) -> float:
return AssetManager._snap_value(v, step, origin)
[docs]
def _is_heavy_gl_action_active(self) -> bool:
"""Return True when an interaction is likely to stress OpenGL drawing."""
if self.action is None:
return False
if self.action in HEAVY_GL_ACTIONS:
return True
# Vector point-add workflows covered by SELECT_BY_VECTOR_ACTIONS.
if self.action in SELECT_BY_VECTOR_ACTIONS:
return True
return False
[docs]
def _plot_mouse_xy_overlay(self) -> None:
"""Draw current world XY near the cursor directly on the OpenGL canvas."""
if not self._is_heavy_gl_action_active():
return
x, y = self._mouse_context.x, self._mouse_context.y
width, height = self.canvas.GetSize()
if width <= 0 or height <= 0:
return
mvp = self.get_ortho_mvp_c_contiguous()
if mvp is None:
return
if self._overlay_xy_text_renderer is None:
self._overlay_xy_text_renderer = TextRenderer2D.get_instance()
font_name = self.overlay_xy_font_name
font_size = self.overlay_xy_font_size
try:
atlas = GlyphAtlas.get(font_name)
except Exception:
atlas = GlyphAtlas.get('arial.ttf')
font_name = 'arial.ttf'
text = f'X: {x:.3f}\nY: {y:.3f}'
# Measure width only (for horizontal room check).
text_w_px, __ = measure_text(text, atlas, scale=float(font_size), line_spacing=1.2)
text_w_px = max(float(text_w_px), 80.0)
# Robust placement in screen space (independent of OS cursor shape),
# then convert back to world coordinates for text rendering.
px = int(width * 0.5)
py = int(height * 0.5)
try:
px = int(self._mouse_context.x_pixel)
py = int(self._mouse_context.y_pixel)
except Exception:
try:
px = int(self._mouse_context.x)
py = int(self._mouse_context.y)
except Exception:
pass
margin = 8
gap_px = 22
prefer_left_side = px <= (width * 0.5)
has_room_right = (px + gap_px + text_w_px + margin) <= width
has_room_left = (px - gap_px - text_w_px - margin) >= 0
if prefer_left_side and has_room_right:
alignment = 'left'
anchor_px = px + gap_px
elif (not prefer_left_side) and has_room_left:
alignment = 'right'
anchor_px = px - gap_px
elif has_room_right:
alignment = 'left'
anchor_px = px + gap_px
else:
alignment = 'right'
anchor_px = px - gap_px
anchor_px = max(margin, min(float(anchor_px), float(width - margin)))
# Vertical: use vertical_alignment so no height measurement is needed.
# 'top' → anchor is the top edge, text flows downward.
# 'bottom' → anchor is the bottom edge, text flows upward.
y_gap_px = 18
prefer_below = py < (height * 0.55)
if prefer_below:
anchor_screen_y = py + y_gap_px
vert_align = 'top'
else:
anchor_screen_y = py - y_gap_px
vert_align = 'bottom'
dx = float(self.xmax - self.xmin)
dy = float(self.ymax - self.ymin)
tx = float(self.xmin) + (float(anchor_px) / float(width)) * dx
ty = float(self.ymax) - (float(anchor_screen_y) / float(height)) * dy
try:
self._overlay_xy_text_renderer.draw_text(
text,
tx,
ty,
mvp,
(int(width), int(height)),
font_name=font_name,
font_size=font_size,
color=(0.0, 0.0, 0.0, 1.0),
size_in_pixels=True,
alignment=alignment,
vertical_alignment=vert_align,
glow_enabled=False,
)
except Exception as ex:
# Keep interaction responsive even if text rendering fails once.
logging.warning(_('Error in XY overlay text rendering: {}').format(ex))
[docs]
def _plot_distance_overlay(self) -> None:
"""Draw cumulative distance near the cursor, stacked below/above the XY overlay."""
if self._tmp_vector_distance is None:
return
if self.action != 'distance along vector':
return
width, height = self.canvas.GetSize()
if width <= 0 or height <= 0:
return
mvp = self.get_ortho_mvp_c_contiguous()
if mvp is None:
return
if self._overlay_xy_text_renderer is None:
self._overlay_xy_text_renderer = TextRenderer2D.get_instance()
font_name = self.overlay_xy_font_name
font_size = self.overlay_xy_font_size
try:
atlas = GlyphAtlas.get(font_name)
except Exception:
atlas = GlyphAtlas.get('arial.ttf')
font_name = 'arial.ttf'
# Build distance text (area formatting preserved from user edits)
self._tmp_vector_distance.update_lengths()
length = self._tmp_vector_distance.length2D
text = f'L: {length:.3f} m'
if self._tmp_vector_distance.nbvertices > 4:
try:
_polygon = self._tmp_vector_distance.polygon
_area = _polygon.area
text += f'\nA: {_area:.3f} m2'
# add hectares and km2 for large areas, using the same formatting as in the properties panel
if _area >= 10000:
text += f'\nA: {_area / 10000.:.6f} ha'
if _area >= 1e6:
text += f'\nA: {_area / 1e6:.9f} km2'
except Exception:
pass
# Measure width only (for horizontal room check).
dist_w_px, __ = measure_text(text, atlas, scale=float(font_size), line_spacing=1.2)
dist_w_px = max(float(dist_w_px), 80.0)
# Replicate cursor pixel position from _plot_mouse_xy_overlay
px = int(width * 0.5)
py = int(height * 0.5)
try:
px = int(self._mouse_context.x_pixel)
py = int(self._mouse_context.y_pixel)
except Exception:
try:
px = int(self._mouse_context.x)
py = int(self._mouse_context.y)
except Exception:
pass
margin = 8
gap_px = 22
# Horizontal placement: same side as XY overlay
prefer_left_side = px <= (width * 0.5)
has_room_right = (px + gap_px + dist_w_px + margin) <= width
has_room_left = (px - gap_px - dist_w_px - margin) >= 0
if prefer_left_side and has_room_right:
alignment = 'left'
anchor_px = px + gap_px
elif (not prefer_left_side) and has_room_left:
alignment = 'right'
anchor_px = px - gap_px
elif has_room_right:
alignment = 'left'
anchor_px = px + gap_px
else:
alignment = 'right'
anchor_px = px - gap_px
anchor_px = max(margin, min(float(anchor_px), float(width - margin)))
# Vertical: opposite side of XY overlay, using vertical_alignment.
# XY uses 'top' (below cursor) when prefer_below, 'bottom' (above) otherwise.
# Distance uses the complementary side so the two blocks never overlap.
y_gap_px = 18
prefer_below = py < (height * 0.55)
if prefer_below:
# XY is below → distance goes above cursor
anchor_screen_y = py - y_gap_px
vert_align = 'bottom'
else:
# XY is above → distance goes below cursor
anchor_screen_y = py + y_gap_px
vert_align = 'top'
dx = float(self.xmax - self.xmin)
dy = float(self.ymax - self.ymin)
tx = float(self.xmin) + (float(anchor_px) / float(width)) * dx
ty = float(self.ymax) - (float(anchor_screen_y) / float(height)) * dy
try:
self._overlay_xy_text_renderer.draw_text(
text,
tx,
ty,
mvp,
(int(width), int(height)),
font_name=font_name,
font_size=font_size,
color=(0.0, 0.0, 0.8, 1.0),
size_in_pixels=True,
alignment=alignment,
vertical_alignment=vert_align,
glow_enabled=False,
)
except Exception as ex:
logging.warning(_('Error in distance overlay text rendering: {}').format(ex))
[docs]
def _set_active_bc(self):
"""Search and activate BCManager according to active_array"""
if self.active_bc is not None:
if self.active_array != self.active_bc.linked_array:
# it is not the good one -> Hide
self.active_bc.Hide()
else:
return
# searching if bcmanager is attached to active_array
self.active_bc = None
for curbc in self.mybc:
if self.active_array == curbc.linked_array:
self.active_bc = curbc
self.active_bc.Show()
return
# Default warning messages used by _check_active when the caller passes None.
# Stored untranslated; _() is applied at call time.
[docs]
_ACTIVE_DEFAULT_MSG: dict = {
'active_array': _("No active array -- Please activate an array first"),
'active_vector': _("No active vector -- Please activate a vector first"),
'active_cs': _("No active cross section -- Please activate one first"),
'active_cloud': _("No active cloud -- Please activate a cloud first"),
'active_tri': _("No active triangulation -- Please activate one first"),
'active_laz': _("No active LAZ data -- Please activate one first"),
'active_zones': _("No active zones -- Please activate one first"),
'active_res2d': _("No active 2D result -- Please activate one first"),
'active_bc': _("No active boundary condition manager -- Please activate one first"),
}
[docs]
def _check_active(self, **slot_messages: 'str | None') -> bool:
"""Vérifie que chaque slot actif nommé est non-None.
Log un warning pour chaque slot manquant et retourne False si au moins
un slot est absent. Retourne True si tous sont présents.
Passer ``None`` comme message utilise le texte par défaut de
:attr:`_ACTIVE_DEFAULT_MSG`.
Usage::
# message explicite
if not self._check_active(
active_array=_('No active array -- Please activate one first'),
):
return
# message par défaut (None)
if not self._check_active(active_array=None, active_vector=None):
return
"""
ok = True
for slot_name, msg in slot_messages.items():
if getattr(self, slot_name) is None:
effective = msg if msg is not None else _(
self._ACTIVE_DEFAULT_MSG.get(slot_name, 'No ' + slot_name)
)
logging.warning(effective)
ok = False
return ok
[docs]
def set_statusbar_text(self, txt:str):
""" Set the status bar text """
self.StatusBar.SetStatusText(txt)
[docs]
def set_label_selecteditem(self, nameitem:str):
""" Set the label of the selected item in the tree list """
self._lbl_selecteditem.SetLabel(nameitem)
[docs]
def get_label_selecteditem(self):
""" Get the label of the selected item in the tree list """
return self._lbl_selecteditem.GetLabel()
[docs]
def OnActivateTreeElem(self, e): #:dataview.TreeListEvent ):
""" Activate the selected item in the tree list """
curzones: Zones
curzone: zone
curvect: vector
myitem = e.GetItem()
ctrl = wx.GetKeyState(wx.WXK_CONTROL)
alt = wx.GetKeyState(wx.WXK_ALT)
myparent = self.treelist.GetItemParent(myitem)
check = self.treelist.GetCheckedState(myitem)
nameparent = self.treelist.GetItemText(myparent).lower()
nameitem = self.treelist.GetItemText(myitem).lower()
myobj = self.treelist.GetItemData(myitem)
self.selected_object = myobj
self.set_label_selecteditem(_('Active : ') + nameitem)
#FIXME : To generalize using draw_type
if type(myobj) == Zones:
self.active_zones = myobj
if ctrl:
myobj.show_properties()
elif type(myobj) == PictureCollection:
self.active_picturecollection = myobj
if ctrl:
myobj.show_properties()
elif type(myobj) == Wolf_LAZ_Data:
self.active_laz = myobj
if ctrl:
myobj.show_properties()
elif type(myobj) == Bridge:
self.active_bridge = myobj
if ctrl:
myobj.show_properties()
elif type(myobj) == Weir:
self.active_weir = myobj
if ctrl:
myobj.show_properties()
elif isinstance(myobj, PlansTerrier):
self.active_landmap = myobj
elif isinstance(myobj, Particularites | Enquetes | Ouvrages | Profils):
self.active_picturecollection = myobj
elif type(myobj) == hydrometry_wolfgui:
if ctrl:
myobj.show_properties()
elif type(myobj) in [Picc_data, Cadaster_data]:
if ctrl:
myobj.show_properties()
elif type(myobj) == Particle_system:
if ctrl:
myobj.show_properties()
elif type(myobj) == Tiles:
self.active_tile= myobj
elif issubclass(type(myobj), WolfArray):
if ctrl:
myobj.show_properties()
# myobj.myops.SetTitle(_('Operations on array: ')+myobj.idx)
# myobj.myops.Show()
logging.info(_('Activating array : ' + nameitem))
self.active_array = myobj
# Refresh hillshade panel when active array changes
self._refresh_hillshade_panel_for_active()
# If BC maneger is attached to the array, we activate it
self._set_active_bc()
#Print info in the status bar
txt = 'Dx : {:.4f} ; Dy : {:.4f}'.format(self.active_array.dx, self.active_array.dy)
txt += ' ; Xmin : {:.4f} ; Ymin : {:.4f}'.format(self.active_array.origx, self.active_array.origy)
txt += ' ; Xmax : {:.4f} ; Ymax : {:.4f}'.format(self.active_array.origx + self.active_array.dx * float(self.active_array.nbx),
self.active_array.origy + self.active_array.dy * float(self.active_array.nby))
txt += ' ; Nx : {:d} ; Ny : {:d}'.format(self.active_array.nbx, self.active_array.nby)
if self.active_array.nb_blocks > 0:
txt += ' ; Nb blocks : {:d}'.format(self.active_array.nb_blocks)
txt += ' ; Type : ' + self.active_array.dtype_str
self.set_statusbar_text(txt)
elif type(myobj) in [WolfViews]:
logging.info(_('Activating view : ' + nameitem))
self.active_view = myobj
elif isinstance(myobj, cloud_vertices):
self.active_cloud = myobj
logging.info(_('Activating cloud of vertices : ' + nameitem))
if ctrl:
myobj.show_properties()
elif isinstance(myobj, cloud_of_clouds):
self.active_cloud = None
logging.info(_('Set Active cloud to None because it is a cloud of clouds : ' + nameitem))
if ctrl:
myobj.show_properties()
elif type(myobj) == crosssections:
if ctrl:
myobj.showstructure()
logging.info(_('Activating cross sections : ' + nameitem))
self.active_cs = myobj
elif type(myobj) == Triangulation:
self.active_tri = myobj
elif type(myobj) == Wolfresults_2D:
logging.info(_('Activating Wolf2d results : ' + nameitem))
self.active_res2d = myobj
if ctrl:
myobj.show_properties()
if alt:
if not self._dialogs.ask_yes_no(_('Do you want to open the 2D model?'), style=DialogStyles.YES_NO_DEFAULT_NO, parent=self):
return
from .PyGui import Wolf2DModel
mywolf = Wolf2DModel(dir=os.path.dirname(self.active_res2d.filenamegen), splash=False)
elif type(myobj) == wolfres2DGPU:
logging.info(_('Activating Wolf2d results : ' + nameitem))
self.active_res2d = myobj
if ctrl:
myobj.show_properties()
elif type(myobj) == Drowning_victim_Viewer:
logging.info(_('Activating Drowning victim event : ' + nameitem))
self.active_drowning = myobj
elif WOLFPYDIKE_AVAILABLE:
if type(myobj) == DikeWolf:
logging.info(_('Activating DikeWolf : ' + nameitem))
self.active_dike = myobj
if myobj.injector is not None:
self.active_injector = myobj.injector
logging.info(_('Activating InjectorDike : ' + nameitem))
if ctrl:
myobj.show_properties()
elif type(myobj) == InjectorDike:
logging.info(_('Activating InjectorDike : ' + nameitem))
self.active_injector = myobj
if ctrl:
myobj.show_properties()
[docs]
def SetActiveCloud(self, cloud: cloud_vertices | None):
""" Set the active cloud of vertices, and update the tooltip if needed """
self.active_cloud = cloud
logging.info(_('Set active cloud of vertices : ' + str(cloud)))
[docs]
def Autoscale(self, update_backfore=True):
""" Redimensionnement de la fenêtre pour afficher tous les objets """
self.findminmax()
self.width = self.xmax - self.xmin
self.height = self.ymax - self.ymin
centerx = self.xmin + self.width / 2.
centery = self.ymin + self.height / 2.
iwidth = self.width * self.sx
iheight = self.height * self.sy
width, height = self.canvas.GetSize()
if iwidth == 0 or iheight == 0:
logging.warning(_('Width or height of the canvas is null -- Please check the "findminmax" routine in "Autoscale" !'))
iwidth = 1
iheight = 1
sx = float(width) / float(iwidth)
sy = float(height) / float(iheight)
if sx == 0 or sy == 0:
logging.error(_('At least one scale factor is null -- Please check the "Autoscale" routine !'))
sx = 1.
sy = 1.
if sx > sy:
self.xmax = self.xmin + self.width * sx / sy
self.width = self.xmax - self.xmin
else:
self.ymax = self.ymin + self.height * sy / sx
self.height = self.ymax - self.ymin
self._center_x = centerx
self._center_y = centery
if update_backfore:
# dessin du background
for obj in self.iterator_over_objects(draw_type.WMSBACK):
obj.reload()
# dessin du foreground
for obj in self.iterator_over_objects(draw_type.WMSFORE):
obj.reload()
self.setbounds()
[docs]
def print_About(self):
""" Print the About window """
from .apps.version import WolfVersion
version = WolfVersion()
self._dialogs.show_message(_('Wolf - Version {}\n\n'.format(str(version))) + _('Developed by : ') + 'HECE ULiège\n' + _('Contact : pierre.archambeau@uliege.be'), _('About'), wx.OK | wx.ICON_INFORMATION)
[docs]
def check_for_updates(self):
""" Check for updates """
from .apps.version import WolfVersion
import requests
import importlib.metadata
# check_gpu = False
# try:
# import wolfgpu
# check_gpu = True
# except:
# pass
msg = ''
current_version = str(WolfVersion())
package_name = "wolfhece"
try:
available_version = requests.get(f"https://pypi.org/pypi/{package_name}/json").json()["info"]["version"]
if available_version > current_version:
msg += _("A new version is available: {}\n\nYour version is {}\n\nIf you want to upgrade, 'pip install wolfhece --upgrade' from your Python environment.").format(available_version, current_version)
else:
msg += _("You have the latest version.")
except Exception as e:
logging.error("Package not found on PyPI. -- {}".format(e))
# if check_gpu:
# # find the version
# package_name = "wolfgpu"
# current_version = importlib.metadata.version(package_name)
# url_wolfgpu = "https://gitlab.uliege.be/api/v4/projects/4180/packages/pypi/simple/" #+ package_name
# try:
# response = requests.get(url_wolfgpu).json()
# update_wolfgpu = False
# #parcourir toutes le entrées et ne conserver que la version la plus récente
# for one in response:
# one["version"] > current_version
# update_wolfgpu = True
# if update_wolfgpu:
# msg += '\n'
# msg += _("A new version of WolfGPU is available: {}.{}.{}, please update it.").format(current_version[0], current_version[1], current_version[2])
# else:
# msg += '\n'
# msg+= _("You have the latest version of WolfGPU.")
# except Exception as e:
# logging.error("Package not found on PyPI. -- {}".format(e))
msg+= '\n\n'
msg+= _('If you use wolfgpu, please check the GPU version independently.')
self._dialogs.show_message(msg, _("Upgrade"), wx.OK | wx.ICON_INFORMATION)
[docs]
def print_shortcuts(self, inframe:bool = None):
""" Print the list of shortcuts into logging """
# shortcuts = "F1 : mise à jour du dernier pas de résultat\n \
# F2 : mise à jour du résultat pas suivant\n \
# F4 : mise à jour du particle system au pas suivant\n \
# Shift+F2 : mise à jour du résultat pas précédent\n \
# Shift+F4 : mise à jour du particle system au pas précédent\n \
# CTRL+F2 : choix du pas\n \
# CTRL+F4 : choix du pas (particle system)\n \
# CTRL+Shift+F2 : choix du pas sur base du temps\n \
# CTRL+Shift+F4 : choix du pas sur base du temps (particle system)\n \
# F5 : autoscale\n \
# F7 : refresh\n \
# F8 : Zoom on Whole Walonia\n \
# F9 : sélection de toutes les mailles dans la matrice courante\n \
# F11 : sélection sur matrice courante\n \
# F12 : opération sur matrice courante\n \
# \n \
# ESPACE : pause/resume animation\n \
# \n \
# Z : zoom avant\n \
# z : zoom artrière\n \
# Flèches : déplacements latéraux\n \
# P : sélection de profil\n \
# 1,2 : Transfert de la sélection de la amtrice courante vers le dictionnaire\n \
# F, CTRL+F : recherche de la polyligne dans la zone courante ou dans toutes les zones\n \
# i : interpolation2D sur base de la sélection sur la matrice courante\n \
# +,- (numpad) : augmente ou diminue la taille des flèches de resultats 2D\n \
# \n \
# o, O : Gestion de la transparence de la matrice courante\n \
# CTRL+o, CTRL+O : Gestion de la transparence du résultat courant\n \
# \n \
# !! ACTIONs !!\n \
# N : sélection noeud par noeud de la matrice courante\n \
# b, B : sélection par vecteur de la matrice courante - trace du vecteur\n \
# v, V : sélection par vecteur de la matrice courante - zone intérieure\n \
# r : reset de la sélection de la matrice courante\n \
# R : reset de toutes les sélections de la matrice courante\n \
# P : sélection de la section transversale par click souris\n \
# D : calcule de distance le long d'un vecteur temporaire\n \
# \n \
# RETURN : end current action (cf aussi double clicks droit 'OnRDClick')\n \
# DELETE : remove item\n \
# \n \
# CTRL+Q : Quit application\n \
# CTRL+U : Import GLTF/GLB\n \
# CTRL+C : Set copy source\n \
# CTRL+V : Paste selected values\n \
# CTRL+ALT+V ou ALTGr+V : Paste/Recopy selection\n \
# CTRL+L : chargement d'une matrice sur base du nom de fichier de la tile\n \
# \n \
# ALT+C : Copy image"
# ALT+SHIFT+C : Copy images from multiviewers \
# CTRL+ALT+SHIFT+C : Copy images from all arrays as independent image \
groups = ['Results', 'Particle system', 'Drawing', 'Arrays', 'Cross sections', 'Zones', 'Action', 'Tree', 'Tiles', 'GLTF/GLB', 'App']
shortcuts = {'F1': _('Results : read the last step'),
'F2': _('Results : read the next step'),
'Shift+F2': _('Results : read the previous step'),
'CTRL+F2': _('Results : choose the step'),
'CTRL+Shift+F2': _('Results : choose the step based on time'),
'+,- (numpad)': _('Results : increase or decrease the size of 2D result arrows'),
'F4': _('Particle system : update to the next step'),
'Shift+F4': _('Particle system : update to the previous step'),
'CTRL+F4': _('Particle system : choose the step'),
'CTRL+Shift+F4': _('Particle system : choose the step based on time'),
'SPACE': _('Particle system : pause/resume animation'),
'LMB double clicks': _('Drawing : center the view on the clicked point -- future zoom will be centered on the point'),
'LMB and move': _('Drawing : translate the view'),
'Mouse wheel click and move': _('Drawing : translate the view'),
'Mouse wheel': _('Drawing : zoom in/out - centered on the middle of the canvas'),
'Mouse wheel + Space Bar': _('Drawing : zoom in/out - centered on the mouse position'),
'z, Z': _('Drawing : zoom out/in - centered on the middle of the canvas'),
'Touchpad 2 fingers': _('Drawing : zoom in/out - centered on the middle of the canvas'),
'CTRL + z': _('Drawing : Autoscale only on active array'),
'CTRL + Z': _('Drawing : Autoscale only on active vector'),
'F5': _('Drawing : autoscale'),
'F7': _('Drawing : refresh'),
'F8': _('Drawing : zoom on whole Walonia'),
'Arrows': _('Drawing : lateral movements'),
'c or C': _('Drawing : copy canvas to Clipboard wo axes'),
'ALT+C': _('Drawing : copy canvas to Clipboard as Matplotlib image'),
'ALT+SHIFT+C': _('Drawing : copy canvas to Clipboard as Matplotlib image with axes - multiviewers'),
'CTRL+ALT+SHIFT+C': _('Drawing : copy canvas to Clipboard as Matplotlib image with axes - all arrays one by one'),
'd or D': _('Drawing : calculate distance along a temporary vector'),
'CTRL+o': _('Results : increase transparency of the current result'),
'CTRL+O': _('Results : decrease transparency of the current result'),
'o': _('Arrays : increase transparency of the current array'),
'O': _('Arrays : decrease transparency of the current array'),
'F9': _('Arrays : select all cells'),
'F10': _('Automatic background - switch mode'),
'F11': _('Arrays : select by criteria'),
'F12': _('Arrays : operations'),
'n or N': _('Arrays : node-by-node selection'),
'b or B': _('Arrays : temporary/active vector selection - along polyline'),
'v or V': _('Arrays : temporary/active vector selection - inside polygon'),
'r': _('Arrays : reset the selection'),
'R': _('Arrays : reset the selection and the associated dictionnary'),
'1,2...9': _('Arrays : transfer the selection to the associated dictionary - key 1 to 9'),
'>, <' : _('Arrays : dilate/erode the selection - cross-shaped neighbours'),
'CTRL+>, CTRL+<': _('Arrays : dilate/erode the selection unselecting the values inside the contour - cross-shaped neighbours'),
'i': _('Arrays : 2D interpolation based on the selection on the current matrix'),
'CTRL+C': _('Arrays : Set copy source and current selection to clipboard as string'),
'CTRL+X': _('Arrays : Crop the active array using the active vector and make a copy'),
'CTRL+V': _('Arrays : paste selected values'),
'CTRL+ALT+C or ALTGr+C': _('Arrays : Set copy source and current selection to clipboard as script'),
'CTRL+ALT+X or ALTGr+X': _('Arrays : Crop the active array using the active vector without masking the values outside the vector'),
'CTRL+ALT+V or ALTGr+V': _('Arrays : paste selection to active array'),
'p or P': _('Cross sections : Pick a profile/cross section'),
'f or F, CTRL+F': _('Zones : search for the polyline in the current zone or in all zones'),
'RETURN': _('Action : End the current action (see also right double-click -- OnRDClick)'),
'Press and Hold CTRL': _('Action : Data Frame follows the mouse cursor'),
'DELETE': _('Tree : Remove item'),
'CTRL+L': _('Tiles: Pick a tile by clicking on it'),
'CTRL+U': _('GLTF/GLB : import/update GLTF/GLB'),
'CTRL+Q': _('App : Quit application'),}
def gettxt():
txt = ''
for curgroup in groups:
txt += curgroup + '\n'
for curkey, curval in shortcuts.items():
if curgroup in curval:
txt += '\t' + curkey + ' : ' + curval.split(':')[1] + '\n'
txt += '\n'
return txt
logging.info(gettxt())
if inframe :
frame = wx.Frame(None, -1, _('Shortcuts'), size=(500, 800))
# panel = wx.Panel(frame, -1)
sizer = wx.BoxSizer(wx.VERTICAL)
multiline = wx.TextCtrl(frame, -1, '', style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2)
multiline.SetValue(gettxt())
sizer.Add(multiline, 1, wx.EXPAND)
frame.SetSizer(sizer)
icon = wx.Icon()
icon_path = Path(__file__).parent / "apps/wolf.ico"
icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY))
frame.SetIcon(icon)
frame.SetAutoLayout(True)
frame.Layout()
frame.Show()
# ****************** >>
# ACTIONS
[docs]
def msg_action(self, which:int = 0):
""" Message to end action
:param which: 0 to start action, 1 to end action
"""
if which == 0:
self.set_statusbar_text(_('Action in progress... -- To quit, press "RETURN" or "double clicks RIGHT" or press "ESC"'))
else:
self.set_statusbar_text('')
[docs]
def register_action(self,
action_id: str,
*,
rdown_handler: '_MouseHandler | None' = None,
motion_handler: '_MouseHandler | None' = None,
ldown_handler: '_LeftDownHandler | None' = None,
key_handler: '_KeyHandler | None' = None,
paint_handler: '_PaintHandler | None' = None,
overload: bool = False,
) -> None:
"""Register custom event handlers for *action_id* on **this instance only**.
The handlers are looked up before the global dispatch tables, so a plugin
can shadow or extend any built-in action without modifying core files.
Typical usage from a Jupyter notebook::
from wolfhece._viewer_plugin_handlers import MouseContext, KeyboardSnapshot
def my_rdown(viewer, ctx: MouseContext) -> None:
print(f"Click at ({ctx.x:.2f}, {ctx.y:.2f})")
def my_key(viewer, kb: KeyboardSnapshot) -> bool:
if kb.key_code == ord('C') and kb.ctrl:
my_data.clear()
return True # consume the event
return False
def my_paint(viewer) -> None:
pass # raw OpenGL drawing here
viewer.register_action('my action',
rdown_handler=my_rdown,
key_handler=my_key,
paint_handler=my_paint)
viewer.start_action('my action', 'Click on the map…')
:param action_id: Lowercase string that identifies the action.
:param rdown_handler: ``(viewer, MouseContext) -> None`` — right mouse-button press.
:param motion_handler: ``(viewer, MouseContext) -> None`` — mouse motion.
:param ldown_handler: ``(viewer, MouseContext) -> None`` — left mouse-button press.
:param key_handler: ``(viewer, KeyboardSnapshot) -> bool`` — key press.
Return ``True`` to consume the event (prevents default).
:param paint_handler: ``(viewer) -> None`` — raw OpenGL drawing hook,
called after all data layers, before UI overlays.
:param overload: When ``True``, the current handler for each slot is saved
and automatically restored when :meth:`unregister_action` is
called. A warning is emitted in both cases whenever an
existing handler is replaced.
"""
key = action_id.lower()
_slots: list[tuple[str, object, dict]] = [
('rdown', rdown_handler, self._custom_rdown_handlers),
('motion', motion_handler, self._custom_motion_handlers),
('ldown', ldown_handler, self._custom_ldown_handlers),
('key', key_handler, self._custom_key_handlers),
('paint', paint_handler, self._custom_paint_handlers),
]
for slot_name, new_handler, table in _slots:
if new_handler is None:
continue
if key in table:
existing = table[key]
existing_name = getattr(existing, '__name__', repr(existing))
if overload:
# Save the displaced handler only once (first overload wins).
saved = self._saved_handlers.setdefault(key, {})
if slot_name not in saved:
saved[slot_name] = existing
logging.warning(
"register_action: '%s' — %s handler overloaded "
"(previous '%s' saved and will be restored on unregister).",
action_id, slot_name, existing_name,
)
else:
logging.warning(
"register_action: '%s' — %s handler already registered "
"(previous '%s' will be lost; pass overload=True to save it).",
action_id, slot_name, existing_name,
)
table[key] = new_handler
[docs]
def unregister_action(self, action_id: str) -> None:
"""Remove a previously registered custom action from this instance.
If the action was registered with ``overload=True``, the previously
displaced handlers are automatically restored.
:param action_id: The action id passed to :meth:`register_action`.
"""
key = action_id.lower()
saved = self._saved_handlers.pop(key, {})
_slot_tables: list[tuple[str, dict]] = [
('rdown', self._custom_rdown_handlers),
('motion', self._custom_motion_handlers),
('ldown', self._custom_ldown_handlers),
('key', self._custom_key_handlers),
('paint', self._custom_paint_handlers),
]
for slot_name, table in _slot_tables:
if slot_name in saved:
restored = saved[slot_name]
if restored is None:
table.pop(key, None)
else:
table[key] = restored
logging.info(
"unregister_action: '%s' — %s handler restored to '%s'.",
action_id, slot_name, getattr(restored, '__name__', repr(restored)),
)
else:
table.pop(key, None)
[docs]
def start_action(self, action: 'str | ActionKind', message: str = ''):
""" Message to start action
:param action: name of the action (used to manage the end of the action in "end_action" method)
:param message: message to print in the log (if empty, the action name will be printed)
"""
assert isinstance(action, str | ActionKind), 'action must be a string or ActionKind enum'
if action == '':
self.action = None
else:
lower = action.lower()
try:
self.action = ActionKind(lower)
except ValueError:
self.action = lower
# Reset brush-refresh coalescing flag for the new action session.
self._sculpt.brush_refresh_pending = False
logging.info(_('ACTION : ') + _(message) if message != '' else _('ACTION : ') + _(action))
self.msg_action(0)
[docs]
def end_action(self, message:str=''):
""" Message to end action
:param message: message to print in the log (if empty, "End of action" will be printed)
"""
_prev_action = self.action
self.action = None
self.active_vertex = None
self.active_cloud_vertex_id = None
self._sculpt.on_end_action(_prev_action)
self._assets.on_end_action()
logging.info(_('ACTION : ') + _(message) if message != '' else _('ACTION : End of action') )
self.msg_action(1)
[docs]
def _endactions(self):
"""
End of actions
Call when the user double click on the right button of the mouse or press return.
Depending on the action, the method will call differnt routines and refresh the figure.
Each action must call self.end_action() to nullify the action and print a message.
"""
if self.action is not None:
locaction = self.action
if self.action in SELECT_BY_VECTOR_ACTIONS:
inside_under = self.action in (ActionKind.SELECT_BY_VECTOR_INSIDE, ActionKind.SELECT_BY_TMP_VECTOR_INSIDE)
outside_under = self.action == ActionKind.SELECT_BY_VECTOR_OUTSIDE
self.end_action(_('End of vector selection'))
self.active_vector.myvertices.pop(-1)
if inside_under:
self.active_vector.close_force()
self.active_array.SelectionData.select_insidepoly(self.active_vector)
elif outside_under:
self.active_vector.close_force()
self.active_array.SelectionData.select_outsidepoly(self.active_vector)
else:
self.active_array.SelectionData.select_underpoly(self.active_vector)
if self.action in (ActionKind.SELECT_BY_TMP_VECTOR_INSIDE, ActionKind.SELECT_BY_TMP_VECTOR_ALONG):
# we must reset the temporary vector
self.active_vector.reset()
elif locaction == ActionKind.ADD_POINTS_TO_CLOUD:
self.end_action(_('End of adding points to cloud'))
elif locaction == ActionKind.MOVE_POINT_IN_CLOUD:
# Ensure KDTree/bounds are refreshed exactly once at end of interaction.
if self.active_cloud is not None and self.active_cloud_vertex_id is not None:
x = None
y = None
for row_id, row in self.active_cloud.iter_rows():
if row_id == self.active_cloud_vertex_id:
curv = row.get('vertex')
if curv is not None:
x = curv.x
y = curv.y
break
if x is not None and y is not None:
self.active_cloud.move_vertex(
self.active_cloud_vertex_id,
x,
y,
invalidate_tree=True,
notify=True,
recompute_bounds=True,
)
self.active_cloud_vertex_id = None
self.end_action(_('End of point move in cloud'))
elif locaction == ActionKind.DISTANCE_ALONG_VECTOR:
if self._dialogs.ask_yes_no(_('Memorize the vector ?'), _('Confirm'), wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION):
self._distances[-1].add_vector(self._tmp_vector_distance, forceparent=True, update_struct=True)
self._tmp_vector_distance = None
self.end_action(_('End of distance measurement along vector'))
elif locaction in PICK_LANDMAP_ACTIONS:
self.end_action(_('End of landmap picking'))
elif locaction == ActionKind.LAZ_TMP_VECTOR:
self.end_action(_('End of LAZ selection'))
self.active_vector.myvertices.pop(-1)
self.plot_laz_around_active_vec()
self.active_vector.reset()
elif locaction == ActionKind.CREATE_POLYGON_TILES:
self.end_action(_('End of polygon creation'))
self.active_vector.myvertices.pop(-1)
self.active_vector.close_force()
if self._dialogs.ask_yes_no(_('Do you want to align vertices on magnetic grid ?'), _('Confirm'), wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION):
ds = self._dialogs.ask_integer(_('Which is the sptial step size [m] ?'), _('Size'), _('Spatial grid size'), 50, 1, 10000)
if ds is None:
return
vertices = self.active_vector.myvertices.copy()
self.active_vector.myvertices.clear()
x_aligned = np.asarray([(curvert.x // ds)*ds for curvert in vertices])
y_aligned = np.asarray([(curvert.y // ds)*ds for curvert in vertices])
if (x_aligned.min() == x_aligned.max()) and (y_aligned.min() == y_aligned.max()):
logging.error(_('All vertices are aligned on the same point -- Choose another step size'))
return
self.active_vector.add_vertex(wolfvertex(x_aligned.min(), y_aligned.min()))
self.active_vector.add_vertex(wolfvertex(x_aligned.max(), y_aligned.min()))
self.active_vector.add_vertex(wolfvertex(x_aligned.max(), y_aligned.max()))
self.active_vector.add_vertex(wolfvertex(x_aligned.min(), y_aligned.max()))
self.active_vector.add_vertex(wolfvertex(x_aligned.min(), y_aligned.min()))
self.active_vector.close_force()
self.active_vector.find_minmax()
self._create_data_from_tiles_common()
elif locaction == ActionKind.CAPTURE_VERTICES:
self.end_action(_('End of points capturing'))
self.active_vector.myvertices.pop(-1)
if self._dialogs.ask_confirmation(
_('End of points capturing') + '\n' + _('Force to close the vector ?'),
_('Confirm'), default='yes', parent=self
):
self.active_vector.close_force()
# force to prepare OpenGL to accelerate the plot
# Le test not(self in self.linkedList) permet de ne pas créer le liste OpenGL en cas de multi-viewers
# car une liste OpenGL ne sera pas tracée sur les autres fenêtres
# C'est donc plus lent mais plus sûr pour que l'affichage dynamique soit correct
if self.active_vector.parentzone is not None:
self.active_vector._on_vertices_changed() # to reset all cached geometries (Shapely, VBO, display list)
# self.active_vector.parentzone.plot(prep = self.linkedList is None or not(self in self.linkedList))
elif locaction == ActionKind.MODIFY_VERTICES:
# end of vertices modification
self.end_action(_('End of vertices modification'))
# force to prepare OpenGL to accelerate the plot
# Le test not(self in self.linkedList) permet de ne pas créer le liste OpenGL en cas de multi-viewers
# car une liste OpenGL ne sera pas tracée sur les autres fenêtres
# C'est donc plus lent mais plus sûr pour que l'affichage dynamique soit correct
if self.active_vector.parentzone is not None:
self.active_vector._on_vertices_changed() # to reset all cached geometries (Shapely, VBO, display list)
# self.active_vector.parentzone.plot(prep = self.linkedList is None or not(self in self.linkedList))
self.active_zones.find_minmax(True)
self.active_vertex = None
elif locaction == ActionKind.INSERT_VERTICES:
self.end_action(_('End of vertices insertion'))
# force to prepare OpenGL to accelerate the plot
# Le test not(self in self.linkedList) permet de ne pas créer le liste OpenGL en cas de multi-viewers
# car une liste OpenGL ne sera pas tracée sur les autres fenêtres
# C'est donc plus lent mais plus sûr pour que l'affichage dynamique soit correct
if self.active_vector.parentzone is not None:
self.active_vector._on_vertices_changed() # to reset all cached geometries (Shapely, VBO, display list)
# self.active_vector.parentzone.plot(prep = self.linkedList is None or not(self in self.linkedList))
self.active_zones.find_minmax(True)
self.active_vertex = None
elif locaction == ActionKind.DYNAMIC_PARALLEL:
self.active_vector.myvertices.pop(-1)
self.active_zone.parallel_active(self.dynapar_dist)
self.active_zones.fill_structure()
self.active_zones.find_minmax(True)
# force to prepare OpenGL to accelerate the plot
# Le test not(self in self.linkedList) permet de ne pas créer le liste OpenGL en cas de multi-viewers
# car une liste OpenGL ne sera pas tracée sur les autres fenêtres
# C'est donc plus lent mais plus sûr pour que l'affichage dynamique soit correct
if self.active_vector.parentzone is not None:
self.active_vector._on_vertices_changed() # to reset all cached geometries (Shapely, VBO, display list)
# self.active_vector.parentzone.plot(prep = self.linkedList is None or not(self in self.linkedList))
self.active_vertex = None
self.end_action(_('End of dynamic parallel'))
elif locaction in SELECT_ACTIVE_VECTOR_ACTIONS:
self.end_action(_('End of vector selection'))
elif locaction in SELECT_NODE_ACTIONS:
self.end_action(_('End of node by node selection'))
elif locaction == ActionKind.TRANSFORM_ASSET_BOUNDS:
ctrl = self._assets.transform_controller
if ctrl is not None and hasattr(ctrl, 'rebuild'):
try:
ctrl.rebuild(ToCheck=True)
except Exception as exc:
logging.warning('Final transform rebuild failed: %s', exc)
ed = self._assets.transform_editor
if ed is not None and hasattr(ed, 'refresh_from_controller'):
try:
ed.refresh_from_controller()
except Exception:
pass
self.end_action(_('End asset transform'))
elif locaction == ActionKind.SCULPT:
# Double right-click / Return ends the sculpt brush action.
# end_action() calls notify_action_ended() on the panel.
self.end_action(_('End sculpting'))
elif locaction == ActionKind.PROFILE:
# Double right-click / Return ends the profile brush action.
self.end_action(_('End profile brush'))
self.copyfrom = None
self.Refresh()
self.mimicme()
# ACTIONS
# ****************** <<
# Sculpt / profile method delegators -----------------------------------
[docs]
def _sculpt_hide_size(self) -> None:
self._sculpt.hide_size()
[docs]
def _sculpt_hide_zone(self) -> None:
self._sculpt.hide_zone()
[docs]
def _get_event_pressure(self, e):
return self._sculpt.get_event_pressure(e)
[docs]
def _profile_apply_at(self, x: float, y: float) -> None:
self._sculpt.profile_apply_at(x, y)
[docs]
def _draw_profile_cursor(self) -> None:
self._sculpt.draw_profile_cursor()
[docs]
def _request_brush_refresh(self) -> None:
self._sculpt.request_brush_refresh()
[docs]
def _do_brush_refresh(self) -> None:
self._sculpt.do_brush_refresh()
[docs]
def _sculpt_apply_at(self, x: float, y: float, pressure: float = 1.0) -> None:
self._sculpt.sculpt_apply_at(x, y, pressure)
[docs]
def _draw_sculpt_cursor(self) -> None:
self._sculpt.draw_sculpt_cursor()
[docs]
def distance_by_multiple_clicks(self):
""" Distance between multiple clicks """
self.start_action('distance along vector', _('Distance by multiple clicks -- Select the points'))
self._tmp_vector_distance = vector()
self._tmp_vector_distance.add_vertex([wolfvertex(0., 0.),
wolfvertex(0., 0.)])
[docs]
def toggle_automatic_background(self):
""" Toggle automatic background update mode """
self.enable_async_background_updates = not self.enable_async_background_updates
logging.info(_('Automatic background updates: ') + ('ON' if self.enable_async_background_updates else 'OFF'))
if self.enable_async_background_updates:
self._update_background_async()
self.Paint()
[docs]
def OnHotKey(self, e: wx.KeyEvent):
"""
Gestion des touches clavier -- see print_shortcuts for more details
"""
kb = self._wx_keyboard_snapshot(e)
logging.debug(_('You are pressing key code : ') + str(kb.key_code))
if kb.ctrl:
logging.debug(_('Ctrl is down'))
if kb.alt:
logging.debug(_('Alt is down'))
# Plugin key hook — consulted first; return True to consume.
if self.action in self._custom_key_handlers:
if self._custom_key_handlers[self.action](self, kb):
return
if kb.ctrl or kb.alt:
self._hotkey_with_modifier(kb)
else:
self._hotkey_bare(kb)
[docs]
def _hotkey_with_modifier(self, kb: 'KeyboardSnapshot') -> None:
"""Key presses with Ctrl or Alt held."""
key, ctrldown, altdown, shiftdown = kb.key_code, kb.ctrl, kb.alt, kb.shift
if key == 60 and shiftdown: #'>'
if self.active_array is not None:
if self.active_array.SelectionData is not None:
self.active_array.SelectionData.dilate_contour_selection(1)
self.active_array.reset_plot()
elif key == 60 and not shiftdown: #'<'
if self.active_array is not None:
if self.active_array.SelectionData is not None:
self.active_array.SelectionData.erode_contour_selection()
self.active_array.reset_plot()
elif key == wx.WXK_F2 and ctrldown and altdown and shiftdown:
if self.active_res2d is None:
logging.info(_('Please activate a simulation before search a specific result'))
self._add_sim_explorer(self.active_res2d)
elif key == wx.WXK_F2 and not shiftdown:
if self.active_res2d is not None:
nb = self.active_res2d.get_nbresults()
nb = self._dialogs.ask_integer(_('Please choose a step (1 -> {})'.format(nb)), 'Step :', _('Select a specific step'), nb, 1, nb)
if nb is None:
return
self.active_res2d.read_oneresult(nb-1)
self.active_res2d.set_currentview()
self.Refresh()
self._update_sim_explorer()
else:
logging.info(_('Please activate a simulation before search a specific result'))
elif key == wx.WXK_F2 and shiftdown:
if self.active_res2d is not None:
nb = self.active_res2d.get_nbresults()
choices = ['{:.3f} [s] - {} [h:m:s]'.format(cur, timedelta(seconds=int(cur),
milliseconds=int(cur-int(cur))*1000))
for cur in self.active_res2d.times]
keyvalue = self._dialogs.ask_single_choice(
_('Please choose a time step'),
_('Select a specific step'),
choices,
parent=self,
)
if keyvalue is None:
return
self.active_res2d.read_oneresult(choices.index(keyvalue))
self.active_res2d.set_currentview()
self.Refresh()
self._update_sim_explorer()
else:
logging.info(_('Please activate a simulation before searching a specific result'))
if key == wx.WXK_F4 and not shiftdown:
if self.active_particle_system is not None:
nb = self.active_particle_system.nb_steps
nb = self._dialogs.ask_integer(_('Please choose a step (1 -> {})'.format(nb)), 'Step :', _('Select a specific step'), nb, 1, nb)
if nb is None:
return
self.active_particle_system.current_step = nb-1
self.Refresh()
self._update_tooltip()
self._update_sim_explorer()
else:
logging.info(_('Please activate a particle system before searching a specific result'))
elif key == wx.WXK_F4 and shiftdown:
if self.active_particle_system is not None:
choices = ['{:.3f} [s] - {} [h:m:s]'.format(cur, timedelta(seconds=int(cur),
milliseconds=int(cur-int(cur))*1000))
for cur in self.active_particle_system.get_times()]
keyvalue = self._dialogs.ask_single_choice(
_('Please choose a time step'),
_('Select a specific step'),
choices,
parent=self,
)
if keyvalue is None:
return
self.active_particle_system.current_step = choices.index(keyvalue)
self.Refresh()
self._update_tooltip()
self._update_sim_explorer()
else:
logging.info(_('Please activate a simulation before search a specific result'))
elif key == wx.WXK_NUMPAD_ADD: #+ from numpad
if self.active_res2d is not None:
self.active_res2d.update_zoom_2(1.1)
self.Refresh()
elif key == wx.WXK_NUMPAD_SUBTRACT: #- from numpad
if self.active_res2d is not None:
self.active_res2d.update_zoom_2(1./1.1)
self.Refresh()
elif key == ord('X'):
# Create a new array from the active array and the active vector
# Node outside the vector are set to NullValue
if self.active_array is not None and self.active_vector is not None:
bbox = self.active_vector.get_bounds_xx_yy()
newarray = self.active_array.crop_array(bbox)
if not altdown:
newarray.mask_outsidepoly(self.active_vector)
newarray.nullify_border(width=1)
#keys for arrays
keys = self.get_list_keys(draw_type.ARRAYS, checked_state=None)
new_key = self.active_array.idx + '_crop'
while new_key in keys:
new_key += '_'
self.add_object('array', newobj = newarray, id = new_key)
self.Refresh()
if key == ord('U'):
# CTRL+U
# Mise à jour des données par import du fichier gtlf2
msg = ''
if self.active_array is None:
msg += _('Active array is None\n')
if msg != '':
msg += _('\n')
msg += _('Retry !\n')
wx.MessageBox(msg)
return
self.set_fn_fnpos_gltf()
self.update_blender_sculpting()
elif key == ord('F'):
if self.active_zones is not None:
self.start_action('select active vector all', _('Select active vector all'))
elif key == ord('L'):
if self.active_tile is not None:
self.start_action('select active tile', _('Select active tile'))
elif key == wx.WXK_UP:
self.upobj()
elif key == wx.WXK_DOWN:
self.downobj()
elif key == ord('C') and altdown and not ctrldown and not shiftdown:
# ALT+C
#Copie du canvas dans le clipboard pour transfert vers autre application
self.copy_canvasogl()
elif key == ord('C') and altdown and not ctrldown and shiftdown:
# ALT+SHIFT+C
# Copie du canvas dans le clipboard pour transfert vers autre application
# Copie des canvas liés
if not self.linked:
logging.error(_('No linked canvas to copy -- calling ALT+C instead'))
self.copy_canvasogl()
return
from tempfile import TemporaryDirectory
logging.info(_('Creating images'))
with TemporaryDirectory() as tmpdirname:
all_images = self.save_linked_canvas(Path(tmpdirname) / 'fig', mpl= True, ds= self.ticks_size, add_title= True)
if len(all_images) == 0:
logging.error(_('No image to combine -- aborting !'))
return
im_assembly = self.assembly_images(all_images, mode= self.assembly_mode)
logging.info(_('Creating images - done'))
# Copy image to clipboard
if im_assembly is not None:
if wx.TheClipboard.Open():
#création d'un objet bitmap wx
wxbitmap = wx.Bitmap().FromBuffer(im_assembly.width, im_assembly.height, im_assembly.tobytes())
# objet wx exportable via le clipboard
dataobj = wx.BitmapDataObject()
dataobj.SetBitmap(wxbitmap)
wx.TheClipboard.SetData(dataobj)
wx.TheClipboard.Close()
logging.info(_('Image copied to clipboard'))
else:
logging.error(_('Cannot open the clipboard'))
else:
logging.error(_('No image to copy to clipboard'))
elif key == ord('C') and ctrldown and not altdown:
# CTRL+C
if self.active_array is None:
self._dialogs.show_message(_('The active array is None - Please active an array from which to copy the values !'), parent=self)
return
logging.info(_('Start copying values / Current selection to clipboard'))
self.copyfrom = self.active_array
self.mimicme_copyfrom() # force le recopiage de copyfrom dans les autres matrices liées
if len(self.active_array.SelectionData.myselection) > 5000:
if not self._dialogs.ask_ok_cancel(_('The selection is large, copy to clipboard may be slow ! -- Continue?'), parent=self):
logging.info(_('Copy to clipboard cancelled -- But source array is well defined !'))
return
self.active_array.SelectionData.copy_to_clipboard()
logging.info(_('Values copied to clipboard'))
elif key == ord('C') and ctrldown and altdown and shiftdown:
# CTRL+ALT+SHIFT+C
# Copie du canvas dans le clipboard pour transfert vers autre application
# Une matrice est associée à chaque canvas
from tempfile import TemporaryDirectory
logging.info(_('Creating images'))
with TemporaryDirectory() as tmpdirname:
all_images = self.save_arrays_indep(Path(tmpdirname) / 'fig', mpl= True, ds= self.ticks_size, add_title= True)
if len(all_images) == 0:
logging.error(_('No image to combine -- aborting !'))
return
im_assembly = self.assembly_images(all_images, mode= self.assembly_mode)
logging.info(_('Creating images - done'))
# Copy image to clipboard
if im_assembly is not None:
if wx.TheClipboard.Open():
#création d'un objet bitmap wx
wxbitmap = wx.Bitmap().FromBuffer(im_assembly.width, im_assembly.height, im_assembly.tobytes())
# objet wx exportable via le clipboard
dataobj = wx.BitmapDataObject()
dataobj.SetBitmap(wxbitmap)
wx.TheClipboard.SetData(dataobj)
wx.TheClipboard.Close()
logging.info(_('Image copied to clipboard'))
else:
logging.error(_('Cannot open the clipboard'))
else:
logging.error(_('No image to copy to clipboard'))
elif key == ord('C') and ctrldown and altdown and not shiftdown:
if self.active_array is None:
self._dialogs.show_message(_('The active array is None - Please active an array from which to copy the selection !'), parent=self)
return
logging.info(_('Start copying selection / Current selection to clipboard as script (Python)'))
self.copyfrom = self.active_array
self.mimicme_copyfrom() # force le recopiage de copyfrom dans les autres matrices liées
if len(self.active_array.SelectionData.myselection) > 5000:
if not self._dialogs.ask_ok_cancel(_('The selection is large, copy to clipboard may be slow ! -- Continue?'), parent=self):
logging.info(_('Copy script to clipboard cancelled -- But source array is well defined !'))
return
self.active_array.SelectionData.copy_to_clipboard(typestr='script')
logging.info(_('Selection copied to clipboard as script (Python)'))
elif key == ord('V') and ctrldown:
# CTRL+V
# CTRL+ALT+V ou Alt Gr + V
if self.active_array is None:
if altdown:
# CTRL+ALT+V
logging.warning(_('The active array is None - Please active an array into which to paste the selection !'))
else:
logging.warning(_('The active array is None - Please active an array into which to paste the values !'))
return
fromarray = self.copyfrom
if fromarray is None:
if self.linked:
if not self.linkedList is None:
for curFrame in self.linkedList:
if curFrame.copyfrom is not None:
fromarray = curFrame.copyfrom
break
if fromarray is None:
logging.warning(_('No selection to be pasted !'))
return
cursel = fromarray.SelectionData.myselection
if altdown:
logging.info(_('Paste selection position'))
if cursel == 'all':
self.active_array.SelectionData.myselection = 'all'
elif len(cursel) > 0:
self.active_array.SelectionData.myselection = cursel.copy()
# self.active_array.SelectionData.update_nb_nodes_selection()
else:
logging.info(_('Paste selection values'))
if cursel == 'all':
self.active_array.paste_all(fromarray)
elif len(cursel) > 0:
z = fromarray.SelectionData.get_values_sel()
self.active_array.set_values_sel(cursel, z)
self.Refresh()
logging.info(_('Selection/Values pasted'))
elif key == ord('Z'):
# Sculpt / profile undo takes priority over zoom
if not self._sculpt.on_key_z(kb):
if ctrldown:
if shiftdown:
if self.active_vector is not None:
self.zoom_on_vector(self.active_vector, canvas_height= self.canvas.GetSize()[1])
else:
logging.warning(_('No active vector to zoom on !'))
else:
if self.active_array is not None:
self.zoom_on_array(self.active_array, canvas_height= self.canvas.GetSize()[1])
else:
logging.warning(_('No active array to zoom on !'))
[docs]
def _hotkey_bare(self, kb: 'KeyboardSnapshot') -> None:
"""Key presses with no modifier (bare key or Shift only)."""
key, ctrldown, shiftdown = kb.key_code, kb.ctrl, kb.shift
if key == wx.WXK_DELETE:
self.removeobj()
elif key == 60 and shiftdown: #'>'
if self.active_array is not None:
if self.active_array.SelectionData is not None:
self.active_array.SelectionData.dilate_selection(1)
self.active_array.reset_plot()
elif key == 60 and not shiftdown: #'<'
if self.active_array is not None:
if self.active_array.SelectionData is not None:
self.active_array.SelectionData.erode_selection(1)
self.active_array.reset_plot()
elif key == wx.WXK_ESCAPE:
logging.info(_('Escape key pressed -- Set all active objects and "action" to None'))
self.action = None
self.reset_all_actives()
self.set_statusbar_text(_('Esc pressed - No more action in progress - No more active object'))
self.set_label_selecteditem('')
self.Paint()
elif key == ord('C'):
self.copy_canvasogl(mpl = False)
elif key == wx.WXK_SPACE:
if self.timer_ps is not None and self.active_particle_system is not None :
if self.timer_ps.IsRunning():
self.timer_ps.Stop()
else:
if self.active_particle_system.current_step_idx == self.active_particle_system.nb_steps-1:
self.active_particle_system.current_step_idx = 0
self.active_particle_system.current_step = 0
self.timer_ps.Start(1000. / self.active_particle_system.fps)
elif key == 388: #+ from numpad
if self.active_res2d is not None:
self.active_res2d.update_arrowpixelsize_vectorfield(-1)
self.Refresh()
elif key == 390: #- from numpad
if self.active_res2d is not None:
self.active_res2d.update_arrowpixelsize_vectorfield(1)
self.Refresh()
elif key == 13 or key==370 or key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
# 13 = RETURN classic keyboard
# 370 = RETURN NUMPAD
self._endactions()
elif key == ord('I'):
if self.active_array is not None :
self.active_array.interpolation2D()
elif key == ord('F'):
if self.active_zone is not None:
self.start_action('select active vector2 all', _('Select active vector2 all'))
elif key in LIST_1TO9:
if self.active_array is not None:
if self.active_array.SelectionData.myselection == 'all':
logging.warning(_('No selection to transfer to the dictionary !'))
logging.info(_('Please select some nodes before transfering to the dictionary, not ALL !'))
return
# colors = [(0, 0, 255, 255),
# (0, 255, 0, 255),
# (0, 128, 255, 255),
# (255, 255, 0, 255),
# (255, 165, 0, 255),
# (128, 0, 128, 255),
# (255, 192, 203, 255),
# (165, 42, 42, 255),
# (128, 128, 128, 255)]
idx = LIST_1TO9.index(key)
if idx > 8:
idx -= 9
self.active_array.SelectionData.move_selectionto(str(idx+1), self.colors1to9[idx])
elif key == wx.WXK_F1:
self.read_last_result()
elif key == wx.WXK_F2 and shiftdown:
self.simul_previous_step()
elif key == wx.WXK_F4 and shiftdown:
self.particle_previous_step()
elif key == wx.WXK_F4:
self.particle_next_step()
elif key == wx.WXK_F2:
self.simul_next_step()
elif key == wx.WXK_F5:
# Autoscale
self.Autoscale()
elif key == wx.WXK_F7:
self.update()
elif key == wx.WXK_F8:
self.zoom_on_whole_walonia()
elif key == wx.WXK_F10:
self.toggle_automatic_background()
elif key == wx.WXK_F12 or key == wx.WXK_F11:
if self.active_array is not None:
self.active_array.myops.SetTitle(_('Operations on array: ')+self.active_array.idx)
self.active_array.myops.Show()
self.active_array.myops.array_ops.SetSelection(1)
self.active_array.myops.Center()
elif key == wx.WXK_F9:
if self.active_array is not None:
if self.active_array.SelectionData is not None:
self.active_array.SelectionData.myselection = 'all'
logging.info(_('Selecting all nodes in the active array !'))
else:
logging.warning(_('No selection manager for this array !'))
if self.active_array.myops is not None:
self.active_array.myops.nbselect.SetLabelText('All')
else:
logging.warning(_('No operations manager for this array !'))
elif key == ord('N'): # N
if self.active_array is not None:
self.active_array.myops.select_node_by_node()
if self.active_res2d is not None:
if self.active_array is not None:
self._dialogs.ask_yes_no(_('Do you want to select the nodes of the active result ?'), _('Select nodes'), wx.YES_NO | wx.ICON_QUESTION)
self.active_res2d.properties.select_node_by_node()
if self.active_array is None and self.active_res2d is None:
logging.warning(_('No active array or result 2D to select node by node !'))
elif key == ord('V'): # V
if self.active_array is not None:
if shiftdown:
self.active_array.myops.select_vector_inside_manager()
else:
self.active_array.myops.select_vector_inside_tmp()
else:
logging.warning(_('No active array to select the vector inside !'))
elif key == ord('B'): # B
if self.active_array is not None:
if shiftdown:
self.active_array.myops.select_vector_under_manager()
else:
self.active_array.myops.select_vector_under_tmp()
else:
logging.warning(_('No active array to select the vector inside !'))
elif key == ord('P'): # P
if self.active_cs is not None:
self.start_action('Select nearest profile', _('Select nearest profile'))
else:
logging.warning(_('No active cross section to select the nearest profile !'))
elif key == ord('Z') and shiftdown: # Z
self.width = self.width / 1.1
self.height = self.height / 1.1
self.setbounds()
elif key == ord('Z'): # z
self.width = self.width * 1.1
self.height = self.height * 1.1
self.setbounds()
elif key == ord('R') and shiftdown: # R
# Skip reset-all-selection when sculpt+RECTANGLE rotate mode is active
if self._sculpt.on_key_r(kb):
return
if self.active_array is not None:
self.active_array.myops.reset_all_selection()
self.Refresh()
if self.active_res2d is not None:
self.active_res2d.SelectionData.reset_all()
self.Refresh()
elif key == ord('R'): # r
# Skip reset-selection when sculpt+RECTANGLE rotate mode is active
if self._sculpt.on_key_r(kb):
return
if self.active_array is not None:
self.active_array.myops.reset_selection()
self.Refresh()
if self.active_res2d is not None:
self.active_res2d.SelectionData.reset()
self.Refresh()
elif key == ord('O'):
# Active Opacity for the active array
if ctrldown:
if self.active_res2d is None:
logging.warning(_('No active result 2D to change the opacity !'))
return
if shiftdown:
self.active_res2d.set_opacity(self.active_res2d.alpha + 0.25)
else:
self.active_res2d.set_opacity(self.active_res2d.alpha - 0.25)
else:
if self.active_array is None:
logging.warning(_('No active array to change the opacity !'))
return
if shiftdown:
self.active_array.set_opacity(self.active_array.alpha + 0.25)
else:
self.active_array.set_opacity(self.active_array.alpha - 0.25)
elif key == wx.WXK_UP:
self._center_y = self._center_y + self.height / 10.
self.setbounds()
elif key == wx.WXK_DOWN:
self._center_y = self._center_y - self.height / 10.
self.setbounds()
elif key == wx.WXK_LEFT:
self._center_x = self._center_x - self.width / 10.
self.setbounds()
elif key == wx.WXK_RIGHT:
self._center_x = self._center_x + self.width / 10.
self.setbounds()
elif key == ord('A'):
if self.active_laz is not None:
self.active_laz.add_pose_in_memory()
elif key == ord('D'):
self.distance_by_multiple_clicks()
elif key == ord('H'):
# Toggle hillshade or adjust sun parameters
if shiftdown:
# Shift+H: open persistent hillshade panel
self._show_hillshade_panel()
else:
# H: toggle hillshade on/off
self.hillshade = not self.hillshade
state = _('ON') if self.hillshade else _('OFF')
logging.info(_('Hillshade %s (alt=%d° az=%d° int=%.1f)'),
state, int(self.sun_altitude),
int(self.sun_azimuth), self.sun_intensity)
self.Paint()
elif key == ord('K'):
# Toggle palette overlay on/off
if self._palette_overlay is None:
self._palette_overlay = PaletteOverlay(self)
logging.info(_('Palette overlay ON'))
else:
self._palette_overlay = None
logging.info(_('Palette overlay OFF'))
self.Refresh()
elif key == ord('T'):
# Toggle toolbar overlay on/off
if self._toolbar_overlay is None:
self._toolbar_overlay = ToolbarOverlay(self)
logging.info(_('Toolbar overlay ON'))
else:
self._toolbar_overlay = None
logging.info(_('Toolbar overlay OFF'))
self.Refresh()
[docs]
def paste_values(self,fromarray:WolfArray):
""" Paste selected values from a WolfArray to the active array """
if self.active_array is None:
logging.warning(_('The active array is None - Please active an array into which to paste the values !'))
return
logging.info(_('Paste selection values'))
cursel = fromarray.SelectionData.myselection
if cursel == 'all':
self.active_array.paste_all(fromarray)
elif len(cursel) > 0:
z = fromarray.SelectionData.get_values_sel()
self.active_array.set_values_sel(cursel, z)
[docs]
def paste_selxy(self,fromarray:WolfArray):
""" Paste selected nodes from a WolfArray to the active array """
if self.active_array is None:
logging.warning(_('The active array is None - Please active an array into which to paste the selection !'))
return
logging.info(_('Paste selection position'))
cursel = fromarray.SelectionData.myselection
if cursel == 'all':
self.active_array.SelectionData.OnAllSelect(0)
elif len(cursel) > 0:
self.active_array.SelectionData.myselection = cursel.copy()
self.active_array.SelectionData.update_nb_nodes_selection()
[docs]
def OntreeRight(self, e: wx.MouseEvent):
""" Gestion du menu contextuel sur l'arbre des objets """
if self.selected_object is None:
return
popup = wx.Menu()
# ---- Static items (always present) ----------------------------------
item_save = popup.Append(wx.ID_ANY, _('Save'))
item_save_as = popup.Append(wx.ID_ANY, _('Save as'))
item_rename = popup.Append(wx.ID_ANY, _('Rename'))
item_duplicate = popup.Append(wx.ID_ANY, _('Duplicate'))
item_delete = popup.Append(wx.ID_ANY, _('Delete'))
item_up = popup.Append(wx.ID_ANY, _('Up'))
item_down = popup.Append(wx.ID_ANY, _('Down'))
item_check = popup.Append(wx.ID_ANY, _('Check/Uncheck'))
item_properties = popup.Append(wx.ID_ANY, _('Properties'))
item_reload = popup.Append(wx.ID_ANY, _('Reload'))
popup.Bind(wx.EVT_MENU, lambda e: self._popup_save(), item_save)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_save_as(), item_save_as)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_rename(), item_rename)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_duplicate(), item_duplicate)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_delete(), item_delete)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_up(), item_up)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_down(), item_down)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_check_uncheck(), item_check)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_properties(), item_properties)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_reload(), item_reload)
# ---- Dynamic items based on object type -----------------------------
if isinstance(self.selected_object, WolfArray):
bc = self.get_boundary_manager(self.selected_object)
if bc is not None:
item_bc = popup.Append(wx.ID_ANY, _('Boundary conditions'))
popup.Bind(wx.EVT_MENU, lambda e: self._popup_boundary_conditions(), item_bc)
item_cont = popup.Append(wx.ID_ANY, _('Contours'))
item_rebin = popup.Append(wx.ID_ANY, _('Rebin'), _('Change the spatial resolution'))
item_nv = popup.Append(wx.ID_ANY, _('Set NullValue'))
popup.Bind(wx.EVT_MENU, lambda e: self._popup_contours(), item_cont)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_rebin(), item_rebin)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_set_nullvalue(), item_nv)
self._popup_append_clip_submenu(popup)
if isinstance(self.selected_object, WolfArrayMB):
item_mono = popup.Append(wx.ID_ANY, _('Convert to mono-block'))
popup.Bind(wx.EVT_MENU, lambda e: self._popup_convert_mono(), item_mono)
if isinstance(self.selected_object, Wolfresults_2D):
item_mono_r = popup.Append(wx.ID_ANY, _('Convert to mono-block (result)'))
item_multi_r = popup.Append(wx.ID_ANY, _('Convert to multi-blocks (result)'))
item_ic = popup.Append(wx.ID_ANY, _('Extract current step as IC (result)'))
popup.Bind(wx.EVT_MENU, lambda e: self._popup_convert_mono(), item_mono_r)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_convert_multi(), item_multi_r)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_extract_ic(), item_ic)
self._popup_append_clip_submenu(popup)
if isinstance(self.selected_object, Zones):
item_rasz = popup.Append(wx.ID_ANY, _('Rasterize active zone'))
item_rasv = popup.Append(wx.ID_ANY, _('Rasterize active vector'))
item_interp = popup.Append(wx.ID_ANY, _('Interpolate on active array'))
popup.Bind(wx.EVT_MENU, lambda e: self._popup_rasterize_zone(), item_rasz)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_rasterize_vector(), item_rasv)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_interpolate_on_array(), item_interp)
self._popup_append_clip_submenu(popup)
if isinstance(self.selected_object, Zones | Bridge | Weir):
item_exp = popup.Append(wx.ID_ANY, _('Export to Shape file'))
item_expz = popup.Append(wx.ID_ANY, _('Export active zone to Shape file'))
popup.Bind(wx.EVT_MENU, lambda e: self._popup_export_shapefile(), item_exp)
popup.Bind(wx.EVT_MENU, lambda e: self._popup_export_active_zone_shapefile(), item_expz)
if isinstance(self.selected_object, Wolf_LAZ_Data):
colrmapmenu = wx.Menu()
popup.AppendSubMenu(colrmapmenu, _('Colormap'))
item_setcol = colrmapmenu.Append(wx.ID_ANY, _('Set colormap'), _('Change colormap'))
item_editcol = colrmapmenu.Append(wx.ID_ANY, _('Edit colormap'), _('Edit colormap'))
item_setcls = colrmapmenu.Append(wx.ID_ANY, _('Set classification'), _('Change classification'))
colrmapmenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_set_colormap(), item_setcol)
colrmapmenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_edit_colormap(), item_editcol)
colrmapmenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_set_classification(), item_setcls)
converttomenu = wx.Menu()
popup.AppendSubMenu(converttomenu, _('Convert to...'))
item_all2cl = converttomenu.Append(wx.ID_ANY, _('All to cloud'), _('Convert all to cloud'))
item_sel2cl = converttomenu.Append(wx.ID_ANY, _('Selection to cloud'), _('Convert selection to cloud'))
item_sel2vec = converttomenu.Append(wx.ID_ANY, _('Selection to vector'), _('Convert selection to vector'))
converttomenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_all_to_cloud(), item_all2cl)
converttomenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_selection_to_cloud(), item_sel2cl)
converttomenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_selection_to_vector(), item_sel2vec)
item_editsel = popup.Append(wx.ID_ANY, _('Edit selection'))
popup.Bind(wx.EVT_MENU, lambda e: self._popup_laz_edit_selection(), item_editsel)
moviemenu = wx.Menu()
popup.AppendSubMenu(moviemenu, _('Movie'))
item_addpt = moviemenu.Append(wx.ID_ANY, _('Add point'), _('Add point passage'))
item_play = moviemenu.Append(wx.ID_ANY, _('Play'), _('Play'))
item_ldflt = moviemenu.Append(wx.ID_ANY, _('Load flight'), _('Load flight'))
item_saveflt = moviemenu.Append(wx.ID_ANY, _('Save flight'), _('Save flight'))
moviemenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_add_point(), item_addpt)
moviemenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_play(), item_play)
moviemenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_load_flight(), item_ldflt)
moviemenu.Bind(wx.EVT_MENU, lambda e: self._popup_laz_save_flight(), item_saveflt)
if isinstance(self.selected_object, Picc_data):
item_ext = popup.Append(wx.ID_ANY, _('Extrude on active array'), _('Extrude building elevation on active array'))
popup.Bind(wx.EVT_MENU, lambda e: self._popup_extrude(), item_ext)
self.treelist.PopupMenu(popup)
popup.Destroy()
[docs]
def zoom_on_whole_walonia(self):
""" Zoom on the whole Walonia """
xmin = 40_000
xmax = 300_000
ymin = 10_000
ymax = 175_000
self._center_x = (xmin + xmax) / 2.
self._center_y = (ymin + ymax) / 2.
self.width = xmax - xmin
self.height = ymax - ymin
self.setbounds()
self.update()
[docs]
def _update_background(self):
"""
Update background (synchronous - blocking)
"""
# dessin du background
for obj in self.iterator_over_objects(draw_type.WMSBACK):
obj.reload()
[docs]
def _update_background_async(self):
"""
Trigger asynchronous background image updates.
This method is called when viewport bounds change (during panning/zooming).
It triggers non-blocking image loads in the background via the AsyncImageLoader.
The textures are updated as images are loaded, without blocking the UI.
"""
# Display hourglass cursor to indicate loading
self.SetCursor(wx.Cursor(wx.CURSOR_WAIT))
# Schedule cursor restoration after 0.5 seconds
def restore_cursor():
self.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT))
wx.CallLater(500, restore_cursor)
# Iterate over background WMS images and trigger async reload
for obj in self.iterator_over_objects(draw_type.WMSBACK):
try:
# Call reload on each background object
# This now supports async loading if the object is wolf_texture with cache
obj.reload()
except Exception as e:
logging.warning(_('Error updating background image: ') + str(e))
[docs]
def _update_foreground(self):
"""
Update foreground (synchronous - blocking)
"""
# dessin du foreground
for obj in self.iterator_over_objects(draw_type.WMSFORE):
obj.reload()
[docs]
def update(self):
"""
Update backgournd et foreground elements and arrays if local minmax is checked.
"""
self._update_background()
self._update_foreground()
if self.locminmax.IsChecked() or self.update_absolute_minmax:
for curarray in self.iterator_over_objects(draw_type.ARRAYS):
curarray: WolfArray
if self.update_absolute_minmax:
curarray.updatepalette()
self.update_absolute_minmax = False
else:
curarray.updatepalette(onzoom=[self.xmin, self.xmax, self.ymin, self.ymax])
curarray.delete_lists()
self.Paint()
[docs]
def _plotting(self, drawing_type: draw_type, checked_state: bool = True):
""" Drawing objets on canvas"""
try:
for curobj in self.iterator_over_objects(drawing_type, checked_state=checked_state):
if not curobj.plotting:
curobj.plotting = True
curobj.plot(sx = self.sx, sy=self.sy, xmin=self.xmin, ymin=self.ymin, xmax=self.xmax, ymax=self.ymax, size = (self.xmax - self.xmin) / 100.)
curobj.plotting = False
except Exception as ex:
curobj.plotting = False
logging.error(_('Error while plotting objects of type {}').format(drawing_type.name))
traceback.print_exc()
logging.error(ex)
# ----------------------------------------------------------------
# Tracking-label bookkeeping
# ----------------------------------------------------------------
[docs]
_tracking_label_zones: set
[docs]
def set_tracking_label(self, zone_id: int, active: bool):
"""Register or unregister a zone as having active tracking labels.
:param zone_id: ``id(zone)`` of the calling zone.
:param active: *True* to register, *False* to unregister.
"""
tracking_set = getattr(self, '_tracking_label_zones', None)
if tracking_set is None:
tracking_set = set()
self._tracking_label_zones = tracking_set
if active:
tracking_set.add(zone_id)
else:
tracking_set.discard(zone_id)
@property
[docs]
def has_tracking_labels(self) -> bool:
"""Return *True* if at least one zone has active tracking labels."""
s = getattr(self, '_tracking_label_zones', None)
return bool(s)
# ----------------------------------------------------------------
# Projection / MVP helpers
# ----------------------------------------------------------------
# Convention: numpy rows store OpenGL matrix columns.
# A C-contiguous float32 array is directly usable with
# ``glUniformMatrix4fv(loc, 1, GL_FALSE, mvp)`` (column-major
# bytes) and ``glLoadMatrixf(mvp)``.
#
# Near/far planes are shared so that ``_set_gl_projection_matrix``
# (fixed-function) and ``get_ortho_mvp_c_contiguous`` (shader
# path) always agree.
# ----------------------------------------------------------------
[docs]
def get_MVP_Viewport_matrix(self):
"""Read back the current GL modelview, projection and viewport.
Requires an active OpenGL context. The returned projection
matrix has the same byte layout as :attr:`mvp` (C-contiguous,
column-major) because PyOpenGL wraps ``glGetFloatv`` that way.
:return: ``(modelview, projection, viewport)`` — each a numpy
array, or ``(None, None, None)`` when no context is
available.
"""
if self.SetCurrentContext():
modelview = glGetFloatv(GL_MODELVIEW_MATRIX)
projection = glGetFloatv(GL_PROJECTION_MATRIX)
viewport = glGetIntegerv(GL_VIEWPORT)
return modelview, projection, viewport
return None, None, None
[docs]
def get_ortho_mvp_c_contiguous(self) -> np.ndarray | None:
"""Build the 2D orthographic MVP purely from Python attributes.
Uses ``self.xmin / xmax / ymin / ymax`` and the shared
:pyattr:`_ORTHO_NEAR` / :pyattr:`_ORTHO_FAR` planes so the
result is always consistent with :meth:`_set_gl_projection_matrix`.
:return: ``np.ndarray`` shape ``(4, 4)``, dtype ``float32``,
C-contiguous (numpy rows = OpenGL columns), or ``None``
when the current view bounds are degenerate.
"""
dx = float(self.xmax - self.xmin)
dy = float(self.ymax - self.ymin)
if abs(dx) < 1e-12 or abs(dy) < 1e-12:
return None
tx = -(self.xmax + self.xmin) / dx
ty = -(self.ymax + self.ymin) / dy
near = self._ORTHO_NEAR
far = self._ORTHO_FAR
dz = far - near # 199998.0
return np.ascontiguousarray(np.array([
[2.0 / dx, 0.0, 0.0, 0.0],
[0.0, 2.0 / dy, 0.0, 0.0],
[0.0, 0.0, -2.0 / dz, 0.0],
[tx, ty, -(far + near) / dz, 1.0],
], dtype=np.float32))
[docs]
def _set_gl_projection_matrix(self):
"""Set the fixed-function projection from current view bounds.
Uses the same near/far as :meth:`get_ortho_mvp_c_contiguous`
so the fixed-function pipeline and shader path always agree.
"""
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(self.xmin, self.xmax, self.ymin, self.ymax,
self._ORTHO_NEAR, self._ORTHO_FAR)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
@property
[docs]
def mvp(self) -> np.ndarray:
"""Model-view-projection matrix (C-contiguous, float32).
Preferred path: pure-Python computation via
:meth:`get_ortho_mvp_c_contiguous` (no GL context needed).
Fallback: GL state readback via :meth:`get_MVP_Viewport_matrix`
wrapped with ``np.ascontiguousarray`` (requires active context).
Last resort: identity matrix.
"""
mvp = self.get_ortho_mvp_c_contiguous()
if mvp is not None:
return mvp
__, p, __ = self.get_MVP_Viewport_matrix()
if p is None:
return np.ascontiguousarray(np.eye(4, dtype=np.float32))
return np.ascontiguousarray(np.asarray(p, dtype=np.float32))
@property
[docs]
def sunposition(self):
"""Sun position as ``glm.vec3`` derived from altitude/azimuth angles.
The position is placed on a large sphere centred on the current
view so that the resulting directional light is consistent
regardless of pan/zoom.
"""
from glm import vec3
import math
alt = math.radians(self._sun_altitude)
azi = math.radians(self._sun_azimuth)
R = 1e6 # large distance → quasi-directional light
cx = (self.xmin + self.xmax) * 0.5
cy = (self.ymin + self.ymax) * 0.5
x = cx + R * math.cos(alt) * math.sin(azi)
y = cy + R * math.cos(alt) * math.cos(azi)
z = R * math.sin(alt)
return vec3(x, y, z)
@property
[docs]
def sunintensity(self):
return self._sun_intensity
@property
[docs]
def hillshade(self) -> bool:
"""Whether the shader hillshade is active."""
return self._hillshade_enabled
@hillshade.setter
def hillshade(self, value: bool):
self._hillshade_enabled = bool(value)
@property
[docs]
def sun_altitude(self) -> float:
"""Sun elevation angle in degrees (0 = horizon, 90 = zenith)."""
return self._sun_altitude
@sun_altitude.setter
def sun_altitude(self, value: float):
self._sun_altitude = max(0.0, min(90.0, float(value)))
@property
[docs]
def sun_azimuth(self) -> float:
"""Sun azimuth in degrees (0 = North, 90 = East, clockwise)."""
return self._sun_azimuth
@sun_azimuth.setter
def sun_azimuth(self, value: float):
self._sun_azimuth = float(value) % 360.0
@property
[docs]
def sun_intensity(self) -> float:
"""Sun intensity multiplier (0 to 2)."""
return self._sun_intensity
@sun_intensity.setter
def sun_intensity(self, value: float):
self._sun_intensity = max(0.0, min(2.0, float(value)))
@property
[docs]
def hillshade_multidirectional(self) -> bool:
"""Whether multi-directional hillshade (8 azimuths) is active."""
return self._hillshade_multidirectional
@hillshade_multidirectional.setter
def hillshade_multidirectional(self, value: bool):
self._hillshade_multidirectional = bool(value)
# ---- Active hillshade material params resolution ----
@property
[docs]
def active_hillshade_params(self) -> "HillshadeRenderParams":
"""Return the material params that the panel/overlay should read/write.
In sync mode → shared params.
In per-array mode → active_array.hillshade_params (fallback to shared).
When selected_object is a Wolfresults_2D, returns the first
block's current array params so the panel controls them.
"""
if self._hillshade_sync:
return self._hillshade_shared_params
# Check Wolfresults_2D first (selected_object may be one)
so = getattr(self, 'selected_object', None)
if so is not None and isinstance(so, Wolfresults_2D):
first = next(so.iter_current_arrays(), None)
if first is not None and hasattr(first, 'hillshade_params'):
return first.hillshade_params
aa = self.active_array
if aa is not None and hasattr(aa, 'hillshade_params'):
return aa.hillshade_params
return self._hillshade_shared_params
[docs]
def resolve_hillshade_params(self, wa) -> "HillshadeRenderParams":
"""Return the material params for a specific WolfArray *wa*.
Called by the shader upload to get per-array or shared params.
"""
if self._hillshade_sync:
return self._hillshade_shared_params
if hasattr(wa, 'hillshade_params'):
return wa.hillshade_params
return self._hillshade_shared_params
@property
[docs]
def hillshade_sync(self) -> bool:
"""True → all arrays share the same material params."""
return self._hillshade_sync
@hillshade_sync.setter
def hillshade_sync(self, value: bool):
self._hillshade_sync = bool(value)
# ---- Convenience material properties (delegate to active params) ----
@property
[docs]
def hillshade_z_exaggeration(self) -> float:
return self.active_hillshade_params.z_exaggeration
@hillshade_z_exaggeration.setter
def hillshade_z_exaggeration(self, value: float):
self.active_hillshade_params.z_exaggeration = max(0.1, float(value))
self._propagate_hillshade_to_children()
@property
[docs]
def hillshade_specular(self) -> float:
return self.active_hillshade_params.specular
@hillshade_specular.setter
def hillshade_specular(self, value: float):
self.active_hillshade_params.specular = max(0.0, min(1.0, float(value)))
self._propagate_hillshade_to_children()
@property
[docs]
def hillshade_glossiness(self) -> float:
return self.active_hillshade_params.glossiness
@hillshade_glossiness.setter
def hillshade_glossiness(self, value: float):
self.active_hillshade_params.glossiness = max(0.0, min(1.0, float(value)))
self._propagate_hillshade_to_children()
@property
[docs]
def hillshade_highlight(self) -> float:
return self.active_hillshade_params.highlight
@hillshade_highlight.setter
def hillshade_highlight(self, value: float):
self.active_hillshade_params.highlight = max(0.0, min(1.0, float(value)))
self._propagate_hillshade_to_children()
[docs]
def _propagate_hillshade_to_children(self):
"""Copy active hillshade params to every child array.
Handles Wolfresults_2D (via iter_current_arrays) and
WolfArrayMB (via myblocks).
"""
if self._hillshade_sync:
return
src = self.active_hillshade_params
# Wolfresults_2D — propagate to each block's current array
so = getattr(self, 'selected_object', None)
if so is not None and isinstance(so, Wolfresults_2D):
for wa in so.iter_current_arrays():
if hasattr(wa, 'hillshade_params') and wa.hillshade_params is not src:
wa.hillshade_params.z_exaggeration = src.z_exaggeration
wa.hillshade_params.specular = src.specular
wa.hillshade_params.glossiness = src.glossiness
wa.hillshade_params.highlight = src.highlight
return
# WolfArrayMB — propagate to each sub-block
aa = self.active_array
if aa is not None and isinstance(aa, WolfArrayMB):
for blk in aa.myblocks.values():
if hasattr(blk, 'hillshade_params') and blk.hillshade_params is not src:
blk.hillshade_params.z_exaggeration = src.z_exaggeration
blk.hillshade_params.specular = src.specular
blk.hillshade_params.glossiness = src.glossiness
blk.hillshade_params.highlight = src.highlight
[docs]
def _show_hillshade_panel(self):
"""Show (or raise) the persistent hillshade control panel."""
if self._hillshade_panel is not None:
try:
self._hillshade_panel.Raise()
return
except RuntimeError:
self._hillshade_panel = None
self._hillshade_panel = HillshadePanel(self)
self._hillshade_panel.Show()
self._hillshade_overlay = HillshadeOverlay(self)
self.Refresh()
[docs]
def _refresh_hillshade_panel_for_active(self):
"""Refresh the hillshade panel controls after the active array changed."""
if self._hillshade_panel is None:
return
try:
self._hillshade_panel.refresh_from_params()
except RuntimeError:
pass
self.Refresh()
[docs]
def SetCurrentContext(self):
""" Set the current OGL context if exists otherwise return False """
if self.context is None:
return False
return self.canvas.SetCurrent(self.context)
[docs]
def Paint(self, ignore_overlays: bool = False):
""" Plotting elements on canvas """
if self.currently_readresults:
logging.warning(_('Currently reading results -- No painting on canvas !'))
return
width, height = self.canvas.GetSize()
# C'est bien ici que la zone de dessin utile est calculée sur base du centre et de la zone en coordonnées réelles
# Les commandes OpenGL sont donc traitées en coordonnées réelles puisque la commande glOrtho définit le cadre visible
self.xmin = self._center_x - self.width / 2.
self.ymin = self._center_y - self.height / 2.
self.xmax = self._center_x + self.width / 2.
self.ymax = self._center_y + self.height / 2.
# Track viewport bounds for async background updates
current_bounds = (self.xmin, self.xmax, self.ymin, self.ymax)
# If bounds have changed AND enough time has passed, trigger async background image updates
# Adaptive throttling: use higher frequency (30 Hz) during rapid zoom/scroll events,
# otherwise use standard frequency (10 Hz)
current_time = time_module.time()
time_since_last_update = (current_time - self._last_async_background_update_time
if self._last_async_background_update_time is not None
else float('inf'))
# Initialize bounds change time on first call
if self._last_bounds_change_time is None:
self._last_bounds_change_time = current_time
# Time since last bounds change (to detect scroll/zoom activity)
time_since_last_bounds_change = current_time - self._last_bounds_change_time
# Detect scroll activity: if bounds changed within last 0.5 sec, we're in "zoom mode"
is_scrolling = time_since_last_bounds_change < 0.5
if self._last_paint_bounds != current_bounds:
# Bounds have changed - user is panning/zooming
# Update the bounds change timestamp
self._last_bounds_change_time = current_time
self._last_paint_bounds = current_bounds
# Initialize async update time on first movement if not already set
if self._last_async_background_update_time is None:
self._last_async_background_update_time = current_time
# Don't update backgrounds during rapid movement to keep interaction smooth
# Schedule a refresh after scroll stabilization to trigger background reload
def schedule_stabilization_refresh():
self.Refresh()
wx.CallLater(600, schedule_stabilization_refresh) # 600ms after typical 0.5s stabilization
else:
# Bounds haven't changed - user has stopped moving
# If movement has stopped for >= 0.5 sec, reload backgrounds
if not is_scrolling:
# User movement has stopped - reload backgrounds if enough time since last update
if time_since_last_update >= 1.0: # Reload at most once per second
if self.enable_async_background_updates:
self._update_background_async()
# else:
# self._update_background()
self._last_async_background_update_time = current_time
if self.SetCurrentContext():
bkg_color = self.bkg_color
glClearColor(bkg_color[0]/255., bkg_color[1]/255., bkg_color[2]/255., bkg_color[3]/255.)
# glClearColor(0., 0., 1., 0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glDisable(GL_DEPTH_TEST)
glViewport(0, 0, int(width), int(height))
self._set_gl_projection_matrix()
# dessin du background
self._plotting(draw_type.WMSBACK)
# Dessin des matrices
self._plotting(draw_type.ARRAYS)
# Dessin des résultats 2D
self._plotting(draw_type.RES2D)
# Dessin des vecteurs
self._plotting(draw_type.VECTORS)
# Dessin des tuiles
self._plotting(draw_type.TILES)
self._plotting(draw_type.IMAGESTILES)
if self.active_vector is not None:
if self.active_vector.parentzone is None:
# we must plot this vector because it is a temporary vector outside any zone
self.active_vector.plot()
# Dessin des triangulations
self._plotting(draw_type.TRIANGULATION)
# Dessin des nuages
self._plotting(draw_type.CLOUD)
if self.active_cloud is not None and self.active_cloud_vertex_id is not None:
if getattr(self.active_cloud.myprop, 'highlightselectedpoint', True):
try:
self.active_cloud.plot_highlight_vertex(self.active_cloud_vertex_id)
except Exception:
pass
# Dessin des vues
self._plotting(draw_type.VIEWS)
# Dessin des "particule systems"
self._plotting(draw_type.PARTICLE_SYSTEM)
# Dessin du reste
self._plotting(draw_type.OTHER)
# Dessin du noyé
self._plotting(draw_type.DROWNING)
# Dessin du Front
self._plotting(draw_type.WMSFORE)
# Dessin des images
self._plotting(draw_type.PICTURECOLLECTION)
# Dessin des QDF/IDF
if self.active_qdfidf is not None:
self.active_qdfidf.plot(sx = self.sx, sy=self.sy,
xmin=self.xmin, ymin=self.ymin,
xmax=self.xmax, ymax=self.ymax,
size = (self.xmax - self.xmin) / 100.)
# Gestion des BC (si actif)
if self.active_bc is not None:
self.active_bc.plot()
if self._tmp_vector_distance is not None:
self._tmp_vector_distance.plot()
if self.active_vector is not None:
if getIfromRGB(self.active_vector_color) != self.active_vector.myprop.color:
old = self.active_vector.myprop.color
self.active_vector.myprop.color = getIfromRGB(self.active_vector_color)
self.active_vector.plot()
self.active_vector._plot_square_at_vertices(size = self.active_vector_square_size)
self.active_vector._plot_fill_anim_center_preview()
self.active_vector.myprop.color = old
else:
self.active_vector._plot_square_at_vertices(size = self.active_vector_square_size)
self.active_vector._plot_fill_anim_center_preview()
_sq_px = self.active_vector_square_size
if self.active_vector.myprop.plot_indices:
self.active_vector._plot_all_indices(sx = self.sx, sy=self.sy,
xmin=self.xmin, ymin=self.ymin,
xmax=self.xmax, ymax=self.ymax,
size = (self.xmax - self.xmin) / 100.,
square_size_px=_sq_px)
elif self.active_vertex is not None:
if self.action == ActionKind.CAPTURE_VERTICES:
# During vertex capture, draw indices for all fixed
# vertices (skip the last one which tracks the mouse)
for _idx in range(self.active_vector.nbvertices - 1):
self.active_vector._plot_index_vertex(idx = _idx,
sx = self.sx, sy=self.sy,
xmin=self.xmin, ymin=self.ymin,
xmax=self.xmax, ymax=self.ymax,
size = (self.xmax - self.xmin) / 100.,
square_size_px=_sq_px)
else:
self.active_vector._plot_index_vertex(idx = self.active_vector.myvertices.index(self.active_vertex),
sx = self.sx, sy=self.sy,
xmin=self.xmin, ymin=self.ymin,
xmax=self.xmax, ymax=self.ymax,
size = (self.xmax - self.xmin) / 100.,
square_size_px=_sq_px)
if self.active_vector.myprop.plot_lengths:
self.active_vector._plot_all_lengths2D(sx = self.sx, sy=self.sy,
xmin=self.xmin, ymin=self.ymin,
xmax=self.xmax, ymax=self.ymax,
size = (self.xmax - self.xmin) / 100.)
# Plugin paint hook — after all data layers, before UI overlays.
if not ignore_overlays and self.action in self._custom_paint_handlers:
self._custom_paint_handlers[self.action](self)
# Sculpt brush cursor (world-coordinate circle/square)
if not ignore_overlays and self.action == ActionKind.SCULPT:
self._draw_sculpt_cursor()
# Profile brush ghost cursor (segment + corridor)
if not ignore_overlays and self.action == ActionKind.PROFILE:
self._draw_profile_cursor()
# Cut / fill earthwork HUD (active during sculpt and profile)
if not ignore_overlays and self.action in ('sculpt', 'profile'):
self._cutfill_overlay.draw()
if not ignore_overlays:
self._plot_grid_transform_overlay()
self._plot_mouse_xy_overlay()
self._plot_distance_overlay()
if self._hillshade_overlay is not None:
self._hillshade_overlay.draw()
if self._palette_overlay is not None:
self._palette_overlay.draw()
if self._toolbar_overlay is not None:
self._toolbar_overlay.draw()
self.canvas.SwapBuffers()
else:
raise NameError(
'Opengl setcurrent -- maybe a conflict with an existing opengl32.dll file - please rename the opengl32.dll in the libs directory and retry')
[docs]
def OnPaint(self, e):
""" event handler for paint event"""
self.Paint()
if e is not None:
e.Skip()
[docs]
def findminmax(self, force=False):
""" Find min/max of all objects """
# FIXME : use iterator
xmin = 1.e30
ymin = 1.e30
xmax = -1.e30
ymax = -1.e30
k = 0
for locarray in self.myarrays:
if locarray.plotted or force:
[xmin_arr, xmax_arr], [ymin_arr, ymax_arr] = locarray.get_bounds()
xmin = min(xmin, xmin_arr)
xmax = max(xmax, xmax_arr)
ymin = min(ymin, ymin_arr)
ymax = max(ymax, ymax_arr)
k += 1
for locvector in self.myvectors:
if locvector.plotted or force:
if locvector.idx != 'grid':
locvector.find_minmax()
if isinstance(locvector,Zones):
xmin = min(locvector.xmin, xmin)
xmax = max(locvector.xmax, xmax)
ymin = min(locvector.ymin, ymin)
ymax = max(locvector.ymax, ymax)
elif isinstance(locvector,Bridges):
xmin = min(locvector.xmin, xmin)
xmax = max(locvector.xmax, xmax)
ymin = min(locvector.ymin, ymin)
ymax = max(locvector.ymax, ymax)
elif isinstance(locvector,crosssections):
xmin = min(locvector.xmin, xmin)
xmax = max(locvector.xmax, xmax)
ymin = min(locvector.ymin, ymin)
ymax = max(locvector.ymax, ymax)
k += 1
for locvector in self.myimagestiles:
if locvector.plotted or force:
locvector.find_minmax()
if isinstance(locvector,ImagesTiles):
xmin = min(locvector.xmin, xmin)
xmax = max(locvector.xmax, xmax)
ymin = min(locvector.ymin, ymin)
ymax = max(locvector.ymax, ymax)
k += 1
for locvector in self.mypicturecollections:
if locvector.plotted or force:
locvector.find_minmax()
xmin = min(locvector.xmin, xmin)
xmax = max(locvector.xmax, xmax)
ymin = min(locvector.ymin, ymin)
ymax = max(locvector.ymax, ymax)
k += 1
for locvector in self.mytiles:
if locvector.plotted or force:
if locvector.idx != 'grid':
locvector.find_minmax()
if isinstance(locvector,Zones):
xmin = min(locvector.xmin, xmin)
xmax = max(locvector.xmax, xmax)
ymin = min(locvector.ymin, ymin)
ymax = max(locvector.ymax, ymax)
elif isinstance(locvector,Bridges):
xmin = min(locvector.xmin, xmin)
xmax = max(locvector.xmax, xmax)
ymin = min(locvector.ymin, ymin)
ymax = max(locvector.ymax, ymax)
elif isinstance(locvector,crosssections):
xmin = min(locvector.xmin, xmin)
xmax = max(locvector.xmax, xmax)
ymin = min(locvector.ymin, ymin)
ymax = max(locvector.ymax, ymax)
k += 1
for loccloud in self.myclouds:
if loccloud.plotted or force:
loccloud.find_minmax(force)
xmin = min(loccloud.xbounds[0], xmin)
xmax = max(loccloud.xbounds[1], xmax)
ymin = min(loccloud.ybounds[0], ymin)
ymax = max(loccloud.ybounds[1], ymax)
k += 1
for loctri in self.mytri:
if loctri.plotted or force:
loctri.find_minmax(force)
xmin = min(loctri.xmin, xmin)
xmax = max(loctri.xmax, xmax)
ymin = min(loctri.ymin, ymin)
ymax = max(loctri.ymax, ymax)
k += 1
for locres2d in self.myres2D:
locres2d:Wolfresults_2D
if locres2d.plotted or force:
locres2d.find_minmax(force)
xmin = min(locres2d.xmin, xmin)
xmax = max(locres2d.xmax, xmax)
ymin = min(locres2d.ymin, ymin)
ymax = max(locres2d.ymax, ymax)
k += 1
for locps in self.mypartsystems:
locps:Particle_system
if locps.plotted or force:
locps.find_minmax(force)
xmin = min(locps.xmin, xmin)
xmax = max(locps.xmax, xmax)
ymin = min(locps.ymin, ymin)
ymax = max(locps.ymax, ymax)
k += 1
for locview in self.myviews:
locview.find_minmax(force)
xmin = min(locview.xmin, xmin)
xmax = max(locview.xmax, xmax)
ymin = min(locview.ymin, ymin)
ymax = max(locview.ymax, ymax)
k += 1
for locothers in self.myothers:
if type(locothers) in [genericImagetexture]: #, hydrometry_wolfgui]:
xmin = min(locothers.xmin, xmin)
xmax = max(locothers.xmax, xmax)
ymin = min(locothers.ymin, ymin)
ymax = max(locothers.ymax, ymax)
k += 1
elif type(locothers) in [PlansTerrier]: #, hydrometry_wolfgui]:
if locothers.initialized:
xmin = min(locothers.xmin, xmin)
xmax = max(locothers.xmax, xmax)
ymin = min(locothers.ymin, ymin)
ymax = max(locothers.ymax, ymax)
k += 1
elif type(locothers) in [Particularites, Enquetes, Ouvrages, Profils]:
if locothers.initialized:
xmin = min(locothers.xmin, xmin)
xmax = max(locothers.xmax, xmax)
ymin = min(locothers.ymin, ymin)
ymax = max(locothers.ymax, ymax)
k += 1
elif isinstance(locothers, Element_To_Draw):
# Generic support for custom drawable assets added in 'others'.
if locothers.plotted or force:
try:
locothers.find_minmax(force)
except TypeError:
locothers.find_minmax()
xmin = min(locothers.xmin, xmin)
xmax = max(locothers.xmax, xmax)
ymin = min(locothers.ymin, ymin)
ymax = max(locothers.ymax, ymax)
k += 1
for drown in self.mydrownings:
if drown.plotted or force:
drown.find_minmax(force)
xmin = min(drown.xmin, xmin)
xmax = max(drown.xmax, xmax)
ymin = min(drown.ymin, ymin)
ymax = max(drown.ymax, ymax)
k += 1
if k > 0:
self.xmin = xmin
self.xmax = xmax
self.ymin = ymin
self.ymax = ymax
[docs]
def resizeFrame(self, w:int, h:int):
""" Resize the frame
:param w: width in pixels
:param h: height in pixels
"""
self.SetClientSize(w, h)
[docs]
def mimicme(self):
"""
Report des caractéristiques de la fenêtre sur les autres éléments liés
"""
if self.linked and self.forcemimic:
if not self.linkedList is None:
width, height = self.GetClientSize()
curFrame: WolfMapViewer
for curFrame in self.linkedList:
curFrame.forcemimic = False
for curFrame in self.linkedList:
if curFrame != self:
curFrame.resizeFrame(width, height)
curFrame._center_x = self._center_x
curFrame._center_y = self._center_y
curFrame.sx = self.sx
curFrame.sy = self.sy
curFrame.width = self.width
curFrame.height = self.height
curFrame.setbounds()
if curFrame.link_shareopsvect:
if curFrame.active_vector is not self.active_vector:
curFrame.Active_vector(self.active_vector)
if curFrame.active_array.myops.active_vector is not self.active_vector:
curFrame.active_array.myops.Active_vector(self.active_vector, False)
curFrame.action = self.action
for curFrame in self.linkedList:
curFrame.forcemimic = True
[docs]
def mimicme_copyfrom(self):
if self.linked and self.forcemimic:
if not self.linkedList is None:
width, height = self.GetClientSize()
curFrame: WolfMapViewer
for curFrame in self.linkedList:
curFrame.forcemimic = False
for curFrame in self.linkedList:
if curFrame != self:
curFrame.copyfrom = self.copyfrom
for curFrame in self.linkedList:
curFrame.forcemimic = True
[docs]
def Active_vector(self, vect):
""" Active un vecteur et sa zone parent si existante """
self.active_vector = vect
if vect is not None:
logging.info(_('Activating vector : ' + vect.myname))
if vect.parentzone is not None:
self.Active_zone(vect.parentzone)
self.mimicme()
self.Paint()
[docs]
def Active_zone(self, zone: zone):
""" Active une zone et son parent si existant """
self.active_zone = zone
self.active_zones = zone.parent
logging.info(_('Activating zone : ' + zone.myname))
[docs]
def list_background(self):
return [cur.idx for cur in self.mywmsback]
[docs]
def list_foreground(self):
return [cur.idx for cur in self.mywmsfore]
[docs]
def check_id(self, id=str, gridsize = 100.):
""" Check an element from its id """
curobj = self.getobj_from_id(id)
if curobj is None:
logging.warning('Bad id')
return
curobj.check_plot()
curitem = self.gettreeitem(curobj)
self.treelist.CheckItem(curitem, True)
if id == 'grid':
curobj.creategrid(gridsize, self.xmin, self.ymin, self.xmax, self.ymax)
[docs]
def uncheck_id(self, id=str, unload=True, forceresetOGL=True, askquestion=False):
""" Uncheck an element from its id """
curobj = self.getobj_from_id(id)
if curobj is None:
logging.warning('Bad id')
return
if issubclass(type(curobj), WolfArray):
curobj.uncheck_plot(unload, forceresetOGL, askquestion)
else:
curobj.uncheck_plot()
curitem = self.gettreeitem(curobj)
self.treelist.UncheckItem(curitem)
[docs]
def get_current_zoom(self):
"""
Get the current zoom
:return: dict with keys 'center', 'xmin', 'xmax', 'ymin', 'ymax', 'width', 'height'
"""
return {'center': (self._center_x, self._center_y),
'xmin' : self.xmin,
'xmax' : self.xmax,
'ymin' : self.ymin,
'ymax' : self.ymax,
'width' : self.xmax-self.xmin,
'height' : self.ymax-self.ymin}
[docs]
def save_current_zoom(self, filepath):
""" Save the current zoom in a json file """
zoom = self.get_current_zoom()
with open(filepath, 'w') as fp:
json.dump(zoom, fp)
[docs]
def read_current_zoom(self, filepath):
""" Read the current zoom from a json file """
if exists(filepath):
with open(filepath, 'r') as fp:
zoom = json.load(fp)
self.zoom_on(zoom)
[docs]
def OnAddBridge(self, e: wx.Event):
self._bridge_mgr.on_add(e)
[docs]
def OnEditBridge(self, e: wx.Event):
self._bridge_mgr.on_edit(e)
[docs]
def OnFindBridge(self, e: wx.Event):
self._bridge_mgr.on_find(e)
[docs]
def pick_bridge(self, x: float, y: float):
self._bridge_mgr.pick(x, y)
[docs]
def OnAddWeir(self, e: wx.Event):
self._weir_mgr.on_add(e)
[docs]
def OnEditWeir(self, e: wx.Event):
self._weir_mgr.on_edit(e)
[docs]
def OnFindWeir(self, e: wx.Event):
self._weir_mgr.on_find(e)
[docs]
def pick_weir(self, x: float, y: float):
self._weir_mgr.pick(x, y)
[docs]
class Compare_Arrays_Results():
def __init__(self, parent:WolfMapViewer = None, share_cmap_array:bool = False, share_cmap_diff:bool = False):
[docs]
self.mapviewers_diff = []
[docs]
self.share_cmap_array = share_cmap_array
[docs]
self.share_cmap_diff = share_cmap_diff
[docs]
self.type = Comp_Type.ARRAYS
[docs]
self._initialized_viewers = False
[docs]
self.independent = True
[docs]
def _check_type(self, file:Path):
"""
Check the type of the file/directory
If it is a file and suffix is empty, it is considered as RES2D.
If it is a directory and contains a simul_gpu_results, it is considered as RES2D_GPU.
If it is a file and suffix is not empty, it is considered as ARRAYS. A check is done to see if it is a multi-block array.
"""
file = Path(file)
if file.suffix == '' and not file.is_dir():
return Comp_Type.RES2D, file
elif file.suffix in ('.bin', '.tif', '.tiff', '.npy', '.npz', '.top', '.frott', '.nap', '.hbin', '.hbinb', '.qxbin', '.qxbinb', '.qybin', '.qybinb', '.inf') :
if file.suffix in ('.bin', '.top', '.frott', '.nap', '.hbin', '.hbinb', '.qxbin', '.qxbinb', '.qybin', '.qybinb', '.inf'):
if file.with_suffix(file.suffix + '.txt').exists():
test = WolfArray(file, preload=False)
test.read_txt_header()
mb = test.nb_blocks > 0
if mb:
return Comp_Type.ARRAYS_MB, file
return Comp_Type.ARRAYS, file
elif (file.parent / 'simul_gpu_results').exists():
file = file.parent / 'simul_gpu_results'
return Comp_Type.RES2D_GPU, file
elif (file.parent.parent / 'simul_gpu_results').exists():
file = file.parent.parent / 'simul_gpu_results'
return Comp_Type.RES2D_GPU, file
else:
return None, None
[docs]
def add(self, file_or_dir:Union[str, Path] = None):
if file_or_dir is None:
filterProject = "all (*.*)|*.*"
filename = self._dialogs.ask_file_open("Choose array/model", wildcard=filterProject, parent=self)
if filename is None:
return False
filename = Path(filename)
self.paths.append(self._check_type(filename))
if self.paths[-1][0] is None:
logging.warning(_('File type not recognized -- Retry !'))
self.paths.pop()
return False
return True
[docs]
def check(self):
""" Check the consystency of the elements to compare """
reftype = self.paths[0][0]
for cur in self.paths:
if cur[0] != reftype:
logging.warning(_('Inconsistency in the type of the elements to compare'))
return False
return True
[docs]
def update_comp(self, idx=list[int]):
"""
Update Arrays from 2D modellings
:param idx: indexes of the time step to update --> steps to read
"""
assert self.type in (Comp_Type.RES2D, Comp_Type.RES2D_GPU), 'This method is only for 2D results'
self.linked_elts = []
for curelt, curstep in zip(self.elements, idx):
curelt.read_oneresult(curstep)
self.linked_elts.append(curelt.as_WolfArray())
for curelt, curlink in zip(self.elements, self.linked_elts):
curlink.idx = curelt.idx + ' ' + curelt.get_currentview().value
self.set_diff()
if self._initialized_viewers:
self.update_viewers()
[docs]
def update_type_result(self, newtype):
"""
Update the result type for each element
"""
assert newtype in views_2D, 'This type is not a 2D result'
assert self.type in (Comp_Type.RES2D, Comp_Type.RES2D_GPU), 'This method is only for 2D results'
for curelt in self.elements:
curelt.set_currentview(newtype, force_updatepal = True)
# remove elements
for baselt, curelt, curmap in zip(self.elements, self.linked_elts, self.mapviewers):
curmap.removeobj_from_id(curelt.idx)
for curdiff, curmap in zip(self.diff, self.mapviewers_diff):
curmap.removeobj_from_id(curdiff.idx)
self.update_comp(self.times.get_times_idx())
[docs]
def set_elements(self):
""" Set the elements to compare with the right type """
from .ui.wolf_times_selection_comparison_models import Times_Selection
if self.check():
self.type = self.paths[0][0]
if self.type == Comp_Type.RES2D_GPU:
self.parent.menu_wolf2d()
self.elements = [wolfres2DGPU(cur[1], plotted=False, idx = cur[1].name + '_' + str(idx)) for idx, cur in enumerate(self.paths)]
times = [curmod.get_times_steps()[0] for curmod in self.elements]
self.times = Times_Selection(self, wx.ID_ANY, _("Times"), size=(400,400), times = times, callback = self.update_comp)
self.times.Show()
elif self.type == Comp_Type.RES2D:
self.parent.menu_wolf2d()
self.elements = [Wolfresults_2D(cur[1], plotted=False, idx = cur[1].name + '_' + str(idx)) for idx, cur in enumerate(self.paths)]
times = [curmod.get_times_steps()[0] for curmod in self.elements]
self.times = Times_Selection(self, wx.ID_ANY, _("Times"), size=(400,400), times = times, callback = self.update_comp)
self.times.Show()
elif self.type == Comp_Type.ARRAYS:
self.elements = [WolfArray(cur[1], plotted=False, idx = cur[1].name + '_' + str(idx)) for idx, cur in enumerate(self.paths)]
elif self.type == Comp_Type.ARRAYS_MB:
self.elements = [WolfArrayMB(cur[1], plotted=False, idx = cur[1].name + '_' + str(idx)) for idx, cur in enumerate(self.paths)]
[docs]
def set_diff(self):
""" Set the differential between the elements and the first one, which is the reference """
if self.type in (Comp_Type.ARRAYS, Comp_Type.ARRAYS_MB):
ref = self.elements[0]
# Recherche d'un masque union des masques partiels
ref.mask_unions(self.elements[1:])
# Création du différentiel -- Les opérateurs mathématiques sont surchargés
self.diff = [cur - ref for cur in self.elements[1:]]
for curdiff, cur in zip(self.diff, self.elements[1:]):
curdiff.idx = _('Difference') + cur.idx +' - ' + ref.idx
elif self.type in (Comp_Type.RES2D, Comp_Type.RES2D_GPU):
if len(self.linked_elts) == 0:
self.update_comp([-1] * len(self.elements))
elif len(self.linked_elts) == len(self.elements):
ref = self.linked_elts[0]
self.diff = [cur - ref for cur in self.linked_elts[1:]]
for curdiff, cur in zip(self.diff, self.linked_elts[1:]):
curdiff.idx = _('Difference') + cur.idx +' - ' + ref.idx
[docs]
def set_viewers(self, independent:bool = None):
"""
Set viewers
"""
if independent is None:
self.independent = wx.MessageDialog(None, _("Create a viewer for each element ?"), _("Viewers"), style=wx.YES_NO | wx.YES_DEFAULT).ShowModal() == wx.ID_YES
else:
self.independent = independent
if not self.independent:
self.mapviewers = [self.parent] * len(self.elements)
self.mapviewers_diff = self.mapviewers
else:
# Création de plusieurs fenêtres de visualisation basées sur la classe "WolfMapViewer"
self.mapviewers = []
self.mapviewers.append(self.parent) # parent as viewer for first element
for id, file in enumerate(self.elements[1:]):
self.mapviewers.append(WolfMapViewer(None, file.idx, w=600, h=600, wxlogging=self.parent.wxlogging, wolfparent = self.parent.wolfparent))
self.mapviewers_diff.append(WolfMapViewer(None, 'Difference' + file.idx, w=600, h=600, wxlogging=self.parent.wxlogging, wolfparent = self.parent.wolfparent))
for curviewer in self.mapviewers[1:] + self.mapviewers_diff:
curviewer.add_grid()
curviewer.add_WMS()
for curviewer in self.mapviewers + self.mapviewers_diff:
curviewer.linked = True
curviewer.linkedList = self.mapviewers + self.mapviewers_diff
self._initialized_viewers = True
self.update_viewers()
[docs]
def set_shields_param(self, diamsize:float = .001, graindensity:float = 2.65):
""" Set the parameters for the shields diagram """
for curelt in self.elements:
curelt.sediment_diameter = diamsize
curelt.sediment_density = graindensity
curelt.load_default_colormap('shields_cst')
[docs]
def update_viewers(self):
""" Update the viewers with the new elements """
if self.type in (Comp_Type.ARRAYS, Comp_Type.ARRAYS_MB):
elts = self.elements
elif self.type in (Comp_Type.RES2D, Comp_Type.RES2D_GPU):
elts = self.linked_elts
# on attribue une matrice par interface graphique
ref = elts[0]
for baselt, curelt, curmap in zip(self.elements, elts, self.mapviewers):
# if self.type in (Comp_Type.RES2D, Comp_Type.RES2D_GPU):
# curmap.active_res2d = baselt
curmap.removeobj_from_id(curelt.idx)
curelt.change_gui(curmap)
curmap.active_array = curelt
curelt.myops.myzones = ref.myops.myzones
# diff = self.diff[0]
for curdiff, curmap in zip(self.diff, self.mapviewers_diff):
curmap.removeobj_from_id(curdiff.idx)
curdiff.change_gui(curmap)
curmap.active_array = curdiff
curdiff.myops.myzones = ref.myops.myzones
# on partage la palette de couleurs
ref.mypal.automatic = False
ref.myops.palauto.SetValue(0)
if self.share_cmap_array:
for curelt in elts[1:]:
curelt.mypal.automatic = False
curelt.myops.palauto.SetValue(0)
ref.add_crosslinked_array(curelt)
ref.share_palette()
else:
for curelt in elts[1:]:
curelt.mypal.automatic = False
curelt.myops.palauto.SetValue(0)
curelt.mypal.updatefrompalette(ref.mypal)
#palette de la différence
diff = self.diff[0]
diff.mypal = wolfpalette()
if isinstance(diff, WolfArrayMB):
diff.link_palette()
path = os.path.dirname(__file__)
fn = join(path, 'models\\diff16.pal')
diff.mypal.readfile(fn)
diff.mypal.automatic = False
diff.myops.palauto.SetValue(0)
if self.share_cmap_diff:
for curelt in self.diff[1:]:
curelt.mypal.automatic = False
curelt.myops.palauto.SetValue(0)
diff.add_crosslinked_array(curelt)
diff.share_palette()
else:
for curelt in self.diff[1:]:
curelt.mypal.automatic = False
curelt.myops.palauto.SetValue(0)
curelt.mypal.updatefrompalette(diff.mypal)
# Ajout des matrices dans les fenêtres de visualisation
for curelt, curmap in zip(elts, self.mapviewers):
curmap.add_object('array', newobj = curelt, ToCheck = True, id = curelt.idx)
for curdiff, curmap in zip(self.diff, self.mapviewers_diff):
curmap.add_object('array', newobj = curdiff, ToCheck = True, id = curdiff.idx)
if self.independent:
for curmap in self.mapviewers + self.mapviewers_diff:
curmap.Refresh()
else:
self.mapviewers[0].Refresh()
[docs]
def bake(self):
self.set_elements()
self.set_diff()
self.set_viewers()
[docs]
class InPaint_waterlevel(wx.Dialog):
def __init__(self, parent, title:str = _('Inpainting'), size:tuple[int,int] = (400,400), mapviewer:WolfMapViewer=None, **kwargs):
super().__init__(parent, title = title, size = size, **kwargs)
[docs]
self._array: WolfArray = None
[docs]
self._dem: WolfArray = None
[docs]
self._dtm: WolfArray = None
[docs]
self._mapviewer = mapviewer
self._init_UI()
[docs]
def _init_UI(self):
""" Create 2 listboxes for the arrays and the masks """
import shutil
if self._mapviewer is None:
logging.warning(_('No mapviewer --> Nothing to do'))
return
self._sizer = wx.BoxSizer(wx.VERTICAL)
self._sizer_lists = wx.BoxSizer(wx.HORIZONTAL)
self._sizer_arrays = wx.BoxSizer(wx.VERTICAL)
self._sizer_ignore = wx.BoxSizer(wx.VERTICAL)
self._label_arrays = wx.StaticText(self, wx.ID_ANY, _('Array'))
self._listbox_arrays = wx.ListBox(self, wx.ID_ANY, choices = self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS), style = wx.LB_SINGLE)
self._label_dem = wx.StaticText(self, wx.ID_ANY, _('DEM'))
self._listbox_dems = wx.ListBox(self, wx.ID_ANY, choices = ['None'] + self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS), style = wx.LB_SINGLE)
self._label_dtm = wx.StaticText(self, wx.ID_ANY, _('DTM'))
self._listbox_dtm = wx.ListBox(self, wx.ID_ANY, choices = ['None'] + self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS), style = wx.LB_SINGLE)
self._label_ignore = wx.StaticText(self, wx.ID_ANY, _('Ignore last holes'))
self._listbox_ignore = wx.ListBox(self, wx.ID_ANY, choices = [str(i) for i in range(10)], style = wx.LB_SINGLE)
self._sizer_ignore.Add(self._label_ignore, 0, wx.EXPAND)
self._sizer_ignore.Add(self._listbox_ignore, 1, wx.EXPAND)
self._sizer_arrays.Add(self._label_arrays, 0, wx.EXPAND)
self._sizer_arrays.Add(self._listbox_arrays, 1, wx.EXPAND)
self._sizer_arrays.Add(self._label_dem, 0, wx.EXPAND)
self._sizer_arrays.Add(self._listbox_dems, 1, wx.EXPAND)
self._sizer_arrays.Add(self._label_dtm, 0, wx.EXPAND)
self._sizer_arrays.Add(self._listbox_dtm, 1, wx.EXPAND)
self._sizer_lists.Add(self._sizer_arrays, 1, wx.EXPAND)
self._sizer_lists.Add(self._sizer_ignore, 1, wx.EXPAND)
self._sizer.Add(self._sizer_lists, 1, wx.EXPAND)
self._sizer_btns = wx.BoxSizer(wx.HORIZONTAL)
self._sizer_inpaint = wx.BoxSizer(wx.VERTICAL)
self._btn_inpaint = wx.Button(self, wx.ID_ANY, _('Inpaint'))
self._sizer_inpaint.Add(self._btn_inpaint, 1, wx.EXPAND)
self._check_fortran = wx.CheckBox(self, wx.ID_ANY, _('Use Fortran'))
self._check_fortran.SetValue(False)
if shutil.which('holes.exe') is not None:
self._sizer_inpaint.Add(self._check_fortran, 1, wx.EXPAND)
self._btn_update_ids = wx.Button(self, wx.ID_ANY, _('Update IDs'))
self._btn_select_holes = wx.Button(self, wx.ID_ANY, _('Select holes'))
self._btn_create_mask = wx.Button(self, wx.ID_ANY, _('Create mask'))
self._sizer_btns.Add(self._sizer_inpaint, 1, wx.EXPAND)
self._sizer_btns.Add(self._btn_update_ids, 1, wx.EXPAND)
self._sizer_btns.Add(self._btn_select_holes, 1, wx.EXPAND)
self._sizer_btns.Add(self._btn_create_mask, 1, wx.EXPAND)
self._sizer.Add(self._sizer_btns, 1, wx.EXPAND)
self.SetSizer(self._sizer)
self._listbox_arrays.Bind(wx.EVT_LISTBOX, self.OnSelectArray)
self._listbox_dems.Bind(wx.EVT_LISTBOX, self.OnSelectMask)
self._listbox_dtm.Bind(wx.EVT_LISTBOX, self.OnSelectDTM)
self._btn_inpaint.Bind(wx.EVT_BUTTON, self.OnInpaint)
self._btn_update_ids.Bind(wx.EVT_BUTTON, self.OnUpdateIDs)
self._btn_select_holes.Bind(wx.EVT_BUTTON, self.OnSelectHoles)
self._btn_create_mask.Bind(wx.EVT_BUTTON, self.OnCreateMask)
self._listbox_dems.SetSelection(0)
self._listbox_dtm.SetSelection(0)
self._listbox_ignore.SetSelection(1)
self.CenterOnScreen()
self.Show()
[docs]
def OnUpdateIDs(self, e):
""" Update the list of arrays/mask/dtm """
self._listbox_arrays.Set(self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS))
self._listbox_dems.Set(['None'] + self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS))
self._listbox_dtm.Set(['None'] + self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS))
[docs]
def OnSelectArray(self, e):
""" Select an array """
self._array = self._mapviewer.getobj_from_id(self._listbox_arrays.GetStringSelection())
[docs]
def OnSelectMask(self, e):
""" Select a mask """
mask_ = self._listbox_dems.GetStringSelection()
if mask_ == 'None':
self._dem = None
else:
self._dem = self._mapviewer.getobj_from_id(self._listbox_dems.GetStringSelection())
[docs]
def OnSelectDTM(self, e):
""" Select a DTM """
dtm_ = self._listbox_dtm.GetStringSelection()
if dtm_ == 'None':
self._dtm = None
else:
self._dtm = self._mapviewer.getobj_from_id(self._listbox_dtm.GetStringSelection())
[docs]
def OnInpaint(self, e):
""" Inpaint the array with the mask """
if self._array is None:
logging.warning(_('Select an array, a mask and a DTM'))
else:
times, wl, wd = self._array._inpaint_waterlevel_dem_dtm(self._dem, self._dtm, ignore_last= self._listbox_ignore.GetSelection(), use_fortran= self._check_fortran.GetValue())
logging.info(_('Inpainting done !'))
if self._dialogs.ask_yes_no(_('Add water depth to the viewer ?'), _('Water depth'), style=DialogStyles.YES_NO_DEFAULT_YES):
self._mapviewer.add_object('array', newobj = wd, id = 'wd_' + self._array.idx)
[docs]
def OnSelectHoles(self, e):
""" Select the holes in the array """
if self._array is None:
logging.warning(_('Select an array'))
return
self._array.select_holes(ignore_last = self._listbox_ignore.GetSelection())
self._mapviewer.Paint()
[docs]
def OnCreateMask(self, e):
""" Create a mask from the array """
if self._array is None:
logging.warning(_('Select an array, a mask and a DTM'))
return
if self._dem is None:
logging.warning(_('Select a dem'))
return
if self._dtm is None:
logging.warning(_('Select a dtm'))
return
newmask = self._array._create_building_holes_dem_dtm(self._dem, self._dtm, ignore_last= self._listbox_ignore.GetSelection())
self._mapviewer.add_object('array', newobj = newmask, id = 'mask_' + self._array.idx)
[docs]
class InPaint_array(wx.Dialog):
def __init__(self, parent, title:str = _('Inpainting'), size:tuple[int,int] = (400,400), mapviewer:WolfMapViewer=None, **kwargs):
super().__init__(parent, title = title, size = size, **kwargs)
[docs]
self._array: WolfArray = None
[docs]
self._mask: WolfArray = None
[docs]
self._test: WolfArray = None
[docs]
self._mapviewer = mapviewer
self._init_UI()
[docs]
def _init_UI(self):
""" Create 2 listboxes for the arrays and the masks """
import shutil
if self._mapviewer is None:
logging.warning(_('No mapviewer --> Nothing to do'))
return
self._sizer = wx.BoxSizer(wx.VERTICAL)
self._sizer_lists = wx.BoxSizer(wx.HORIZONTAL)
self._sizer_arrays = wx.BoxSizer(wx.VERTICAL)
self._sizer_ignore = wx.BoxSizer(wx.VERTICAL)
self._label_arrays = wx.StaticText(self, wx.ID_ANY, _('Array'))
self._listbox_arrays = wx.ListBox(self, wx.ID_ANY, choices = self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS), style = wx.LB_SINGLE)
self._label_masks = wx.StaticText(self, wx.ID_ANY, _('Mask == where to inpaint'))
self._listbox_masks = wx.ListBox(self, wx.ID_ANY, choices = ['None'] + self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS), style = wx.LB_SINGLE)
self._label_test = wx.StaticText(self, wx.ID_ANY, _('Test == local inpainted value must be greater than this value'))
self._listbox_test = wx.ListBox(self, wx.ID_ANY, choices = ['None'] + self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS), style = wx.LB_SINGLE)
self._label_ignore = wx.StaticText(self, wx.ID_ANY, _('Ignore last holes'))
self._listbox_ignore = wx.ListBox(self, wx.ID_ANY, choices = [str(i) for i in range(10)], style = wx.LB_SINGLE)
self._sizer_ignore.Add(self._label_ignore, 0, wx.EXPAND)
self._sizer_ignore.Add(self._listbox_ignore, 1, wx.EXPAND)
self._sizer_arrays.Add(self._label_arrays, 0, wx.EXPAND)
self._sizer_arrays.Add(self._listbox_arrays, 1, wx.EXPAND)
self._sizer_arrays.Add(self._label_masks, 0, wx.EXPAND)
self._sizer_arrays.Add(self._listbox_masks, 1, wx.EXPAND)
self._sizer_arrays.Add(self._label_test, 0, wx.EXPAND)
self._sizer_arrays.Add(self._listbox_test, 1, wx.EXPAND)
self._sizer_lists.Add(self._sizer_arrays, 1, wx.EXPAND)
self._sizer_lists.Add(self._sizer_ignore, 1, wx.EXPAND)
self._sizer.Add(self._sizer_lists, 1, wx.EXPAND)
self._sizer_btns = wx.BoxSizer(wx.HORIZONTAL)
self._sizer_inpaint = wx.BoxSizer(wx.VERTICAL)
self._btn_inpaint = wx.Button(self, wx.ID_ANY, _('Inpaint'))
self._sizer_inpaint.Add(self._btn_inpaint, 1, wx.EXPAND)
self._btn_update_ids = wx.Button(self, wx.ID_ANY, _('Update IDs'))
self._btn_select_holes = wx.Button(self, wx.ID_ANY, _('Select holes'))
self._btn_create_mask = wx.Button(self, wx.ID_ANY, _('Create mask'))
self._sizer_btns.Add(self._sizer_inpaint, 1, wx.EXPAND)
self._sizer_btns.Add(self._btn_update_ids, 1, wx.EXPAND)
self._sizer_btns.Add(self._btn_select_holes, 1, wx.EXPAND)
self._sizer_btns.Add(self._btn_create_mask, 1, wx.EXPAND)
self._sizer.Add(self._sizer_btns, 1, wx.EXPAND)
self.SetSizer(self._sizer)
self._listbox_arrays.Bind(wx.EVT_LISTBOX, self.OnSelectArray)
self._listbox_masks.Bind(wx.EVT_LISTBOX, self.OnSelectMask)
self._listbox_test.Bind(wx.EVT_LISTBOX, self.OnSelectTest)
self._btn_inpaint.Bind(wx.EVT_BUTTON, self.OnInpaint)
self._btn_update_ids.Bind(wx.EVT_BUTTON, self.OnUpdateIDs)
self._btn_select_holes.Bind(wx.EVT_BUTTON, self.OnSelectHoles)
self._btn_create_mask.Bind(wx.EVT_BUTTON, self.OnCreateMask)
self._listbox_masks.SetSelection(0)
self._listbox_test.SetSelection(0)
self._listbox_ignore.SetSelection(0)
self.CenterOnScreen()
self.Show()
[docs]
def OnUpdateIDs(self, e):
""" Update the list of arrays/mask/dtm """
self._listbox_arrays.Set(self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS))
self._listbox_masks.Set(['None'] + self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS))
self._listbox_test.Set(['None'] + self._mapviewer.get_list_keys(drawing_type = draw_type.ARRAYS))
[docs]
def OnSelectArray(self, e):
""" Select an array """
self._array = self._mapviewer.getobj_from_id(self._listbox_arrays.GetStringSelection())
[docs]
def OnSelectMask(self, e):
""" Select a mask """
mask_ = self._listbox_masks.GetStringSelection()
if mask_ == 'None':
self._mask = None
else:
self._mask = self._mapviewer.getobj_from_id(self._listbox_masks.GetStringSelection())
[docs]
def OnSelectTest(self, e):
""" Select a DTM """
dtm_ = self._listbox_test.GetStringSelection()
if dtm_ == 'None':
self._test = None
else:
self._test = self._mapviewer.getobj_from_id(self._listbox_test.GetStringSelection())
[docs]
def OnInpaint(self, e):
""" Inpaint the array with the mask """
if self._array is None:
logging.warning(_('Select an array, a mask and a DTM'))
else:
times, wl, wd = self._array.inpaint(self._mask, self._test, ignore_last= self._listbox_ignore.GetSelection())
logging.info(_('Inpainting done !'))
if self._dialogs.ask_yes_no(_('Add extra array to the viewer ?'), _('Inerpolation - test data'), style=DialogStyles.YES_NO_DEFAULT_YES):
self._mapviewer.add_object('array', newobj = wd, id = 'extra_' + self._array.idx)
[docs]
def OnSelectHoles(self, e):
""" Select the holes in the array """
if self._mask is None:
logging.warning(_('Select a mask array'))
return
self._mask.select_holes(ignore_last = self._listbox_ignore.GetSelection())
self._mapviewer.Paint()
[docs]
def OnCreateMask(self, e):
""" Create a mask from the array """
if self._array is None:
logging.warning(_('Select an array'))
return
newmask = self._array.create_mask_holes(ignore_last= self._listbox_ignore.GetSelection())
self._mapviewer.add_object('array', newobj = newmask, id = 'mask_' + self._array.idx)