11import math
2- from typing import List , Optional , Tuple
2+ import pathlib
3+ import typing
4+ from typing import Dict , List , Optional , Tuple
35
46from qgis .core import (
57 QgsCoordinateReferenceSystem ,
1517 QgsRectangle ,
1618 qgsFloatNear ,
1719)
20+ from qgis .PyQt .QtCore import QFile , QIODevice
21+ from qgis .PyQt .QtXml import QDomDocument
1822
23+ from los_tools .constants .plugin import PluginConstants
1924from los_tools .processing .tools .util_functions import bilinear_interpolated_value
2025
2126
2227class ListOfRasters :
28+
2329 def __init__ (self , rasters : List [QgsMapLayer ]):
24- self .rasters : List [QgsRasterLayer ] = []
30+
31+ self ._dict_rasters : Dict [str , QgsRasterLayer ] = {}
2532
2633 if rasters :
2734 first_crs = rasters [0 ].crs ()
@@ -32,10 +39,31 @@ def __init__(self, rasters: List[QgsMapLayer]):
3239 if not first_crs == raster .crs ():
3340 raise ValueError ("All CRS must be equal." )
3441
35- self .rasters . append ( raster )
42+ self ._dict_rasters [ raster . id ()] = raster
3643
3744 self .order_by_pixel_size ()
3845
46+ def __len__ (self ):
47+ return len (self ._dict_rasters )
48+
49+ def __repr__ (self ):
50+ return f"ListOfRasters: [{ ", " .join ([x .name () for x in self .rasters ])} ]"
51+
52+ def raster_to_use (self ) -> str :
53+ return ", " .join ([x .name () for x in self .rasters ])
54+
55+ @property
56+ def rasters (self ) -> List [QgsRasterLayer ]:
57+ return list (self ._dict_rasters .values ())
58+
59+ @property
60+ def raster_ids (self ) -> List [str ]:
61+ return list (self ._dict_rasters .keys ())
62+
63+ def remove_raster (self , raster_id : str ) -> None :
64+ if raster_id in self ._dict_rasters :
65+ self ._dict_rasters .pop (raster_id )
66+
3967 @staticmethod
4068 def validate_bands (rasters : List [QgsMapLayer ]) -> Tuple [bool , str ]:
4169 for raster in rasters :
@@ -139,11 +167,14 @@ def order_by_pixel_size(self) -> None:
139167 tuples = []
140168
141169 for raster in self .rasters :
142- tuples .append ((raster , raster .extent ().width () / raster .width ()))
170+ tuples .append ((raster . id () , raster .extent ().width () / raster .width (), raster ))
143171
144172 sorted_by_cell_size = sorted (tuples , key = lambda tup : tup [1 ])
145173
146- self .rasters = [x [0 ] for x in sorted_by_cell_size ]
174+ self ._dict_rasters = {}
175+
176+ for x in sorted_by_cell_size :
177+ self ._dict_rasters [x [0 ]] = x [2 ]
147178
148179 @property
149180 def rasters_dp (self ) -> List [QgsRasterDataProvider ]:
@@ -201,3 +232,99 @@ def add_z_values(self, points: List[QgsPoint]) -> QgsLineString:
201232 points3d .append (QgsPoint (point .x (), point .y (), z ))
202233
203234 return QgsLineString (points3d )
235+
236+ def save_to_file (self , file_path : str ) -> typing .Tuple [bool , str ]:
237+ """Saves configuration to XML file. Result is a tuple with success status and message."""
238+
239+ path = pathlib .Path (file_path )
240+ if path .suffix .lower () != PluginConstants .rasters_xml_extension :
241+ file_path = path .with_suffix (PluginConstants .rasters_xml_extension ).as_posix ()
242+
243+ doc = QDomDocument ()
244+
245+ root = doc .createElement ("ListOfRasters" )
246+
247+ doc .appendChild (root )
248+
249+ for raster in self .rasters :
250+ try :
251+ relative_path = pathlib .Path (raster .source ()).relative_to (pathlib .Path (file_path ).parent )
252+ path_type = "relative"
253+ except ValueError :
254+ path_type = "absolute"
255+ relative_path = raster .source ()
256+
257+ raster_element = doc .createElement ("raster" )
258+ raster_element .setAttribute ("dataProvider" , raster .dataProvider ().name ())
259+ raster_element .setAttribute ("name" , raster .name ())
260+ raster_element .setAttribute ("path" , relative_path )
261+ raster_element .setAttribute ("pathType" , path_type )
262+ raster_element .setAttribute ("crs" , raster .crs ().authid ())
263+ raster_element .setAttribute ("cellsWidth" , raster .width ())
264+ raster_element .setAttribute ("cellsHeight" , raster .height ())
265+ raster_element .setAttribute ("extentWidth" , raster .extent ().width ())
266+ raster_element .setAttribute ("extentHeight" , raster .extent ().height ())
267+
268+ root .appendChild (raster_element )
269+
270+ file = QFile (file_path )
271+ if file .open (QIODevice .OpenModeFlag .WriteOnly | QIODevice .OpenModeFlag .Text ):
272+ bytes_written = file .write (doc .toByteArray ())
273+ if bytes_written == - 1 :
274+ file .close ()
275+ return False , f"Could not write to file `{ file_path } `."
276+ file .close ()
277+
278+ return True , f"Configuration saved to `{ file_path } `."
279+
280+ def read_from_file (self , file_path : str ) -> typing .Tuple [bool , str ]:
281+
282+ self ._dict_rasters = {}
283+
284+ file = QFile (file_path )
285+ if not file .open (QIODevice .OpenModeFlag .ReadOnly | QIODevice .OpenModeFlag .Text ):
286+ return False , f"Could not open file `{ file_path } `."
287+
288+ doc = QDomDocument ()
289+ if not doc .setContent (file ):
290+ file .close ()
291+ return False , f"Could not read content of file `{ file_path } `."
292+ file .close ()
293+
294+ root = doc .documentElement ()
295+ if root .tagName () != "ListOfRasters" :
296+ return False , f"File `{ file_path } ` is not a valid { PluginConstants .rasters_xml_name } file."
297+
298+ items = root .elementsByTagName ("raster" )
299+
300+ load_messages = []
301+ for i in range (items .length ()):
302+ item = items .item (i ).toElement ()
303+
304+ raster_path = item .attribute ("path" )
305+ if item .attribute ("pathType" ) == "relative" :
306+ raster_path = pathlib .Path (file_path ).parent / raster_path
307+ else :
308+ raster_path = pathlib .Path (raster_path )
309+
310+ if not pathlib .Path (raster_path ).exists ():
311+ continue
312+
313+ raster = QgsRasterLayer (raster_path .as_posix (), item .attribute ("name" ), item .attribute ("dataProvider" ))
314+ if not raster .isValid ():
315+ continue
316+
317+ if not (
318+ raster .crs ().authid () == item .attribute ("crs" )
319+ and raster .width () == int (item .attribute ("cellsWidth" ))
320+ and raster .height () == int (item .attribute ("cellsHeight" ))
321+ and raster .extent ().width () == float (item .attribute ("extentWidth" ))
322+ and raster .extent ().height () == float (item .attribute ("extentHeight" ))
323+ ):
324+ load_messages .append (f"Raster `{ raster .name ()} ` does not fit with definition in the file." )
325+
326+ self ._dict_rasters [raster .id ()] = raster
327+
328+ self .order_by_pixel_size ()
329+
330+ return True , "," .join (load_messages )
0 commit comments