"""
Small utility classes extracted from PyDraw.py.
Includes MplFigViewer, Memory_View helpers, draw_type enum,
Colors_1to9 panel, and DragdropFileTarget.
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.
"""
from __future__ import annotations
import json
import logging
import wx
from pathlib import Path
from enum import Enum
from os import scandir
from .matplotlib_fig import Matplotlib_Figure as MplFig
from typing import TYPE_CHECKING
from .PyTranslate import _
from .wolf_array import WolfArray, WolfArrayMB, header_wolf
from .PyVertexvectors import Zones
from .PyVertex import cloud_vertices
if TYPE_CHECKING:
from .PyDraw import WolfMapViewer
__all__ = [
'MplFigViewer',
'Memory_View',
'Memory_View_encoder',
'Memory_View_decoder',
'Memory_Views',
'Memory_Views_GUI',
'draw_type',
'Colors_1to9',
'DragdropFileTarget',
]
# ---------------------------------------------------------------------------
# The classes below were originally in PyDraw.py (lines 210-877).
# "WolfMapViewer" is referenced only as a forward-reference string annotation,
# so no circular import occurs.
# ---------------------------------------------------------------------------
[docs]
class MplFigViewer(MplFig):
def __init__(self, layout = None, idx:str='',
mapviewer:"WolfMapViewer" = None,
caption:str = '', size:tuple = (800, 600),
style:int = wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER):
super().__init__(layout)
[docs]
self._mapviewer = mapviewer
self.SetTitle(caption)
self.SetSize(size)
dpi = self.fig.get_dpi()
size_x = (size[0]-16)/dpi
size_y = (size[1]-240)/dpi
self.fig.set_size_inches(size_x, size_y)
self.SetWindowStyle(style)
self.Bind(wx.EVT_CLOSE, self.OnClose)
[docs]
def OnClose(self, event):
""" Close the window """
if self.mapviewer is not None:
self.mapviewer.destroy_fig_by_id(self.idx)
else:
self.Destroy()
@property
[docs]
def mapviewer(self):
return self._mapviewer
@property
[docs]
def idx(self):
return self._idx
[docs]
class Memory_View():
""" Memory view """
def __init__(self, screen_width, screen_height, xmin, xmax, ymin, ymax):
""" Constructor """
[docs]
self.screen_width = screen_width
[docs]
self.screen_height = screen_height
@property
[docs]
def width(self):
""" Width of the view """
return self.xmax - self.xmin
@property
[docs]
def height(self):
""" Height of the view """
return self.ymax - self.ymin
def __str__(self):
""" String representation of the view """
return f"Memory view : {self.screen_width}x{self.screen_height} ({self.xmin},{self.ymin})-({self.xmax},{self.ymax})"
[docs]
def serialize(self):
""" Serialize the view """
return {
"screen_width": self.screen_width,
"screen_height": self.screen_height,
"xmin": self.xmin,
"xmax": self.xmax,
"ymin": self.ymin,
"ymax": self.ymax
}
@staticmethod
[docs]
def deserialize(data:dict):
""" Deserialize the view """
return Memory_View(data["screen_width"], data["screen_height"], data["xmin"], data["xmax"], data["ymin"], data["ymax"])
[docs]
class Memory_View_encoder(json.JSONEncoder):
""" Memory view encoder """
[docs]
def default(self, o):
""" Default method """
if isinstance(o, Memory_View):
return o.serialize()
else:
return super().default(o)
[docs]
class Memory_View_decoder(json.JSONDecoder):
""" Memory view decoder """
def __init__(self, *args, **kwargs):
""" Constructor """
super().__init__(object_hook=self.object_hook, *args, **kwargs)
[docs]
def object_hook(self, obj):
""" Decode the object """
if "screen_width" in obj and "screen_height" in obj and "xmin" in obj and "xmax" in obj and "ymin" in obj and "ymax" in obj:
return Memory_View.deserialize(obj)
else:
return obj
[docs]
class Memory_Views():
""" Memory views """
def __init__(self):
[docs]
self.views:dict[str,Memory_View] = {}
def __len__(self):
""" Number of views """
return len(self.views)
[docs]
def add_view(self, name:str, screen_width:int, screen_height:int, xmin:float, xmax:float, ymin:float, ymax:float):
""" Add a new view to the memory views """
self.views[name] = Memory_View(screen_width, screen_height, xmin, xmax, ymin, ymax)
[docs]
def remove_view(self, name:str):
""" Remove a view from the memory views """
if name in self.views:
self.views.pop(name)
[docs]
def reset(self):
""" Reset the memory views """
self.views = {}
def __getitem__(self, name:str) -> Memory_View:
""" Get a view from the memory views """
if name in self.views:
return self.views[name]
else:
return None
[docs]
def zoom_on(self, name:str, mapviewer:"WolfMapViewer"):
""" Zoom on a view """
if name not in self.views:
return
view = self.views[name]
mapviewer.zoom_on(width= view.width, height=view.height, xll=view.xmin, yll=view.ymin, canvas_height=view.screen_height)
[docs]
def save(self, filename:str):
""" Save the memory views """
with open(filename, 'w') as f:
json.dump(self.views, f,
cls=Memory_View_encoder,
indent=4)
[docs]
def load(self, filename:str):
""" Load the memory views """
with open(filename, 'r') as f:
self.views = json.load(f, cls=Memory_View_decoder)
# self.views = {k:Memory_View.deserialize(v) for k,v in tmp_views.items()}
[docs]
class Memory_Views_GUI(wx.Frame):
""" Memory views GUI """
def __init__(self, parent, title, memory_views:Memory_Views, mapviewer:"WolfMapViewer"):
""" Constructor """
super(Memory_Views_GUI, self).__init__(parent, title=title, size=(200, 400), style = wx.DEFAULT_FRAME_STYLE & ~ (wx.RESIZE_BORDER | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX))
[docs]
self.mapviewer = mapviewer
[docs]
self._memory_views = memory_views
panel = wx.Panel(self)
panel.SetBackgroundColour(wx.Colour(255, 255, 255))
sizer = wx.BoxSizer(wx.VERTICAL)
[docs]
self._views = wx.ListBox(panel, choices= list(memory_views.views.keys()), style=wx.LB_SINGLE)
[docs]
self._cmdZoom = wx.Button(panel, wx.ID_ANY, _('Zoom on'))
self._cmdZoom.SetToolTip(_('Zoom on the selected view'))
[docs]
self._cmdAdd = wx.Button(panel, wx.ID_ADD, _('+'))
self._cmdAdd.SetToolTip(_('Add a view based on the current zoom and shape of the canvas'))
[docs]
self._cmdDelete = wx.Button(panel, wx.ID_DELETE, _('-'))
self._cmdDelete.SetToolTip(_('Delete the selected view'))
[docs]
self._cmdReset = wx.Button(panel, wx.ID_RESET, _('Reset'))
self._cmdReset.SetToolTip(_('Reset the views'))
sizer.Add(self._views, 5, wx.EXPAND | wx.ALL, 2)
sizer_but = wx.BoxSizer(wx.HORIZONTAL)
sizer_but.Add(self._cmdAdd, 1, wx.EXPAND)
sizer_but.Add(self._cmdDelete, 1, wx.EXPAND)
sizer.Add(self._cmdZoom, 1, wx.EXPAND, 2)
sizer.Add(sizer_but, 1, wx.EXPAND, 2)
sizer.Add(self._cmdReset, 1, wx.EXPAND , 2)
sizer_manual = wx.BoxSizer(wx.VERTICAL)
sizer_xmin = wx.BoxSizer(wx.HORIZONTAL)
[docs]
self._label_xmin = wx.StaticText(panel, label=_('X min'))
[docs]
self._xmin = wx.TextCtrl(panel, value=str(mapviewer.xmin), style=wx.ALIGN_CENTER_HORIZONTAL)
sizer_xmin.Add(self._label_xmin, 1, wx.ALL, 2)
sizer_xmin.Add(self._xmin, 1, wx.ALL, 2)
sizer_xmax = wx.BoxSizer(wx.HORIZONTAL)
[docs]
self._label_xmax = wx.StaticText(panel, label=_('X max'))
[docs]
self._xmax = wx.TextCtrl(panel, value=str(mapviewer.xmax), style=wx.ALIGN_CENTER_HORIZONTAL)
sizer_xmax.Add(self._label_xmax, 1, wx.ALL, 2)
sizer_xmax.Add(self._xmax, 1, wx.ALL, 2)
sizer_ymin = wx.BoxSizer(wx.HORIZONTAL)
[docs]
self._label_ymin = wx.StaticText(panel, label=_('Y min'))
[docs]
self._ymin = wx.TextCtrl(panel, value=str(mapviewer.ymin), style=wx.ALIGN_CENTER_HORIZONTAL)
sizer_ymin.Add(self._label_ymin, 1, wx.ALL, 2)
sizer_ymin.Add(self._ymin, 1, wx.ALL, 2)
sizer_ymax = wx.BoxSizer(wx.HORIZONTAL)
[docs]
self._label_ymax = wx.StaticText(panel, label=_('Y max'))
[docs]
self._ymax = wx.TextCtrl(panel, value=str(mapviewer.ymax), style=wx.ALIGN_CENTER_HORIZONTAL)
sizer_ymax.Add(self._label_ymax, 1, wx.ALL, 2)
sizer_ymax.Add(self._ymax, 1, wx.ALL, 2)
sizer_canvas_height = wx.BoxSizer(wx.HORIZONTAL)
[docs]
self._label_canvas_height = wx.StaticText(panel, label=_('Canvas height'))
[docs]
self._canvas_height = wx.TextCtrl(panel, value=str(mapviewer.canvasheight), style=wx.ALIGN_CENTER_HORIZONTAL)
self._label_canvas_height.SetToolTip(_('Height of the canvas in pixels'))
self._canvas_height.SetToolTip(_('Height of the canvas in pixels'))
sizer_canvas_height.Add(self._label_canvas_height, 1, wx.ALL, 2)
sizer_canvas_height.Add(self._canvas_height, 1, wx.ALL, 2)
sizer_manual.Add(sizer_xmin, 1, wx.ALL, 2)
sizer_manual.Add(sizer_xmax, 1, wx.ALL, 2)
sizer_manual.Add(sizer_ymin, 1, wx.ALL, 2)
sizer_manual.Add(sizer_ymax, 1, wx.ALL, 2)
sizer_manual.Add(sizer_canvas_height, 1, wx.ALL, 2)
sizer_but2 = wx.BoxSizer(wx.HORIZONTAL)
[docs]
self._cmdGet = wx.Button(panel, wx.ID_ANY, _('Get'))
self._cmdGet.SetToolTip(_('Get the current bounds of the canvas and fill the fields'))
[docs]
self._cmdApply = wx.Button(panel, wx.ID_APPLY, _('Apply'))
self._cmdApply.SetToolTip(_('Apply the values to the selected view'))
self._cmdApply.Bind(wx.EVT_BUTTON, self.OnApply)
self._cmdGet.Bind(wx.EVT_BUTTON, self.OnGet)
sizer_but2.Add(self._cmdGet, 1, wx.ALL, 2)
sizer_but2.Add(self._cmdApply, 1, wx.ALL, 2)
sizer_manual.Add(sizer_but2, 1, wx.EXPAND, 2)
sizer.Add(sizer_manual, 1, wx.ALL, 2)
sizer_save_load = wx.BoxSizer(wx.HORIZONTAL)
[docs]
self._cmdSave = wx.Button(panel, wx.ID_SAVE, _('Save'))
self._cmdSave.SetToolTip(_('Save the memory views to a json file'))
[docs]
self._cmdLoad = wx.Button(panel, wx.ID_OPEN, _('Load'))
self._cmdLoad.SetToolTip(_('Load the memory views from a json file'))
sizer_save_load.Add(self._cmdSave, 1, wx.ALL, 2)
sizer_save_load.Add(self._cmdLoad, 1, wx.ALL, 2)
sizer.Add(sizer_save_load, 1, wx.EXPAND, 2)
self._views.Bind(wx.EVT_LISTBOX, self.OnSelectView)
self.Bind(wx.EVT_BUTTON, self.OnAdd, self._cmdAdd)
self.Bind(wx.EVT_BUTTON, self.OnDelete, self._cmdDelete)
self.Bind(wx.EVT_BUTTON, self.OnReset, self._cmdReset)
self.Bind(wx.EVT_CLOSE, self.OnClose)
self.Bind(wx.EVT_BUTTON, self.OnSave, self._cmdSave)
self.Bind(wx.EVT_BUTTON, self.OnLoad, self._cmdLoad)
self.Bind(wx.EVT_BUTTON, self.OnZoom, self._cmdZoom)
panel.SetSizer(sizer)
self.CenterOnScreen()
icon = wx.Icon()
icon_path = Path(__file__).parent / "apps/wolf.ico"
icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY))
self.SetIcon(icon)
self.Show()
[docs]
def OnSave(self, event):
""" Save the memory views """
with wx.FileDialog(self, _('Save the memory views'), wildcard="JSON files (*.json)|*.json",
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return
self._memory_views.save(fileDialog.GetPath())
[docs]
def OnLoad(self, event):
""" Load the memory views """
with wx.FileDialog(self, _('Load the memory views'), wildcard="JSON files (*.json)|*.json",
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog:
if fileDialog.ShowModal() == wx.ID_CANCEL:
return
self._memory_views.load(fileDialog.GetPath())
if self._memory_views is None:
logging.error(_('Error while loading the memory views'))
return
if self._memory_views.views is None:
logging.error(_('Error while loading the memory views'))
return
self._views.Set(list(self._memory_views.views.keys()))
[docs]
def OnClose(self, event):
""" Close the memory views GUI """
self.mapviewer._memory_views_gui = None
self.Destroy()
[docs]
def OnSelectView(self, event):
""" Select a view """
view = self._views.GetStringSelection()
locview = self._memory_views[view]
if locview is None:
return
self._xmax.SetValue(str(self._memory_views[view].xmax))
self._xmin.SetValue(str(self._memory_views[view].xmin))
self._ymax.SetValue(str(self._memory_views[view].ymax))
self._ymin.SetValue(str(self._memory_views[view].ymin))
self._canvas_height.SetValue(str(self._memory_views[view].screen_height))
[docs]
def OnZoom(self, event):
""" Zoom on the current view """
view = self._views.GetStringSelection()
self._memory_views.zoom_on(view, self.mapviewer)
[docs]
def OnAdd(self, event):
""" Add a view """
name = "View " + str(len(self._memory_views.views) + 1)
with wx.TextEntryDialog(self, _('Enter the name of the view'), _('Add a view'), name) as dlg:
if dlg.ShowModal() == wx.ID_OK:
name = dlg.GetValue()
self._memory_views.add_view(name, self.mapviewer.canvaswidth, self.mapviewer.canvasheight, self.mapviewer.xmin, self.mapviewer.xmax, self.mapviewer.ymin, self.mapviewer.ymax)
self._views.Set(list(self._memory_views.views.keys()))
[docs]
def OnDelete(self, event):
""" Delete a view """
view = self._views.GetStringSelection()
self._memory_views.remove_view(view)
self._views.Set(list(self._memory_views.views.keys()))
[docs]
def OnReset(self, event):
""" Reset the views """
self._memory_views.reset()
[docs]
def OnApply(self, event):
""" Apply the changes """
view = self._views.GetStringSelection()
try:
xmin = float(self._xmin.GetValue())
xmax = float(self._xmax.GetValue())
ymin = float(self._ymin.GetValue())
ymax = float(self._ymax.GetValue())
canvas_height = int(self._canvas_height.GetValue())
self._memory_views.add_view(view, self.mapviewer.canvaswidth, canvas_height, xmin, xmax, ymin, ymax)
except:
logging.error(_('Error while applying the changes'))
[docs]
def OnGet(self, event):
""" Get the values """
self._xmin.SetValue(str(self.mapviewer.xmin))
self._xmax.SetValue(str(self.mapviewer.xmax))
self._ymin.SetValue(str(self.mapviewer.ymin))
self._ymax.SetValue(str(self.mapviewer.ymax))
self._canvas_height.SetValue(str(self.mapviewer.canvasheight))
[docs]
class draw_type(Enum):
# FIXME: change this to be more robust -> Done !
# Be careful with the enum name, it must be the same than the one used to create the tree list elements, but in lower case
# see : self.treelist.AppendItem in __init__
[docs]
TRIANGULATION = 'triangulations'
[docs]
PARTICLE_SYSTEM = 'particle systems'
[docs]
CROSS_SECTIONS = 'cross_sections'
[docs]
WMSBACK = 'wms-background'
[docs]
WMSFORE = 'wms-foreground'
[docs]
IMAGESTILES = 'imagestiles'
[docs]
PICTURECOLLECTION = 'picture_collection'
[docs]
class Colors_1to9(wx.Frame):
def __init__(self, parent):
[docs]
self.colors1to9 = [(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)]
if self.file.exists():
self.OnLoad(None)
@property
[docs]
def directory(self):
tmp = Path(__file__).parent / 'data'
if not tmp.exists():
tmp.mkdir()
return tmp
@property
[docs]
def file(self):
return self.directory / 'colors1to9.json'
def __getitem__(self, i):
return self.colors1to9[i]
[docs]
def change_colors(self, e):
super(Colors_1to9, self).__init__(self._parent, title=_('Colors 1 to 9'), size=(200, 400), style = wx.DEFAULT_FRAME_STYLE & ~ (wx.RESIZE_BORDER | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.CLOSE_BOX))
panel = wx.Panel(self)
panel.SetBackgroundColour(wx.Colour(255, 255, 255))
sizer = wx.BoxSizer(wx.VERTICAL)
self.pickers={}
for i in range(9):
horsizer = wx.BoxSizer(wx.HORIZONTAL)
horsizer.Add(wx.StaticText(panel, label=_('Color ') + str(i + 1)), 0, wx.ALL, 2)
color = self.colors1to9[i]
color = wx.Colour(color[0], color[1], color[2], color[3])
self.pickers[i] = wx.ColourPickerCtrl(panel, colour=color)
horsizer.Add(self.pickers[i], 0, wx.ALL, 2)
sizer.Add(horsizer, 0, wx.ALL, 2)
cmdOK = wx.Button(panel, wx.ID_OK, _('OK'))
cmdCancel = wx.Button(panel, wx.ID_CANCEL, _('Cancel'))
cdmSetDefault = wx.Button(panel, wx.ID_APPLY, _('Default'))
cmdSave = wx.Button(panel, wx.ID_SAVE, _('Save'))
horsizer = wx.BoxSizer(wx.HORIZONTAL)
horsizer2 = wx.BoxSizer(wx.HORIZONTAL)
horsizer.Add(cmdOK, 0, wx.ALL, 2)
horsizer.Add(cmdCancel, 0, wx.ALL, 2)
horsizer2.Add(cdmSetDefault, 0, wx.ALL, 2)
horsizer2.Add(cmdSave, 0, wx.ALL, 2)
sizer.Add(horsizer, 0, wx.ALL, 2)
sizer.Add(horsizer2, 0, wx.ALL, 2)
panel.SetSizer(sizer)
self.Bind(wx.EVT_BUTTON, self.OnOK, cmdOK)
self.Bind(wx.EVT_BUTTON, self.OnCancel, cmdCancel)
self.Bind(wx.EVT_BUTTON, self.OnSetDefault, cdmSetDefault)
self.Bind(wx.EVT_BUTTON, self.OnSave, cmdSave)
icon = wx.Icon()
icon_path = Path(__file__).parent / "apps/wolf.ico"
icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY))
self.SetIcon(icon)
self.CenterOnScreen()
self.Show()
[docs]
def Apply(self):
for i in range(9):
color = self.pickers[i].GetColour()
self.colors1to9[i] = (color.Red(), color.Green(), color.Blue(), color.Alpha())
[docs]
def OnOK(self, event):
self.Apply()
self.Close()
[docs]
def OnCancel(self, event):
self.Close()
[docs]
def OnSetDefault(self, event):
self.colors1to9 = [(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)]
for i in range(9):
color = self.colors1to9[i]
color = wx.Colour(color[0], color[1], color[2], color[3])
self.pickers[i].SetColour(color)
[docs]
def OnSave(self, event):
self.Apply()
with open(self.file, 'w') as f:
json.dump(self.colors1to9, f)
[docs]
def OnLoad(self, event):
with open(self.file, 'r') as f:
self.colors1to9 = json.load(f)
[docs]
class DragdropFileTarget(wx.FileDropTarget):
def __init__(self, window:"WolfMapViewer"):
wx.FileDropTarget.__init__(self)
[docs]
def OnDropFiles(self, x, y, filenames):
def test_if_array(filename):
ext = Path(filename).suffix
if ext.lower() in ['.bin', '.npy', '.hbin', '.qxin','.qybin', '.top',
'.kbin', '.epsbin', '.tif', '.tiff', '.frot', '.topini_fine']:
return True
else:
return False
def test_if_arrayMB(filename):
ext = Path(filename).suffix
if ext.lower() in ['.hbinb', '.qxbinb','.qybinb', '.kbinb',
'.epsbinb', '.topini', '.frotini']:
return True
else:
return False
def test_if_vector(filename):
ext = Path(filename).suffix
if ext.lower() in ['.vec', '.vecz', '.shp', '.dxf']:
return True
else:
return False
def test_if_cloud(filename):
ext = Path(filename).suffix
if ext.lower() in ['.xyz']:
return True
else:
return False
pgbar = wx.ProgressDialog(_('Loading files'), _('Loading files'), maximum=len(filenames), parent=self.window, style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE)
for name in filenames:
if Path(name).is_dir():
for file in scandir(name):
if file.is_file():
self.OnDropFiles(x, y, [file.path])
continue
if test_if_array(name):
ids = self.window.get_list_keys(draw_type.ARRAYS, checked_state=None)
id = Path(name).stem
while id in ids:
id = id + '_1'
try:
h = header_wolf.read_header(name)
if h.nb_blocks>0:
newobj = WolfArrayMB(fname=name, mapviewer= self.window)
else:
newobj = WolfArray(fname=name, mapviewer= self.window)
self.window.add_object('array', newobj = newobj, id = id)
except:
logging.error(_('Error while loading array : ') + name)
elif test_if_arrayMB(name):
ids = self.window.get_list_keys(draw_type.ARRAYS, checked_state=None)
id = Path(name).stem
while id in ids:
id = id + '_1'
try:
newobj = WolfArrayMB(fname=name, mapviewer= self.window)
self.window.add_object('array', newobj = newobj, id = id)
except:
logging.error(_('Error while loading array : ') + name)
elif test_if_vector(name):
ids = self.window.get_list_keys(draw_type.VECTORS, checked_state=None)
id = Path(name).stem
while id in ids:
id = id + '_1'
try:
newobj = Zones(filename=name, parent=self.window, mapviewer=self.window)
self.window.add_object('vector', newobj = newobj, id = id)
except:
logging.error(_('Error while loading vector : ') + name)
elif test_if_cloud(name):
ids = self.window.get_list_keys(draw_type.CLOUD, checked_state=None)
id = Path(name).stem
while id in ids:
id = id + '_1'
try:
newobj = cloud_vertices(fname=name, mapviewer=self.window)
self.window.add_object('cloud', newobj = newobj, id = id)
except:
logging.error(_('Error while loading cloud : ') + name)
pgbar.Update(pgbar.GetValue() + 1)
pgbar.Destroy()
return True