Source code for wolfhece.PyDraw

"""
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] THROTTLING_FREQUENCY_SCROLL = 30 # Higher frequency during rapid scroll/zoom events (30 Hz = 0.033 sec minimum)
[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_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] _T = TypeVar('_T')
[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] myothers: list
[docs] myviews:list[views_2D]
[docs] mywmsback: list
[docs] mywmsfore: list
[docs] myres2D: list
[docs] mytiles: list[Tiles]
[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] mytooltip: Wolf_Param # Objet WOLF permettant l'analyse de ce qui est sous la souris
[docs] treelist: TreeListCtrl # Gestion des éléments sous forme d'arbre
[docs] _lbl_selecteditem: StaticText
[docs] leftbox: BoxSizer
# 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 check_tooltip(self): """ Check if tooltip window is shown """ if self.mytooltip is None: logging.info(_('No tooltip window')) return self.mytooltip.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 check_2D_MB_headers(self): """ Check headers of a 2D simulation without opening viewer""" # Check 2D simulation filename = self._dialogs.ask_file_open(_("Choose 2D simulation file"), parent=self) if filename is None: return from .mesh2d.wolf2dprev import prev_sim2D sim = prev_sim2D(filename) sim.verify_files()
[docs] def get_mapviewer(self): """ Retourne une instance WolfMapViewer """ return self
[docs] def do_quit(self): pass
[docs] def create_triangles_menu(self): """ Menu for triangulations """ if self.trianglesmenu is None: self.trianglesmenu = wx.Menu() self.menubar.Append(self.trianglesmenu, _('&Triangulations')) self._menuinteractptri = self.trianglesmenu.Append(wx.ID_ANY, _("Interpolate on active triangulation..."), _('Interpolate active array on active triangulation')) self._menuinteractptri_above = self.trianglesmenu.Append(wx.ID_ANY, _("Interpolate on active triangulation (keep only above)..."), _('Interpolate active array on active triangulation but keep only values above the array')) self._menuinteractptri_below = self.trianglesmenu.Append(wx.ID_ANY, _("Interpolate on active triangulation (keep only below)..."), _('Interpolate active array on active triangulation but keep only values below the array')) self._menucomparetri = self.trianglesmenu.Append(wx.ID_ANY, _("Compare triangles to array..."), _("Comparison")) self._menumovetri = self.trianglesmenu.Append(wx.ID_ANY, _("Move triangles..."), _("Move triangles")) self._menurotatetri = self.trianglesmenu.Append(wx.ID_ANY, _("Rotate triangles..."), _("Rotate triangles")) self.trianglesmenu.Bind(wx.EVT_MENU, lambda e: self.interpolate_triangulation(keep='all'), self._menuinteractptri) self.trianglesmenu.Bind(wx.EVT_MENU, lambda e: self.interpolate_triangulation(keep='above'), self._menuinteractptri_above) self.trianglesmenu.Bind(wx.EVT_MENU, lambda e: self.interpolate_triangulation(keep='below'), self._menuinteractptri_below) self.trianglesmenu.Bind(wx.EVT_MENU, lambda e: self.compare_tri2array(), self._menucomparetri) self.trianglesmenu.Bind(wx.EVT_MENU, lambda e: self.move_triangles(), self._menumovetri) self.trianglesmenu.Bind(wx.EVT_MENU, lambda e: self.rotate_triangles(), self._menurotatetri)
[docs] def create_cloud_menu(self): """ Menu for cloud points """ if self.cloudmenu is None: self.cloudmenu = wx.Menu() self.menubar.Append(self.cloudmenu, _('Cloud')) interpcloudonarray = self.cloudmenu.Append(wx.ID_ANY, _("Interpolate active cloud on active array..."), _("Interpolation")) self._menucomparecloud = self.cloudmenu.Append(wx.ID_ANY, _("Compare cloud to array..."), _("Comparison")) split_cloud = self.cloudmenu.Append(wx.ID_ANY, _("Split cloud..."), _("Split cloud according to the active vector")) self.cloudmenu.Bind(wx.EVT_MENU, lambda e: self.interpolate_cloud(), interpcloudonarray) self.cloudmenu.Bind(wx.EVT_MENU, lambda e: self.compare_cloud2array(), self._menucomparecloud) self.cloudmenu.Bind(wx.EVT_MENU, self._on_split_cloud, split_cloud)
[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 menu_tiles(self): """ Menu for tiles """ self._tiles_mgr.menu_build()
[docs] def pîck_image_tile(self, event: wx.Event): self._tiles_mgr.on_pick_image_tile(event)
[docs] def menu_pictcollection(self): """ Menu for picture collections """ self._pictcollection.menu_build()
[docs] def menu_qdfidf(self): self._qdfidf.menu_build()
[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 menu_imagestiles(self): """ Menu for image tiles """ # Handled by _tiles_mgr.menu_build() pass
[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_laz(self): self._laz_mgr.menu_build()
[docs] def menu_wolf2d(self): self._wolf2dresults_mgr.menu_build()
[docs] def menu_alaro_forecasts(self): self._alaro.menu_build()
[docs] def Onmenualaro(self, event: wx.MenuEvent): warnings.warn("Onmenualaro is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[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)
[docs] def Onmenuwalous_ocs(self, event: wx.MenuEvent): warnings.warn("Onmenuwalous_ocs is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[docs] def Onmenuwalous_uts(self, event: wx.MenuEvent): warnings.warn("Onmenuwalous_uts 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 menu_drowning(self): self._drowning.menu_build()
[docs] def Onmenudrowning(self, event: wx.MenuEvent): warnings.warn("Onmenudrowning is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[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 menu_dike(self): self._dike.menu_build()
[docs] def Onmenudike(self, event: wx.MenuEvent): warnings.warn("Onmenudike is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[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 menu_lidaxe(self): self._lidaxe.menu_build()
[docs] def Onmenulidaxe(self, event: wx.MenuEvent): warnings.warn("Onmenulidaxe is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[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 Onmenuwolf2d(self, event: wx.MenuEvent): warnings.warn("Onmenuwolf2d is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[docs] def menu_2dgpu(self): self._wolf2dresults_mgr.menu_build_gpu_ext()
[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 menu_sim2D(self): """ Menu for 2D simulations """ self._simtools2d_mgr.menu_build()
[docs] def menu_sim2DGPU(self): """ Menu for 2D GPU simulations """ self._simtools2d_gpu_mgr.menu_build()
[docs] def Onmenusim2DGPU(self, event: wx.MenuEvent): warnings.warn("Onmenusim2DGPU is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[docs] def Onmenusim2D(self, event: wx.MenuEvent): warnings.warn("Onmenusim2D is deprecated — menu uses per-item bindings", DeprecationWarning, stacklevel=2)
[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
[docs] def toolbar_tooltip_font_size(self) -> int: """Return font size in pixels used by toolbar tooltip text.""" config = self.get_configuration() if config is None: return 13 try: return max(6, int(config[ConfigurationKeys.TOOLBAR_TOOLTIP_FONT_SIZE])) except Exception: return 13
@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_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 OnMenuHighlight(self, event:wx.MenuEvent): id = event.GetId() item:wx.MenuItem item = self.menubar.FindItemById(event.GetId()) if item is not None: self.set_statusbar_text(item.GetHelp())
[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_check_headers(self, event): self.check_2D_MB_headers()
[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_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_uniform_parts(self, event): self.uniforminparts_all(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 uniforminparts_all(self, TrueOrFalse:bool): for curarray in self.myarrays: curarray:WolfArray if curarray.plotted: curarray.mypal.interval_cst = TrueOrFalse curarray.reset_plot() for curarray in self.myres2D: curarray:Wolfresults_2D if curarray.plotted: curarray.mypal.interval_cst = TrueOrFalse curarray.link_palette() curarray.reset_plot()
[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()
[docs] def OnShowPopup(self, event): """Deprecated — context menu is now built and shown directly by OntreeRight.""" import warnings warnings.warn( 'OnShowPopup is deprecated and no longer functional. ' 'Context menus are built and shown by OntreeRight.', DeprecationWarning, stacklevel=2, )
[docs] def OnPopupItemSelected(self, event): """Deprecated — event routing is now handled by direct wx.EVT_MENU bindings in OntreeRight.""" import warnings warnings.warn( 'OnPopupItemSelected is deprecated and no longer functional. ' 'Each popup item is bound directly to its _popup_* handler in OntreeRight.', DeprecationWarning, stacklevel=2, )
# ---- Popup item handlers -------------------------------------------------
[docs] def _popup_up(self) -> None: self.upobj()
[docs] def _popup_down(self) -> None: self.downobj()
[docs] def _popup_delete(self) -> None: self.removeobj()
[docs] def _popup_check_uncheck(self) -> None: self.checkuncheckobj()
[docs] def _popup_save(self) -> None: if self.selected_object is not None: if issubclass(type(self.selected_object), WolfArray): self.selected_object.write_all() elif type(self.selected_object) is Zones: self.selected_object.saveas() elif type(self.selected_object) in [Bridge, Weir]: self.selected_object.saveas() elif type(self.selected_object) is Triangulation: self.selected_object.saveas() elif isinstance(self.selected_object, Particle_system): self.selected_object.save() elif isinstance(self.selected_object, Drowning_victim_Viewer): self.selected_object.save() elif isinstance(self.selected_object, DikeWolf): self.selected_object.save() elif isinstance(self.selected_object, InjectorDike): self.selected_object.save() elif isinstance(self.selected_object, (cloud_vertices, cloud_of_clouds)): self.selected_object.save_json(self.selected_object.filename)
[docs] def _popup_save_as(self) -> None: if self.selected_object is not None: if issubclass(type(self.selected_object), WolfArray): filterArray = "bin (*.bin)|*.bin|Geotif (*.tif)|*.tif|Numpy (*.npy)|*.npy|all (*.*)|*.*" chosen = self._dialogs.ask_file_save( "Choose file name for Array : " + self.selected_object.idx, wildcard=filterArray, parent=self, ) if chosen is not None: self.selected_object.filename = chosen self.selected_object.write_all() elif type(self.selected_object) in [Zones, Bridge, Weir]: filterArray = "vec (*.vec)|*.vec|vecz (*.vecz)|*.vecz|Shapefile (*.shp)|*.shp|all (*.*)|*.*" chosen = self._dialogs.ask_file_save( "Choose file name for Vector :" + self.selected_object.idx, wildcard=filterArray, parent=self, ) if chosen is not None: self.selected_object.saveas(chosen) elif type(self.selected_object) in [PictureCollection, Particularites, Enquetes, Ouvrages, Profils]: filterArray = "vec (*.vec)|*.vec|vecz (*.vecz)|*.vecz" chosen = self._dialogs.ask_file_save( "Choose file name for Collection :" + self.selected_object.idx, wildcard=filterArray, parent=self, ) if chosen is not None: self.selected_object.saveas(chosen) elif type(self.selected_object) is Triangulation: filterArray = "tri (*.tri)|*.tri|all (*.*)|*.*" chosen = self._dialogs.ask_file_save( "Choose file name for triangulation :" + self.selected_object.idx, wildcard=filterArray, parent=self, ) if chosen is not None: self.selected_object.saveas(chosen) elif isinstance(self.selected_object, Particle_system): filterArray = "json (*.json)|*.json|all (*.*)|*.*" chosen = self._dialogs.ask_file_save( "Choose file name for particle system :" + self.selected_object.idx, wildcard=filterArray, parent=self, ) if chosen is not None: self.selected_object.save(chosen) elif isinstance(self.selected_object, DikeWolf): self.selected_object.save_as() elif isinstance(self.selected_object, InjectorDike): self.selected_object.save_as() elif isinstance(self.selected_object, Wolf_LAZ_Data): filterArray = "Dump (*.dump)|*.dmp|all (*.*)|*.*" chosen = self._dialogs.ask_file_save( "Choose file name for LAZ data :" + self.selected_object.idx, wildcard=filterArray, parent=self, ) if chosen is not None: self.selected_object.saveas(chosen) elif isinstance(self.selected_object, Drowning_victim_Viewer): self.selected_object.saveas() elif isinstance(self.selected_object, crosssections): filterArray = "vecz (*.vecz)|*.vecz|SXY (*.sxy)|*.sxy" chosen = self._dialogs.ask_file_save( "Choose file name for Cross Sections :" + self.selected_object.idx, wildcard=filterArray, parent=self, ) if chosen is not None: self.selected_object.saveas(chosen) elif isinstance(self.selected_object, (cloud_vertices, cloud_of_clouds)): filterArray = "json (*.json)|*.json|all (*.*)|*.*" chosen = self._dialogs.ask_file_save( "Choose file name for cloud vertices :" + self.selected_object.idx, wildcard=filterArray, parent=self, ) if chosen is not None: self.selected_object.save_json(chosen)
[docs] def _popup_rename(self) -> None: if self.selected_object is not None: all_ids = [x.lower() for x in self.get_list_ids(checked_state=None)] label = self.selected_object.idx all_ids.remove(label.lower()) newlab = self._dialogs.ask_text(_('Chose a new label :'), default=label, parent=self) if newlab is not None: if newlab.lower() in all_ids: wx.MessageBox(_('This label already exists. Please choose another one.'), _('Error'), wx.OK | wx.ICON_ERROR) return self.selected_object.idx = newlab self.treelist.SetItemText(self.selected_treeitem, newlab) if self.get_label_selecteditem() == _("Active : ") + label: self.set_label_selecteditem(_("Active : ") + newlab)
[docs] def _popup_duplicate(self) -> None: if self.selected_object is not None: label = self.selected_object.idx + '_copy' newlab = self._dialogs.ask_text(_('Chose a label for the copy:'), default=label, parent=self) if newlab is None: return if isinstance(self.selected_object, WolfArray) and (not type(self.selected_object) in [WolfArrayMB, WolfArrayMNAP]): curtype = self.selected_object.dtype if curtype == np.float64: curtype = 'float64' elif curtype == np.float32: curtype = 'float32' elif curtype == np.int32: curtype = 'int32' elif curtype == np.int16: curtype = 'int16' elif curtype == np.int8: curtype = 'int8' if self._dialogs.ask_confirmation(_('The type of the data is {}.\nDo you want to change this type?'.format(curtype)), default='no', parent=self): _choices = ['float32','float64','int32','int16','int8'] _sel = self._dialogs.ask_single_choice(_('Choose a type'), _('Type'), _choices, parent=self) if _sel is None: return idx = _choices.index(_sel) if idx == 0: curtype = WOLF_ARRAY_FULL_SINGLE elif idx == 1: curtype = WOLF_ARRAY_FULL_DOUBLE elif idx == 2: curtype = WOLF_ARRAY_FULL_INTEGER elif idx == 3: curtype = WOLF_ARRAY_FULL_INTEGER16 elif idx == 4: curtype = WOLF_ARRAY_FULL_INTEGER8 newarray = WolfArray(srcheader=self.selected_object.get_header(), whichtype=curtype, nullvalue=self.selected_object.nullvalue) newarray.allocate_ressources() asnewtype = self.selected_object.array.data.astype(newarray.dtype) newarray.array.data[:,:] = asnewtype[:,:] newarray.copy_mask(self.selected_object, forcenullvalue=True, link=False) else: newarray = WolfArray(mold=self.selected_object) self.add_object('array', newobj=newarray, id=newlab) self.Refresh() elif isinstance(self.selected_object, cloud_vertices): newcloud = self.selected_object.duplicate() newcloud.idx = newlab self.add_object('cloud', newobj=newcloud, id=newlab) self.Refresh() elif isinstance(self.selected_object, cloud_of_clouds): newcloud = self.selected_object.duplicate() newcloud.idx = newlab self.add_object('clouds', newobj=newcloud, id=newlab) self.Refresh() else: logging.warning(_('Not yet implemented'))
[docs] def _popup_properties(self) -> None: myobj = self.selected_object if type(myobj) in [WolfArray, WolfArrayMB, WolfArrayMNAP, Zones, Wolfresults_2D, wolfres2DGPU, Particle_system, Picc_data, Cadaster_data, hydrometry_wolfgui, Bridge, Weir, Wolf_LAZ_Data, DikeWolf, Drowning_victim_Viewer, InjectorDike]: myobj.show_properties() elif isinstance(myobj, (cloud_vertices, cloud_of_clouds)): myobj.show_properties()
[docs] def _popup_boundary_conditions(self) -> None: bc = self.get_boundary_manager(self.selected_object) if bc is not None: bc.Show()
[docs] def _popup_contours(self) -> None: if isinstance(self.selected_object, WolfArray): cont = self.selected_object.contour() cont.prep_listogl() self.add_object('vector', newobj=cont, id=cont.idx) self.Paint()
[docs] def _popup_rebin(self) -> None: if isinstance(self.selected_object, WolfArray): res = self._dialogs.ask_float(_('Enter the rebin factor (>1 will decrease the resolution, <1 will increase the resolution) :'), _('Rebin'), default='1') if res is not None: ops = ['Mean', 'Sum', 'Max', 'Min', 'Median'] op = self._dialogs.ask_single_choice(_('Choose the operation'), _('Operation'), ops, parent=self) if op is None: logging.info(_('Rebin cancelled')) return self.selected_object.rebin(res, op.lower()) else: logging.warning(_('Rebin not yet implemented for this type of object'))
[docs] def _popup_set_nullvalue(self) -> None: if isinstance(self.selected_object, WolfArray): res = self._dialogs.ask_float(_('Enter the new null value :'), _('Set NullValue'), default=self.selected_object.array.data[0,0]) if res is not None: self.selected_object.nullvalue = res self.selected_object.mask_data(res) self.selected_object.reset_plot() self.Refresh()
[docs] def _popup_reload(self) -> None: if isinstance(self.selected_object, WolfArray): if self.selected_object.filename is not None: if self._dialogs.ask_yes_no(_('Do you want to reload the file ?'), _('Reload'), wx.YES_NO | wx.NO_DEFAULT): self.selected_object.read_all() self.selected_object.mask_data(self.selected_object.nullvalue) self.selected_object.reset_plot() else: logging.warning(_('Reload not yet implemented for this type of object'))
[docs] def _popup_clip_from_view(self) -> None: from .array_core.clipping import ClipZoneMixin as _CZM if isinstance(self.selected_object, _CZM): existing_view_clips = [cz for cz in self.selected_object.clip_zones if not cz.sliders] invert = len(existing_view_clips) > 0 self.selected_object.setup_clip_from_view(self.xmin, self.ymin, self.xmax, self.ymax, invert=invert) self.Refresh()
[docs] def _popup_clip_h_slider(self) -> None: from .array_core.clipping import ClipSliderEdge, ClipZoneMixin as _CZM if isinstance(self.selected_object, _CZM): cz, slider = self.selected_object.setup_curtain_clip(edge=ClipSliderEdge.TOP) self.Refresh()
[docs] def _popup_clip_v_slider(self) -> None: from .array_core.clipping import ClipSliderEdge, ClipZoneMixin as _CZM if isinstance(self.selected_object, _CZM): cz, slider = self.selected_object.setup_curtain_clip(edge=ClipSliderEdge.RIGHT) self.Refresh()
[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 _popup_clip_save_config(self) -> None: from .array_core.clipping import ClipConfigs, ClipZoneMixin as _CZM if isinstance(self.selected_object, _CZM): path = self._dialogs.ask_file_save( _('Save clip configuration'), wildcard='JSON files (*.json)|*.json', parent=self, ) if path is not None: cfg = self.selected_object.snapshot_clip_config(name=self.selected_object.idx) configs = ClipConfigs() configs.add(cfg) configs.save(path)
[docs] def _popup_clip_load_config(self) -> None: from .array_core.clipping import ClipConfigs, ClipZoneMixin as _CZM if isinstance(self.selected_object, _CZM): path = self._dialogs.ask_file_open( _('Load clip configuration'), wildcard='JSON files (*.json)|*.json', style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST, parent=self, ) if path is not None: configs = ClipConfigs() configs.load(path) if len(configs) > 0: cfg = list(configs.configs.values())[0] self.selected_object.apply_clip_config(cfg) self.Refresh()
[docs] def _popup_clip_remove_all(self) -> None: from .array_core.clipping import ClipZoneMixin as _CZM if isinstance(self.selected_object, _CZM): self.selected_object.clear_clip_zones() self.Refresh()
[docs] def _popup_clip_toggle_bars(self) -> None: from .array_core.clipping import ClipZoneMixin as _CZM if isinstance(self.selected_object, _CZM): self.selected_object._clip_show_bars = not self.selected_object._clip_show_bars self.Refresh()
[docs] def _popup_convert_mono(self) -> None: if isinstance(self.selected_object, WolfArrayMB): mono = self.selected_object.as_WolfArray() self.add_object('array', newobj=mono, id=self.selected_object.idx + '_mono') logging.info(_('Mono-block created and added to the viewer')) elif isinstance(self.selected_object, Wolfresults_2D): mono = self.selected_object.as_WolfArray() if isinstance(mono, WolfArrayMB): mono = mono.as_WolfArray() self.add_object('array', newobj=mono, id=self.selected_object.idx + '_mono') logging.info(_('Mono-block created and added to the viewer')) else: logging.warning(_('Convert to mono-blocks not yet implemented for this type of object'))
[docs] def _popup_convert_multi(self) -> None: if isinstance(self.selected_object, Wolfresults_2D): mb = self.selected_object.as_WolfArray(force_mb=True) if isinstance(mb, WolfArrayMB): logging.info(_('Multi-blocks created and added to the viewer')) elif isinstance(mb, WolfArray): logging.warning(_('Mono-blocks created and added to the viewer -- Instead of multi-blocks as only one block was found')) self.add_object('array', newobj=mb, id=self.selected_object.idx + '_mb') else: logging.warning(_('Convert to multi-blocks not yet implemented for this type of object'))
[docs] def _popup_extract_ic(self) -> None: if isinstance(self.selected_object, Wolfresults_2D): if self.selected_object.current_result is None or self.selected_object.current_result < 0: logging.warning(_('No current step defined')) return import shutil logging.info(_('Extracting current step as IC')) dir = Path(self.selected_object.filename).parent files = ['h.npy', 'qx.npy', 'qy.npy'] for curfile in files: if (dir / curfile).exists(): logging.info(f'File {curfile} already exists -- renaming it') shutil.copyfile(dir / curfile, dir / ('__' + curfile)) logging.info(_('Extracting current step as IC')) self.selected_object.export_as(Path(self.selected_object.filename).parent, [views_2D.WATERDEPTH, views_2D.QX, views_2D.QY], 'numpy', False) logging.info(_('Done !'))
[docs] def _popup_export_shapefile(self) -> None: if isinstance(self.selected_object, Zones | Bridge | Weir): filterArray = "Shapefile (*.shp)|*.shp" path = self._dialogs.ask_file_save( "Choose file name for Zones :" + self.selected_object.idx, wildcard=filterArray, parent=self, ) if path is not None: self.selected_object.export_to_shapefile(path)
[docs] def _popup_export_active_zone_shapefile(self) -> None: if isinstance(self.selected_object, Zones | Bridge | Weir): filterArray = "Shapefile (*.shp)|*.shp" path = self._dialogs.ask_file_save( "Choose file name for Vector :" + self.selected_object.idx, wildcard=filterArray, parent=self, ) if path is not None: self.selected_object.export_active_zone_to_shapefile(path)
[docs] def _popup_laz_set_colormap(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): self.selected_object.associated_color = self._choice_laz_colormap()
[docs] def _popup_laz_edit_colormap(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): self.selected_object.interactive_update_colors()
[docs] def _popup_laz_set_classification(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): self.selected_object.set_classification(self._choice_laz_classification())
[docs] def _popup_laz_edit_selection(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): self.selected_object._edit_selection()
[docs] def _popup_laz_all_to_cloud(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): if self.selected_object.num_points > 100000: if not self._dialogs.ask_confirmation(_('The number of points is high, it could take some time to convert to cloud.\nDo you want to continue ?'), _('Warning'), default='no', parent=self): return newcloud = cloud_vertices() newcloud.init_from_nparray(self.selected_object.xyz) self.add_object('cloud', newobj=newcloud, id=self.selected_object.idx + '_cloud')
[docs] def _popup_laz_selection_to_cloud(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): xyz = self.selected_object.xyz_selected if xyz.shape[0] == 0: logging.warning('No points selected') return if xyz.shape[0] > 100000: if not self._dialogs.ask_confirmation(_('The number of points is high, it could take some time to convert to cloud.\nDo you want to continue ?'), _('Warning'), default='no', parent=self): return newcloud = cloud_vertices() newcloud.init_from_nparray(xyz) self.add_object('cloud', newobj=newcloud, id=self.selected_object.idx + '_cloud_sel')
[docs] def _popup_laz_selection_to_vector(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): if self.active_zone is None: logging.warning(_('No active zone selected')) return xyz = self.selected_object.xyz_selected if xyz.shape[0] == 0: logging.warning(_('No points selected')) if self.selected_object._myprops[('Selection', 'Codes')] != '': logging.info(_('You filtered the points with the codes : {}'.format(self.selected_object._myprops[('Selection', 'Codes')]))) return if xyz.shape[0] > 100000: if not self._dialogs.ask_confirmation(_('The number of points is high, it could take some time to convert to cloud.\nDo you want to continue ?'), _('Warning'), default='no', parent=self): return def approximate_vector(xyz): """ Get a cloud of points and return a vector based on the best approximated segment and points projected on its trace """ model = linear_model.RANSACRegressor() model.fit(xyz[:,0].reshape(-1,1), xyz[:,1]) proj = model.predict(xyz[:,0].reshape(-1,1)) xyz_proj = np.zeros((xyz.shape[0],3)) xyz_proj[:,0] = xyz[:,0] xyz_proj[:,1] = proj xyz_proj[:,2] = xyz[:,2] idx = np.argsort(xyz_proj[:,0]) xyz_proj = xyz_proj[idx] return xyz_proj newvector = vector(name=self.selected_object.idx + '_vector_sel', fromnumpy=approximate_vector(xyz)) self.active_zone.add_vector(newvector, forceparent=True) self.active_zone.parent.find_minmax(True) self.active_zone.parent.reset_listogl() self.active_zone.parent.fill_structure() self.Refresh()
[docs] def _popup_laz_play(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): self.selected_object.play_flight()
[docs] def _popup_laz_add_point(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): self.selected_object.add_pose_in_memory()
[docs] def _popup_laz_record(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): path = self._dialogs.ask_directory(_('Choose a directory to save the video'), style=wx.DD_DEFAULT_STYLE, parent=self) if path is not None: self.selected_object.record_flight(path)
[docs] def _popup_laz_load_flight(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): path = self._dialogs.ask_file_open( _('Choose a file to load the flight'), wildcard='JSON (*.json)|*.json|All (*.*)|*.*', parent=self, ) if path is not None: self.selected_object.load_flight(path)
[docs] def _popup_laz_save_flight(self) -> None: if isinstance(self.selected_object, Wolf_LAZ_Data): path = self._dialogs.ask_file_save( _('Choose a file to save the flight'), wildcard='JSON (*.json)|*.json|All (*.*)|*.*', parent=self, ) if path is not None: self.selected_object.save_flight(path)
[docs] def _popup_rasterize_zone(self) -> None: if self.active_array is None and self.active_res2d is None: logging.warning(_('No active array selected')) return if self.active_array is not None and self.active_res2d is not None: choice = self._dialogs.ask_single_choice(_('Choose the type of rasterization'), _('Rasterization Type'), ['Array', 'Res2D'], parent=self) if choice is None: return if choice == 'Array': based_array = self.active_array else: if self.active_res2d.nb_blocks == 1: based_array = self.active_res2d.get_header_block(1) elif self.active_res2d.nb_blocks > 1: logging.error(_('Rasterization not implemented for multi-blocks res2d -- Convert to mono-blocks first')) return elif self.active_array is not None: based_array = self.active_array elif self.active_res2d is not None: if self.active_res2d.nb_blocks == 1: based_array = self.active_res2d.get_header_block(1) elif self.active_res2d.nb_blocks > 1: logging.error(_('Rasterization not implemented for multi-blocks res2d -- Convert to mono-blocks first')) return if isinstance(self.selected_object, Zones): if self.selected_object.active_zone is None: logging.warning(_('No active zone selected')) return if self.selected_object.active_zone.parent is None: logging.warning(_('No parent object for the active zone')) else: new_zone = based_array.rasterize_zone_along_grid(self.selected_object.active_zone, outformat=zone) if new_zone is not None: new_zone.myname = self.selected_object.active_zone.myname + '_rasterized' self.selected_object.active_zone.parent.add_zone(new_zone, forceparent=True) self.selected_object.active_zone.parent.fill_structure() self.selected_object.active_zone.reset_listogl()
[docs] def _popup_rasterize_vector(self) -> None: if self.active_array is None and self.active_res2d is None: logging.warning(_('No active array selected')) return if self.active_array is not None and self.active_res2d is not None: choice = self._dialogs.ask_single_choice(_('Choose the type of rasterization'), _('Rasterization Type'), ['Array', 'Res2D'], parent=self) if choice is None: return if choice == 'Array': based_array = self.active_array else: if self.active_res2d.nb_blocks == 1: based_array = self.active_res2d.get_header_block(1) elif self.active_res2d.nb_blocks > 1: logging.error(_('Rasterization not implemented for multi-blocks res2d -- Convert to mono-blocks first')) return elif self.active_array is not None: based_array = self.active_array elif self.active_res2d is not None: if self.active_res2d.nb_blocks == 1: based_array = self.active_res2d.get_header_block(1) elif self.active_res2d.nb_blocks > 1: logging.error(_('Rasterization not implemented for multi-blocks res2d -- Convert to mono-blocks first')) return if isinstance(self.selected_object, Zones): if self.selected_object.active_vector is None: logging.warning(_('No active vector selected')) return vec_raster = based_array.rasterize_vector_along_grid(self.selected_object.active_vector) if vec_raster is not None: vec_raster.myname = self.selected_object.active_vector.myname + '_rasterized' self.selected_object.active_vector.parentzone.add_vector(vec_raster, forceparent=True, update_struct=True) self.selected_object.active_vector.parentzone.reset_listogl()
[docs] def _popup_extrude(self) -> None: if isinstance(self.selected_object, Picc_data): if self.active_array is None: logging.warning(_('No active array selected')) return logging.info(_('Extruding polygons on active array')) logging.info(_('Please wait, it could take some time...')) self.selected_object.extrude_polygons(self.active_array) logging.info(_('Extrusion done !')) self.active_array.reset_plot() self.Refresh()
[docs] def _popup_interpolate_on_array(self) -> None: if isinstance(self.selected_object, Zones): if self.active_array is None: logging.warning(_('No active array selected')) return choice = self._dialogs.ask_single_choice(_('With respect to the array values, keep the extruded values which are ?'), _('Interpolate on active array'), ['Above', 'Below', 'All'], parent=self) if choice is None: return choice = choice.lower() logging.info(_('Interpolating polygons on active array')) logging.info(_('Please wait, it could take some time...')) for curzone in self.selected_object.myzones: self.active_array.interpolate_on_polygons(curzone, keep=choice) logging.info(_('Interpolation done !'))
[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_Mouse_Button(self, e: wx.MouseEvent): d = e.GetWheelDelta() r = e.GetWheelRotation() a = e.GetWheelAxis() altdown = e.AltDown() ctrldown = e.ControlDown() shiftdown = e.ShiftDown() spacedown = wx.GetKeyState(wx.WXK_SPACE) # Sculpt brush wheel controls (consumed first when sculpt action is active) if self._sculpt.on_wheel(self._wx_mouse_context(e)): return # Hillshade overlay wheel controls (only when panel is open) if self._hillshade_overlay is not None and (ctrldown or shiftdown): if self._hillshade_overlay.handle_wheel(r, d, ctrldown, shiftdown): return if self.action == ActionKind.DYNAMIC_PARALLEL and shiftdown and not ctrldown: self.dynapar_dist *= (1 - .1 * (r / max(d, 1))) self.dynapar_dist = max(self.dynapar_dist, .01) self.active_zone.parallel_active(self.dynapar_dist) self.Refresh() return elif self.action == ActionKind.DYNAMIC_PARALLEL and shiftdown and ctrldown: _ds = self._dialogs.ask_integer(_('What is the desired size [cm] ?'), 'ds', 'ds size', int(self.dynapar_dist * 100.), 1, 100000) if _ds is None: return self.dynapar_dist = float(_ds) / 100. self.dynapar_dist = max(self.dynapar_dist, .01) self.active_zone.parallel_active(self.dynapar_dist) self.Refresh() return elif shiftdown: if self.active_vector is None: logging.warning(_('No vector selected -- Please select a vector first !')) return if self.active_vector.myprop.textureimage is None: logging.warning(_('No image available -- Please load an image first !')) return self.active_vector.myprop.image_scale /= (1 - .1 * (r / max(d, 1))) # limit to 1. or upper self.active_vector.myprop.image_scale = max(self.active_vector.myprop.image_scale, 1.) self.active_vector.myprop.update_myprops() self.active_vector.myprop.update_image_texture() self.Refresh() return # Allow the user to zoom onto the pixel where the # mouse cursor is # Step1: move the map so that the pixem under the mouse cursor # ends up right in the middle of the screen (this move is # not visible from the end user point of view, it's just # here to make computation seasier) if spacedown: self.center_view_on( *self.getXY( e.GetPosition())) # Zoom/dezoom, center pf the tranfromation is the center of the screen self.width = self.width * (1 - .1 * (r / max(d, 1))) self.height = self.height * (1 - .1 * (r / max(d, 1))) if spacedown: self.updatescalefactors() # not base on center_x, center_y but on the center of the screen, so that the zoom is centered on the mouse cursor # Translate back the pixel at the center of the screen to where the # mouse cursor is. For that we measure the delta in screen coordinates # and transform it to map space coordinates. x_mid, y_mid = self.canvas.GetSize() x_mid, y_mid = self.getXY((0.5*x_mid, y_mid*0.5)) x, y = self.getXY( e.GetPosition()) dx, dy = x_mid - x, y_mid - y self._center_x += dx self._center_y += dy # will translate and rescale the map view so that it fits the window. self.setbounds()
[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 # ----------------------------------------------------------------
[docs] def _get_asset_transform_bounds(self): return self._assets._get_asset_transform_bounds()
@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)
[docs] def _set_asset_transform_cursor(self, handle) -> None: self._assets._set_asset_transform_cursor(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)
[docs] def _asset_transform_snap_steps(self) -> tuple[float, float]: return self._assets._asset_transform_snap_steps()
[docs] def _asset_transform_snap_origin(self) -> tuple[float, float]: return self._assets._asset_transform_snap_origin()
@staticmethod
[docs] def _snap_value(v: float, step: float, origin: float = 0.0) -> float: return AssetManager._snap_value(v, step, origin)
[docs] def _apply_asset_transform_drag(self, x: float, y: float, **kwargs) -> bool: return self._assets._apply_asset_transform_drag(x, y, **kwargs)
[docs] def _update_asset_transform_cursor(self, x: float, y: float) -> None: self._assets._update_asset_transform_cursor(x, y)
[docs] def _plot_grid_transform_overlay(self): show_snap_grid = self.action in ('transform asset bounds', 'move point in cloud', 'add points to cloud', 'insert vertices', 'modify vertices', 'capture vertices') if not show_snap_grid: return # Show snap grid only when snapping is currently active. if wx.GetKeyState(wx.WXK_ALT): step_x, step_y = self._asset_transform_snap_steps() origin_x, origin_y = self._asset_transform_snap_origin() view_xmin, view_xmax = self.xmin, self.xmax view_ymin, view_ymax = self.ymin, self.ymax ix0 = int(np.floor((view_xmin - origin_x) / max(step_x, 1e-12))) ix1 = int(np.ceil((view_xmax - origin_x) / max(step_x, 1e-12))) iy0 = int(np.floor((view_ymin - origin_y) / max(step_y, 1e-12))) iy1 = int(np.ceil((view_ymax - origin_y) / max(step_y, 1e-12))) glColor3ub(210, 210, 210) glLineWidth(1.0) glBegin(GL_LINES) for i in range(ix0, ix1 + 1): gx = origin_x + i * step_x glVertex2f(gx, view_ymin) glVertex2f(gx, view_ymax) for j in range(iy0, iy1 + 1): gy = origin_y + j * step_y glVertex2f(view_xmin, gy) glVertex2f(view_xmax, gy) glEnd() # Asset bounds/handles are only relevant in asset transform mode. if self.action != 'transform asset bounds': return bounds = self._get_asset_transform_bounds() if bounds is None: return xmin, ymin, xmax, ymax = bounds handles = self._compute_asset_handles(bounds) glColor3ub(240, 80, 40) glLineWidth(2.0) glBegin(GL_LINE_LOOP) glVertex2f(xmin, ymin) glVertex2f(xmax, ymin) glVertex2f(xmax, ymax) glVertex2f(xmin, ymax) glEnd() glPointSize(8.0) glBegin(GL_POINTS) for hx, hy in handles.values(): glVertex2f(hx, hy) glEnd()
[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 _update_tooltip(self): """ Update the tooltip with the values of the active arrays and results at position x,y """ x, y, x_pixel, y_pixel = self._mouse_context.x, self._mouse_context.y, self._mouse_context.x_pixel, self._mouse_context.y_pixel self.mytooltip.myparams.clear() curgroup = 'Position' self.mytooltip.myparams[curgroup] = {} curpar = _('Pixel (col,row)') self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '(' + str(x_pixel) + ' ; ' + str(y_pixel) + ')', type = Type_Param.String, comment = '') curpar = _('Coordinate X [m]') self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '{:3f}'.format(x), type = Type_Param.String, comment = '') curpar = _('Coordinate Y [m]') self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '{:3f}'.format(y), type = Type_Param.String, comment = '') if self._tmp_vector_distance is not None: curgroup = _('Temporary vector') self.mytooltip.myparams[curgroup] = {} curpar = _('Length [m]') self._tmp_vector_distance.update_lengths() self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '{:3f}'.format(self._tmp_vector_distance.length2D), type = Type_Param.Float, comment = '') if self._tmp_vector_distance.nbvertices > 4: _polygon = self._tmp_vector_distance.polygon _area = _polygon.area curpar = _('Area [m2]') self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '{:3f}'.format(_area), type = Type_Param.Float, comment = '') curpar = _('Area [ha]') self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '{:3f}'.format(_area / 10000.), type = Type_Param.Float, comment = '') curpar = _('Area [km2]') self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '{:3f}'.format(_area / 1e6), type = Type_Param.Float, comment = '') for locarray in self.myres2D: locarray:Wolfresults_2D curgroup = locarray.idx if locarray.checked: try: vals,labs = locarray.get_values_labels(x,y) i, j, curbloc = locarray.get_blockij_from_xy(x, y) if i != '-' and i != -1: curpar = 'Indices (i;j;bloc) (1-based)' self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '(' + str(i) + ';' + str(j) + ';' + str(curbloc) + ')', type = Type_Param.String, comment = '') for val,curpar in zip(vals,labs): if isinstance(val, str): self.mytooltip.add_param(groupname = curgroup, name = curpar, value = val, type = Type_Param.String, comment = '') elif isinstance(val, int): self.mytooltip.add_param(groupname = curgroup, name = curpar, value = int(val), type = Type_Param.Integer, comment = '') elif np.ma.is_masked(val): self.mytooltip.add_param(groupname = curgroup, name = 'Value', value = "Masked", type = Type_Param.String, comment = '') elif math.isnan(val): self.mytooltip.add_param(groupname = curgroup, name = 'Value', value = "Nan", type = Type_Param.String, comment = '') else: self.mytooltip.add_param(groupname = curgroup, name = curpar, value = float(val), type = Type_Param.Float, comment = '') except: pass for locarray in self.myarrays: if locarray.checked: curgroup = locarray.idx try: val = locarray.get_value(x, y) if val != -99999.: if locarray.wolftype in WOLF_ARRAY_MB: i, j, curbloc = locarray.get_blockij_from_xy(x, y) curpar = 'Indices (i;j;bloc) (1-based)' self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '(' + str(i+1) + ';' + str(j+1) + ';' + str(curbloc) + ')', type = Type_Param.String, comment = '') else: i, j = locarray.get_ij_from_xy(x, y) curpar = 'Indices (i;j) (1-based)' self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '(' + str(i+1) + ';' + str(j+1) + ')', type = Type_Param.String, comment = '') curpar = 'Value' if math.isnan(val): self.mytooltip.add_param(groupname = curgroup, name = 'Value', value = "Nan", type = Type_Param.String, comment = '') elif np.ma.is_masked(val): self.mytooltip.add_param(groupname = curgroup, name = 'Value', value = "Masked", type = Type_Param.String, comment = '') elif isinstance(val, str): self.mytooltip.add_param(groupname = curgroup, name = curpar, value = val, type = Type_Param.String, comment = '') elif isinstance(val, int): self.mytooltip.add_param(groupname = curgroup, name = curpar, value = int(val), type = Type_Param.Integer, comment = '') else: self.mytooltip.add_param(groupname = curgroup, name = curpar, value = float(val), type = Type_Param.Float, comment = '') except: pass if self.linked: for curFrame in self.linkedList: if not curFrame is self: title = curFrame.GetTitle() if curFrame.GetTitle() != 'Wolf - main data manager' else 'Main' for locarray in curFrame.myarrays: curgroup = title + ' -' + locarray.idx if locarray.plotted: try: val = locarray.get_value(x, y) if val != -99999.: if locarray.wolftype in WOLF_ARRAY_MB: i, j, curbloc = locarray.get_blockij_from_xy(x, y) curpar = 'Indices (i;j;bloc) (1-based)' self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '(' + str(i+1) + ';' + str(j+1) + ';' + str(curbloc) + ')', type = Type_Param.String, comment = '') else: i, j = locarray.get_ij_from_xy(x, y) curpar = 'Indices (i;j) (1-based)' self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '(' + str(i+1) + ';' + str(j+1) + ')', type = Type_Param.String, comment = '') curpar = 'Value' if math.isnan(val): self.mytooltip.add_param(groupname = curgroup, name = 'Value', value = "Nan", type = Type_Param.String, comment = '') elif np.ma.is_masked(val): self.mytooltip.add_param(groupname = curgroup, name = 'Value', value = "Masked", type = Type_Param.String, comment = '') elif isinstance(val, str): self.mytooltip.add_param(groupname = curgroup, name = curpar, value = val, type = Type_Param.String, comment = '') elif isinstance(val, int): self.mytooltip.add_param(groupname = curgroup, name = curpar, value = int(val), type = Type_Param.Integer, comment = '') else: self.mytooltip.add_param(groupname = curgroup, name = curpar, value = float(val), type = Type_Param.Float, comment = '') except: logging.warning(_('Error in linked frame Arrays -- Please check !')) for locarray in curFrame.myres2D: locarray:Wolfresults_2D curgroup = title + ' - ' + locarray.idx if locarray.checked: try: vals,labs = locarray.get_values_labels(x,y) i, j, curbloc = locarray.get_blockij_from_xy(x, y) if i != '-' and i != -1: curpar = 'Indices (i;j;bloc) (1-based)' self.mytooltip.add_param(groupname = curgroup, name = curpar, value = '(' + str(i) + ';' + str(j) + ';' + str(curbloc) + ')', type = Type_Param.String, comment = '') for val,curpar in zip(vals,labs): if math.isnan(val): self.mytooltip.add_param(groupname = curgroup, name = 'Value', value = "Nan", type = Type_Param.String, comment = '') elif np.ma.is_masked(val): self.mytooltip.add_param(groupname = curgroup, name = 'Value', value = "Masked", type = Type_Param.String, comment = '') elif isinstance(val, str): self.mytooltip.add_param(groupname = curgroup, name = curpar, value = val, type = Type_Param.String, comment = '') elif isinstance(val, int): self.mytooltip.add_param(groupname = curgroup, name = curpar, value = int(val), type = Type_Param.Integer, comment = '') else: self.mytooltip.add_param(groupname = curgroup, name = curpar, value = float(val), type = Type_Param.Float, comment = '') except: logging.warning(_('Error in linked frame Results2D -- Please check !')) for loc_ps in self.mypartsystems: if loc_ps.checked: curgroup = loc_ps.idx try: self.mytooltip.add_param(groupname = curgroup, name = _('Step [s]'), value = loc_ps.current_step, type = Type_Param.Float, comment = 'Step in seconds') self.mytooltip.add_param(groupname = curgroup, name = _('Step [-]'), value = loc_ps.current_step_idx, type = Type_Param.Integer, comment = 'Step index') except: pass for loc_drowning in self.mydrownings: if loc_drowning.checked: try: if loc_drowning.bottom_cells is not None: curgroup = loc_drowning.idx try: i_bottom, j_bottom = loc_drowning.bottom_cells.get_ij_from_xy(x, y) i_surface, j_surface = loc_drowning.surface_cells.get_ij_from_xy(x, y) except: pass value = loc_drowning.bottom_cells.array[i_bottom,j_bottom] if not np.ma.is_masked(value) and value >0: self.mytooltip.add_param(groupname = curgroup, name = 'Bottom - percentage of the whole sample', value = value, type = Type_Param.Float, comment = '') value = loc_drowning.surface_cells.array[i_surface,j_surface] if not np.ma.is_masked(value) and value >0: self.mytooltip.add_param(groupname = curgroup, name = 'Surface - percentage of the whole sample', value = value, type = Type_Param.Float, comment = '') except: pass self.mytooltip.PopulateOnePage()
[docs] def _update_tooltip_position(self) -> None: """Position the tooltip window after each mouse motion. Called at the end of On_Mouse_Motion once _update_tooltip() has refreshed the tooltip content. Two modes: * **CTRL held** — tooltip follows the cursor (CTRL-follow mode). * **CTRL released** — tooltip is placed at its last stored position (or stacked next to the frame when Shift is held or the position has never been set). """ _ctx = self._mouse_context posframe = self.GetPosition() if _ctx.ctrl: # Control key is pressed, so the tooltip must be near the mouse position if self._oldpos_tooltip is None: # Store the position of the tooltip # Useful to restore it after CTRL is released self._oldpos_tooltip = self.mytooltip.GetPosition() if not self._tooltip_ctrl_active: # Apply z-order/style only once when entering CTRL-follow mode. self.mytooltip.SetWindowStyleFlag(wx.STAY_ON_TOP | wx.DEFAULT_FRAME_STYLE) self._tooltip_ctrl_active = True try: ttsize = self.mytooltip.GetSize() if ttsize[0] == 0 or ttsize[1] == 0: pass if not self._is_heavy_gl_action_active(): self.mytooltip.position((_ctx.x_pixel, _ctx.y_pixel) + posframe + (ttsize[0] / 2. + 30, 30)) if not self.mytooltip.IsShown(): self.mytooltip.Show(True) except Exception as ex: logging.warning(_('Error in tooltip positionning : ') + str(ex)) self.mytooltip.Show(True) else: if self._tooltip_ctrl_active: # Restore default z-order/style once when leaving CTRL-follow mode. self.mytooltip.SetWindowStyleFlag(wx.DEFAULT_FRAME_STYLE) self._tooltip_ctrl_active = False width, height = self.GetSize() if self.IsMaximized(): # Frame is maximized -> tooltip must be on the Screen self.mytooltip.SetWindowStyleFlag(wx.STAY_ON_TOP | wx.DEFAULT_FRAME_STYLE) else: if self._oldpos_tooltip is None: # No old position stored -> tooltip does not move pos_tooltip = self.mytooltip.GetPosition() else: # Restore the position of the tooltip pos_tooltip = self._oldpos_tooltip # Reset the old position, so when CTRL is pressed again, the memory will be updated self._oldpos_tooltip = None if _ctx.shift or (pos_tooltip[0] == 0 and pos_tooltip[1] == 0): # SHIFT is pressed or tooltip is at the top right corner of the Frame # or it is the first time the tooltip is displayed posframe[0] += width posframe[1] -= 50 self.mytooltip.position(posframe) w, h = self.mytooltip.GetSize() self.mytooltip.SetSize((w, height)) else: # Force the position self.mytooltip.SetPosition(pos_tooltip) # self.mytooltip.SetIcon(self.GetIcon()) # update icon self.mytooltip.SetWindowStyleFlag(wx.DEFAULT_FRAME_STYLE) # | wx.STAY_ON_TOP) # on top, with Title bar # Force to show the tooltip --> useful when the tooltip was hidden by the user self.mytooltip.Show(True)
[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 _popup_append_clip_submenu(self, popup: wx.Menu) -> None: """ Append a Clip zone sub-menu with per-item bindings to *popup*. """ from .array_core.clipping import ClipZoneMixin as _CZM clipmenu = wx.Menu() popup.AppendSubMenu(clipmenu, _('Clip zone')) item_view = clipmenu.Append(wx.ID_ANY, _('Clip from current view'), _('Clip rendering to the current viewport extent')) item_hslider = clipmenu.Append(wx.ID_ANY, _('Clip horizontal slider'), _('Add a horizontal slider to reveal/hide from top')) item_vslider = clipmenu.Append(wx.ID_ANY, _('Clip vertical slider'), _('Add a vertical slider to reveal/hide from right')) item_hband = clipmenu.Append(wx.ID_ANY, _('Clip horizontal band'), _('Add top + bottom sliders to define a horizontal band')) item_vband = clipmenu.Append(wx.ID_ANY, _('Clip vertical band'), _('Add left + right sliders to define a vertical band')) clipmenu.AppendSeparator() item_savecfg = clipmenu.Append(wx.ID_ANY, _('Save clip config...'), _('Save clip zones to a JSON file')) item_loadcfg = clipmenu.Append(wx.ID_ANY, _('Load clip config...'), _('Load clip zones from a JSON file')) clipmenu.AppendSeparator() show_bars = isinstance(self.selected_object, _CZM) and self.selected_object._clip_show_bars hide_label = _('Hide clip bars') if show_bars else _('Show clip bars') item_togbars = clipmenu.Append(wx.ID_ANY, hide_label, _('Toggle visibility of slider bars and borders')) item_remove = clipmenu.Append(wx.ID_ANY, _('Remove all clip zones'), _('Remove all clip zones')) clipmenu.Bind(wx.EVT_MENU, lambda e: self._popup_clip_from_view(), item_view) clipmenu.Bind(wx.EVT_MENU, lambda e: self._popup_clip_h_slider(), item_hslider) clipmenu.Bind(wx.EVT_MENU, lambda e: self._popup_clip_v_slider(), item_vslider) clipmenu.Bind(wx.EVT_MENU, lambda e: self._popup_clip_h_band(), item_hband) clipmenu.Bind(wx.EVT_MENU, lambda e: self._popup_clip_v_band(), item_vband) clipmenu.Bind(wx.EVT_MENU, lambda e: self._popup_clip_save_config(), item_savecfg) clipmenu.Bind(wx.EVT_MENU, lambda e: self._popup_clip_load_config(), item_loadcfg) clipmenu.Bind(wx.EVT_MENU, lambda e: self._popup_clip_toggle_bars(), item_togbars) clipmenu.Bind(wx.EVT_MENU, lambda e: self._popup_clip_remove_all(), item_remove)
[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] _ORTHO_NEAR = -99999.0
[docs] _ORTHO_FAR = 99999.0
[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 menu_bridges(self): self._bridge_mgr.menu_build()
[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 menu_weirs(self): self._weir_mgr.menu_build()
[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 Comp_Type(Enum):
[docs] ARRAYS = 1
[docs] ARRAYS_MB = 2
[docs] RES2D = 3
[docs] RES2D_GPU = 4
[docs] class Compare_Arrays_Results(): def __init__(self, parent:WolfMapViewer = None, share_cmap_array:bool = False, share_cmap_diff:bool = False):
[docs] self.parent = parent
[docs] self.paths = []
[docs] self.elements = []
[docs] self.linked_elts = []
[docs] self.diff = []
[docs] self.mapviewers = []
[docs] self.mapviewers_diff = []
[docs] self.times = None
[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)