From c0bb5f27c48ab639017ab30ee65c00c2972f56a2 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 13:09:35 +1000 Subject: [PATCH 001/116] template for basal contacts processing tool --- map2loop/processing/algorithms/__init__.py | 1 + .../algorithms/extract_basal_contacts.py | 76 +++++++++++++++++++ map2loop/processing/provider.py | 3 + 3 files changed, 80 insertions(+) create mode 100644 map2loop/processing/algorithms/__init__.py create mode 100644 map2loop/processing/algorithms/extract_basal_contacts.py diff --git a/map2loop/processing/algorithms/__init__.py b/map2loop/processing/algorithms/__init__.py new file mode 100644 index 0000000..618a57d --- /dev/null +++ b/map2loop/processing/algorithms/__init__.py @@ -0,0 +1 @@ +from .extract_basal_contacts import BasalContactsAlgorithm diff --git a/map2loop/processing/algorithms/extract_basal_contacts.py b/map2loop/processing/algorithms/extract_basal_contacts.py new file mode 100644 index 0000000..11d406c --- /dev/null +++ b/map2loop/processing/algorithms/extract_basal_contacts.py @@ -0,0 +1,76 @@ +""" +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" + +from typing import Any, Optional + +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, +) + + +class BasalContactsAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm to create basal contacts.""" + + INPUT = "INPUT" + OUTPUT = "OUTPUT" + + def name(self) -> str: + """Return the algorithm name.""" + return "loop: basal_contacts" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Basal Contacts" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT, + "Geology Polygons", + [QgsProcessing.TypeVectorPolygon], + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Basal Contacts", + ) + ) + pass + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + pass + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() diff --git a/map2loop/processing/provider.py b/map2loop/processing/provider.py index 6e0add8..ba879bb 100644 --- a/map2loop/processing/provider.py +++ b/map2loop/processing/provider.py @@ -16,6 +16,8 @@ __version__, ) +from .algorithms import BasalContactsAlgorithm + # ############################################################################ # ########## Classes ############### # ################################## @@ -26,6 +28,7 @@ class Map2LoopProvider(QgsProcessingProvider): def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" + self.addAlgorithm(BasalContactsAlgorithm()) pass def id(self) -> str: From f9878b0396c19f57160fc4c27b4086221e83c3b6 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Tue, 17 Jun 2025 13:11:57 +1000 Subject: [PATCH 002/116] Update linter.yml --- .github/workflows/linter.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 57c2289..f87c4e5 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -7,7 +7,7 @@ on: pull_request: branches: - - master + - main paths: - '**.py' workflow_dispatch: @@ -55,5 +55,5 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} title: "style: auto format fixes" body: "This PR applies style fixes by black and ruff." - base: master + base: main branch: lint/style-fixes-${{ github.run_id }} From 773ef4844f12f3183da8caed672eb7af34dfbd35 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 23 Jun 2025 13:55:03 +0930 Subject: [PATCH 003/116] feature: Implement StratigraphySorterAlgorithm --- map2loop/processing/algorithms/sorter.py | 201 +++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 map2loop/processing/algorithms/sorter.py diff --git a/map2loop/processing/algorithms/sorter.py b/map2loop/processing/algorithms/sorter.py new file mode 100644 index 0000000..999fcad --- /dev/null +++ b/map2loop/processing/algorithms/sorter.py @@ -0,0 +1,201 @@ +from typing import Any, Optional + +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsFields, QgsField, QgsFeature, QgsGeometry, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterEnum, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, + QgsVectorLayer, +) + +# ──────────────────────────────────────────────── +# map2loop sorters +# ──────────────────────────────────────────────── +from map2loop.map2loop.sorter import ( + SorterAlpha, + SorterAgeBased, + SorterMaximiseContacts, + SorterObservationProjections, + SorterUseNetworkX, + SorterUseHint, # kept for backwards compatibility +) + +# a lookup so we don’t need a giant if/else block +SORTER_LIST = { + "Age‐based": SorterAgeBased, + "NetworkX topological": SorterUseNetworkX, + "Hint (deprecated)": SorterUseHint, + "Adjacency α": SorterAlpha, + "Maximise contacts": SorterMaximiseContacts, + "Observation projections": SorterObservationProjections, +} + +class StratigraphySorterAlgorithm(QgsProcessingAlgorithm): + """ + Creates a one-column ‘stratigraphic column’ table ordered + by the selected map2loop sorter. + """ + + INPUT = "INPUT" + ALGO = "SORT_ALGO" + OUTPUT = "OUTPUT" + + # ---------------------------------------------------------- + # Metadata + # ---------------------------------------------------------- + def name(self) -> str: + return "loop_sorter" + + def displayName(self) -> str: + return "loop: Stratigraphic sorter" + + def group(self) -> str: + return "Loop3d" + + def groupId(self) -> str: + return "loop3d" + + # ---------------------------------------------------------- + # Parameters + # ---------------------------------------------------------- + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT, + self.tr("Geology polygons"), + [QgsProcessing.TypeVectorPolygon], + ) + ) + + # enum so the user can pick the strategy from a dropdown + self.addParameter( + QgsProcessingParameterEnum( + self.ALGO, + self.tr("Sorting strategy"), + options=list(SORTER_LIST.keys()), + defaultValue=0, # Age-based is safest default + ) + ) #:contentReference[oaicite:0]{index=0} + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + self.tr("Stratigraphic column"), + ) + ) + + # ---------------------------------------------------------- + # Core + # ---------------------------------------------------------- + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + # 1 ► fetch user selections + in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT, context) + algo_index: int = self.parameterAsEnum(parameters, self.ALGO, context) + sorter_cls = list(SORTER_LIST.values())[algo_index] + + feedback.pushInfo(f"Using sorter: {sorter_cls.__name__}") + + # 2 ► convert QGIS layers / tables to pandas + # -------------------------------------------------- + # You must supply these three DataFrames: + # units_df — required (layerId, name, minAge, maxAge, group) + # relationships_df — required (Index1 / Unitname1, Index2 / Unitname2 …) + # contacts_df — required for all but Age‐based + # + # Typical workflow: + # • iterate over in_layer.getFeatures() + # • build dicts/lists + # • pd.DataFrame(…) + # + # NB: map2loop does *not* need geometries – only attribute values. + # -------------------------------------------------- + units_df, relationships_df, contacts_df, map_data = build_input_frames(in_layer, feedback) + + # 3 ► run the sorter + sorter = sorter_cls() # instantiation is always zero-argument + order = sorter.sort( + units_df, + relationships_df, + contacts_df, + map_data, + ) + + # 4 ► write an in-memory table with the result + sink_fields = QgsFields() + sink_fields.append(QgsField("strat_pos", int)) + sink_fields.append(QgsField("unit_name", str)) + + (sink, dest_id) = self.parameterAsSink( + parameters, + self.OUTPUT, + context, + sink_fields, + QgsWkbTypes.NoGeometry, + in_layer.sourceCrs(), + ) + + for pos, name in enumerate(order, start=1): + f = QgsFeature(sink_fields) + f.setAttributes([pos, name]) + sink.addFeature(f, QgsFeatureSink.FastInsert) + + return {self.OUTPUT: dest_id} + + # ---------------------------------------------------------- + def createInstance(self) -> QgsProcessingAlgorithm: + return StratigraphySorterAlgorithm() + + +# ------------------------------------------------------------------------- +# Helper stub – you must replace with *your* conversion logic +# ------------------------------------------------------------------------- +def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: + """ + Placeholder that turns the geology layer (and any other project + layers) into the four objects required by the sorter. + + Returns + ------- + (units_df, relationships_df, contacts_df, map_data) + """ + import pandas as pd + from map2loop.map2loop.mapdata import MapData # adjust import path if needed + + # Example: convert the geology layer to a very small units_df + units_records = [] + for f in layer.getFeatures(): + units_records.append( + dict( + layerId=f.id(), + name=f["UNITNAME"], # attribute names → your schema + minAge=f.attribute("MIN_AGE"), + maxAge=f.attribute("MAX_AGE"), + group=f["GROUP"], + ) + ) + units_df = pd.DataFrame.from_records(units_records) + + # relationships_df and contacts_df are domain-specific ─ fill them here + relationships_df = pd.DataFrame(columns=["Index1", "UNITNAME_1", "Index2", "UNITNAME_2"]) + contacts_df = pd.DataFrame(columns=["UNITNAME_1", "UNITNAME_2", "length"]) + + # map_data can be mocked if you only use Age-based sorter + map_data = MapData() # or MapData.from_project(…) / MapData.from_files(…) + + feedback.pushInfo(f"Units → {len(units_df)} records") + + return units_df, relationships_df, contacts_df, map_data From e7bb9f660416ef3a6ea9fa08f5a2df9a1fb91267 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 12:35:07 +0930 Subject: [PATCH 004/116] feature: functions to convert layers to GeoDataFrame and DataFrame --- map2loop/main/vectorLayerWrapper.py | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 map2loop/main/vectorLayerWrapper.py diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py new file mode 100644 index 0000000..7e146db --- /dev/null +++ b/map2loop/main/vectorLayerWrapper.py @@ -0,0 +1,78 @@ +import pandas as pd +import geopandas as gpd +from qgis.core import QgsRaster, QgsWkbTypes + + +def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: + if layer is None: + return None + features = layer.getFeatures() + fields = layer.fields() + data = {'geometry': []} + for f in fields: + data[f.name()] = [] + for feature in features: + geom = feature.geometry() + if geom.isEmpty(): + continue + data['geometry'].append(geom) + for f in fields: + data[f.name()].append(feature[f.name()]) + return gpd.GeoDataFrame(data, crs=layer.crs().authid()) + + +def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: + """Convert a vector layer to a pandas DataFrame + samples the geometry using either points or the vertices of the lines + + :param layer: _description_ + :type layer: _type_ + :param dtm: Digital Terrain Model to evaluate Z values + :type dtm: _type_ or None + :return: the dataframe object + :rtype: pd.DataFrame + """ + if layer is None: + return None + fields = layer.fields() + data = {} + data['X'] = [] + data['Y'] = [] + data['Z'] = [] + + for field in fields: + data[field.name()] = [] + for feature in layer.getFeatures(): + geom = feature.geometry() + points = [] + if geom.isMultipart(): + if geom.type() == QgsWkbTypes.PointGeometry: + points = geom.asMultiPoint() + elif geom.type() == QgsWkbTypes.LineGeometry: + for line in geom.asMultiPolyline(): + points.extend(line) + # points = geom.asMultiPolyline()[0] + else: + if geom.type() == QgsWkbTypes.PointGeometry: + points = [geom.asPoint()] + elif geom.type() == QgsWkbTypes.LineGeometry: + points = geom.asPolyline() + + for p in points: + data['X'].append(p.x()) + data['Y'].append(p.y()) + if dtm is not None: + # Replace with your coordinates + + # Extract the value at the point + z_value = dtm.dataProvider().identify(p, QgsRaster.IdentifyFormatValue) + if z_value.isValid(): + z_value = z_value.results()[1] + else: + z_value = -9999 + data['Z'].append(z_value) + if dtm is None: + data['Z'].append(0) + for field in fields: + data[field.name()].append(feature[field.name()]) + return pd.DataFrame(data) From 95ba89b4544eea809501c1844060b1d78a47b32d Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:21:09 +0930 Subject: [PATCH 005/116] feature: add geodataframe to qgsLayer conversion --- map2loop/main/vectorLayerWrapper.py | 122 +++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 7e146db..81af364 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -1,6 +1,20 @@ +from qgis.core import ( + QgsVectorLayer, + QgsFields, + QgsField, + QgsFeature, + QgsGeometry, + QgsWkbTypes, + QgsCoordinateReferenceSystem, + QgsProject, + QgsRaster + ) +from qgis.PyQt.QtCore import QVariant, QDateTime + +from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon import pandas as pd import geopandas as gpd -from qgis.core import QgsRaster, QgsWkbTypes + def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: @@ -76,3 +90,109 @@ def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: for field in fields: data[field.name()].append(feature[field.name()]) return pd.DataFrame(data) + +def gdf_to_qgis_layer(gdf, layer_name="from_gdf"): + """ + Convert a GeoPandas GeoDataFrame to a QGIS memory layer (QgsVectorLayer). + Keeps attributes and CRS. Works for Point/LineString/Polygon and their Multi*. + """ + + if gdf is None or gdf.empty: + raise ValueError("GeoDataFrame is empty") + + # --- infer geometry type from first non-empty geometry + def infer_wkb(geoms): + for g in geoms: + if g is None: + continue + if hasattr(g, "is_empty") and g.is_empty: + continue + if isinstance(g, MultiPoint): return QgsWkbTypes.MultiPoint + if isinstance(g, Point): return QgsWkbTypes.Point + if isinstance(g, MultiLineString): return QgsWkbTypes.MultiLineString + if isinstance(g, LineString): return QgsWkbTypes.LineString + if isinstance(g, MultiPolygon): return QgsWkbTypes.MultiPolygon + if isinstance(g, Polygon): return QgsWkbTypes.Polygon + raise ValueError("Could not infer geometry type (all geometries empty?)") + + wkb_type = infer_wkb(gdf.geometry) + + # --- build CRS + crs_qgis = QgsCoordinateReferenceSystem() + if gdf.crs is not None: + try: + crs_qgis = QgsCoordinateReferenceSystem.fromWkt(gdf.crs.to_wkt()) + except Exception: + epsg = gdf.crs.to_epsg() + if epsg: + crs_qgis = QgsCoordinateReferenceSystem.fromEpsgId(int(epsg)) + + geom_str = QgsWkbTypes.displayString(wkb_type) # e.g. "LineString" + uri = f"{geom_str}?crs={crs_qgis.authid()}" if crs_qgis.isValid() else geom_str + layer = QgsVectorLayer(uri, layer_name, "memory") + prov = layer.dataProvider() + + # --- fields: map pandas dtypes → QGIS + import numpy as np + fields = QgsFields() + for col in gdf.columns: + if col == gdf.geometry.name: + continue + dtype = gdf[col].dtype + if pd.api.types.is_integer_dtype(dtype): + qtype = QVariant.Int + elif pd.api.types.is_float_dtype(dtype): + qtype = QVariant.Double + elif pd.api.types.is_bool_dtype(dtype): + qtype = QVariant.Bool + elif pd.api.types.is_datetime64_any_dtype(dtype): + qtype = QVariant.DateTime + else: + qtype = QVariant.String + fields.append(QgsField(str(col), qtype)) + prov.addAttributes(list(fields)) + layer.updateFields() + + # --- features + feats = [] + non_geom_cols = [c for c in gdf.columns if c != gdf.geometry.name] + + for _, row in gdf.iterrows(): + geom = row[gdf.geometry.name] + if geom is None or (hasattr(geom, "is_empty") and geom.is_empty): + continue + + f = QgsFeature(fields) + + # attributes in declared order with type cleanup + attrs = [] + for col in non_geom_cols: + val = row[col] + # numpy scalar → python scalar + if isinstance(val, (np.generic,)): + try: + val = val.item() + except Exception: + pass + # pandas Timestamp → QDateTime (if column is datetime) + if pd.api.types.is_datetime64_any_dtype(gdf[col].dtype): + if pd.isna(val): + val = None + else: + val = QDateTime(val.to_pydatetime()) + attrs.append(val) + f.setAttributes(attrs) + + # geometry (shapely → QGIS) + try: + f.setGeometry(QgsGeometry.fromWkb(geom.wkb)) + except Exception: + f.setGeometry(QgsGeometry.fromWkt(geom.wkt)) + + feats.append(f) + + if feats: + prov.addFeatures(feats) + layer.updateExtents() + + return layer # optionally: QgsProject.instance().addMapLayer(layer) From f744d83bb484218d11c3583a8154ef5c19e3f1ac Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:23:52 +0930 Subject: [PATCH 006/116] refactor: update GeoDataFrameToQgsLayer --- map2loop/main/vectorLayerWrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 81af364..b106fb3 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -91,7 +91,7 @@ def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: data[field.name()].append(feature[field.name()]) return pd.DataFrame(data) -def gdf_to_qgis_layer(gdf, layer_name="from_gdf"): +def GeoDataFrameToQgsLayer(gdf, layer_name="from_gdf"): """ Convert a GeoPandas GeoDataFrame to a QGIS memory layer (QgsVectorLayer). Keeps attributes and CRS. Works for Point/LineString/Polygon and their Multi*. From ea67c2e2819416cb81a642f9a6312ae8a0afeee7 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:37:22 +0930 Subject: [PATCH 007/116] fix: GeoDataFrameToQgsLayer to FeatureSink --- map2loop/main/vectorLayerWrapper.py | 221 +++++++++++++++++++--------- 1 file changed, 150 insertions(+), 71 deletions(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index b106fb3..90d0625 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -91,91 +91,170 @@ def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: data[field.name()].append(feature[field.name()]) return pd.DataFrame(data) -def GeoDataFrameToQgsLayer(gdf, layer_name="from_gdf"): +def GeoDataFrameToQgsLayer(qgs_algorithm, geodataframe, parameters, context, output_key, feedback=None): """ - Convert a GeoPandas GeoDataFrame to a QGIS memory layer (QgsVectorLayer). - Keeps attributes and CRS. Works for Point/LineString/Polygon and their Multi*. + Write a GeoPandas GeoDataFrame directly to a QGIS Processing FeatureSink. + + Parameters + ---------- + alg : QgsProcessingAlgorithm (self) + gdf : geopandas.GeoDataFrame + parameters : dict (from processAlgorithm) + context : QgsProcessingContext + output_key : str (e.g. self.OUTPUT) + feedback : QgsProcessingFeedback | None + + Returns + ------- + str : dest_id to return from processAlgorithm, e.g. { output_key: dest_id } """ + import pandas as pd + import numpy as np + from shapely.geometry import ( + Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon + ) + + from qgis.core import ( + QgsFields, QgsField, QgsFeature, QgsGeometry, + QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink + ) + from qgis.PyQt.QtCore import QVariant, QDateTime + + if feedback is None: + class _Dummy: + def pushInfo(self, *a, **k): pass + def reportError(self, *a, **k): pass + def setProgress(self, *a, **k): pass + def isCanceled(self): return False + feedback = _Dummy() + + if geodataframe is None: + raise ValueError("GeoDataFrame is None") + if geodataframe.empty: + feedback.pushInfo("Input GeoDataFrame is empty; creating empty output layer.") + + # --- infer WKB type (family, Multi, Z) + def _infer_wkb(series): + base = None + any_multi = False + has_z = False + for geom in series: + if geom is None: continue + if getattr(geom, "is_empty", False): continue + # multi? + if isinstance(geom, (MultiPoint, MultiLineString, MultiPolygon)): + any_multi = True + g0 = next(iter(getattr(geom, "geoms", [])), None) + gt = getattr(g0, "geom_type", None) or None + else: + gt = getattr(geom, "geom_type", None) + + # base family + if gt in ("Point", "LineString", "Polygon"): + base = gt + # z? + try: + has_z = has_z or bool(getattr(geom, "has_z", False)) + except Exception: + pass + if base: + break + + if base is None: + # default safely to LineString if everything is empty; adjust if you prefer Point/Polygon + base = "LineString" + + fam = { + "Point": QgsWkbTypes.Point, + "LineString": QgsWkbTypes.LineString, + "Polygon": QgsWkbTypes.Polygon, + }[base] - if gdf is None or gdf.empty: - raise ValueError("GeoDataFrame is empty") - - # --- infer geometry type from first non-empty geometry - def infer_wkb(geoms): - for g in geoms: - if g is None: - continue - if hasattr(g, "is_empty") and g.is_empty: - continue - if isinstance(g, MultiPoint): return QgsWkbTypes.MultiPoint - if isinstance(g, Point): return QgsWkbTypes.Point - if isinstance(g, MultiLineString): return QgsWkbTypes.MultiLineString - if isinstance(g, LineString): return QgsWkbTypes.LineString - if isinstance(g, MultiPolygon): return QgsWkbTypes.MultiPolygon - if isinstance(g, Polygon): return QgsWkbTypes.Polygon - raise ValueError("Could not infer geometry type (all geometries empty?)") - - wkb_type = infer_wkb(gdf.geometry) - - # --- build CRS - crs_qgis = QgsCoordinateReferenceSystem() - if gdf.crs is not None: + if any_multi: + fam = QgsWkbTypes.multiType(fam) + if has_z: + fam = QgsWkbTypes.addZ(fam) + return fam + + wkb_type = _infer_wkb(geodataframe.geometry) + + # --- build CRS from gdf.crs + crs = QgsCoordinateReferenceSystem() + if geodataframe.crs is not None: try: - crs_qgis = QgsCoordinateReferenceSystem.fromWkt(gdf.crs.to_wkt()) + crs = QgsCoordinateReferenceSystem.fromWkt(geodataframe.crs.to_wkt()) except Exception: - epsg = gdf.crs.to_epsg() - if epsg: - crs_qgis = QgsCoordinateReferenceSystem.fromEpsgId(int(epsg)) + try: + epsg = geodataframe.crs.to_epsg() + if epsg: + crs = QgsCoordinateReferenceSystem.fromEpsgId(int(epsg)) + except Exception: + pass - geom_str = QgsWkbTypes.displayString(wkb_type) # e.g. "LineString" - uri = f"{geom_str}?crs={crs_qgis.authid()}" if crs_qgis.isValid() else geom_str - layer = QgsVectorLayer(uri, layer_name, "memory") - prov = layer.dataProvider() - - # --- fields: map pandas dtypes → QGIS - import numpy as np + # --- build QGIS fields from pandas dtypes fields = QgsFields() - for col in gdf.columns: - if col == gdf.geometry.name: - continue - dtype = gdf[col].dtype + non_geom_cols = [c for c in geodataframe.columns if c != geodataframe.geometry.name] + + def _qvariant_type(dtype) -> QVariant.Type: if pd.api.types.is_integer_dtype(dtype): - qtype = QVariant.Int - elif pd.api.types.is_float_dtype(dtype): - qtype = QVariant.Double - elif pd.api.types.is_bool_dtype(dtype): - qtype = QVariant.Bool - elif pd.api.types.is_datetime64_any_dtype(dtype): - qtype = QVariant.DateTime - else: - qtype = QVariant.String - fields.append(QgsField(str(col), qtype)) - prov.addAttributes(list(fields)) - layer.updateFields() - - # --- features - feats = [] - non_geom_cols = [c for c in gdf.columns if c != gdf.geometry.name] - - for _, row in gdf.iterrows(): - geom = row[gdf.geometry.name] - if geom is None or (hasattr(geom, "is_empty") and geom.is_empty): + return QVariant.Int + if pd.api.types.is_float_dtype(dtype): + return QVariant.Double + if pd.api.types.is_bool_dtype(dtype): + return QVariant.Bool + if pd.api.types.is_datetime64_any_dtype(dtype): + return QVariant.DateTime + return QVariant.String + + for col in non_geom_cols: + fields.append(QgsField(str(col), _qvariant_type(geodataframe[col].dtype))) + + # --- create sink + sink, dest_id = qgs_algorithm.parameterAsSink( + parameters, + output_key, + context, + fields, + wkb_type, + crs, + ) + if sink is None: + from qgis.core import QgsProcessingException + raise QgsProcessingException("Could not create output sink") + + # --- write features + total = len(geodataframe.index) + is_multi_sink = QgsWkbTypes.isMultiType(wkb_type) + + for i, (_, row) in enumerate(geodataframe.iterrows()): + if feedback.isCanceled(): + break + + geom = row[geodataframe.geometry.name] + if geom is None or getattr(geom, "is_empty", False): continue + # promote single → multi if needed + if is_multi_sink: + if isinstance(geom, Point): + geom = MultiPoint([geom]) + elif isinstance(geom, LineString): + geom = MultiLineString([geom]) + elif isinstance(geom, Polygon): + geom = MultiPolygon([geom]) + f = QgsFeature(fields) - # attributes in declared order with type cleanup + # attributes in declared order attrs = [] for col in non_geom_cols: val = row[col] - # numpy scalar → python scalar - if isinstance(val, (np.generic,)): + if isinstance(val, np.generic): try: val = val.item() except Exception: pass - # pandas Timestamp → QDateTime (if column is datetime) - if pd.api.types.is_datetime64_any_dtype(gdf[col].dtype): + if pd.api.types.is_datetime64_any_dtype(geodataframe[col].dtype): if pd.isna(val): val = None else: @@ -189,10 +268,10 @@ def infer_wkb(geoms): except Exception: f.setGeometry(QgsGeometry.fromWkt(geom.wkt)) - feats.append(f) + sink.addFeature(f, QgsFeatureSink.FastInsert) + + if total: + feedback.setProgress(int(100.0 * (i + 1) / total)) - if feats: - prov.addFeatures(feats) - layer.updateExtents() + return dest_id - return layer # optionally: QgsProject.instance().addMapLayer(layer) From f11d7131477b1ee28b6ae7effdec951541b3fef0 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:37:36 +0930 Subject: [PATCH 008/116] refactor: clean up imports in vectorLayerWrapper.py --- map2loop/main/vectorLayerWrapper.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 90d0625..e58b1b2 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -1,19 +1,19 @@ from qgis.core import ( - QgsVectorLayer, + QgsRaster, QgsFields, - QgsField, - QgsFeature, + QgsField, + QgsFeature, QgsGeometry, QgsWkbTypes, QgsCoordinateReferenceSystem, - QgsProject, - QgsRaster + QgsFeatureSink ) from qgis.PyQt.QtCore import QVariant, QDateTime from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon import pandas as pd import geopandas as gpd +import numpy as np @@ -108,17 +108,6 @@ def GeoDataFrameToQgsLayer(qgs_algorithm, geodataframe, parameters, context, out ------- str : dest_id to return from processAlgorithm, e.g. { output_key: dest_id } """ - import pandas as pd - import numpy as np - from shapely.geometry import ( - Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon - ) - - from qgis.core import ( - QgsFields, QgsField, QgsFeature, QgsGeometry, - QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink - ) - from qgis.PyQt.QtCore import QVariant, QDateTime if feedback is None: class _Dummy: From d718b962120d91b310eef219acc9b25c8a789ce5 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 13:38:02 +0930 Subject: [PATCH 009/116] feature: add basal contacts extraction algorithm --- .../algorithms/extract_basal_contacts.py | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/map2loop/processing/algorithms/extract_basal_contacts.py b/map2loop/processing/algorithms/extract_basal_contacts.py index 11d406c..534958e 100644 --- a/map2loop/processing/algorithms/extract_basal_contacts.py +++ b/map2loop/processing/algorithms/extract_basal_contacts.py @@ -8,9 +8,10 @@ * * *************************************************************************** """ - +# Python imports from typing import Any, Optional +# QGIS imports from qgis import processing from qgis.core import ( QgsFeatureSink, @@ -22,17 +23,23 @@ QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, ) +# Internal imports +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from map2loop import ContactExtractor class BasalContactsAlgorithm(QgsProcessingAlgorithm): """Processing algorithm to create basal contacts.""" - - INPUT = "INPUT" - OUTPUT = "OUTPUT" + + + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_FAULTS = 'FAULTS' + INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + OUTPUT = "BASAL_CONTACTS" def name(self) -> str: """Return the algorithm name.""" - return "loop: basal_contacts" + return "basal_contacts" def displayName(self) -> str: """Return the algorithm display name.""" @@ -48,20 +55,37 @@ def groupId(self) -> str: def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" + self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT, - "Geology Polygons", + self.INPUT_GEOLOGY, + "GEOLOGY", [QgsProcessing.TypeVectorPolygon], ) ) + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_FAULTS, + "FAULTS", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "STRATIGRAPHIC_COLUMN", + [QgsProcessing.TypeVectorLine], + ) + ) + self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, "Basal Contacts", ) ) - pass def processAlgorithm( self, @@ -69,7 +93,26 @@ def processAlgorithm( context: QgsProcessingContext, feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - pass + + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + + geology = qgsLayerToGeoDataFrame(geology) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + + feedback.pushInfo("Extracting Basal Contacts...") + contact_extractor = ContactExtractor(geology, faults, feedback) + contact_extractor.extract_basal_contacts(strati_column) + + basal_contacts = GeoDataFrameToQgsLayer( + self, + contact_extractor.basal_contacts, + parameters=parameters, + context=context, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" From 0c7e7688c50f10ba74704c539070ac3984a96be6 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 18 Aug 2025 14:55:57 +0930 Subject: [PATCH 010/116] refactor: add imports in sorter.py --- map2loop/processing/algorithms/sorter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/map2loop/processing/algorithms/sorter.py b/map2loop/processing/algorithms/sorter.py index 999fcad..a159855 100644 --- a/map2loop/processing/algorithms/sorter.py +++ b/map2loop/processing/algorithms/sorter.py @@ -3,7 +3,10 @@ from qgis import processing from qgis.core import ( QgsFeatureSink, - QgsFields, QgsField, QgsFeature, QgsGeometry, + QgsFields, + QgsField, + QgsFeature, + QgsGeometry, QgsProcessing, QgsProcessingAlgorithm, QgsProcessingContext, @@ -13,6 +16,7 @@ QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, QgsVectorLayer, + QgsWkbTypes ) # ──────────────────────────────────────────────── From aeed68b750cbb46634a646f0cf67ae56d16d43d3 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:18:44 +0930 Subject: [PATCH 011/116] feature: implement sampler algorithm --- map2loop/processing/algorithms/sampler.py | 149 ++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 map2loop/processing/algorithms/sampler.py diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py new file mode 100644 index 0000000..5962a2a --- /dev/null +++ b/map2loop/processing/algorithms/sampler.py @@ -0,0 +1,149 @@ +""" +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" +# Python imports +from typing import Any, Optional + +# QGIS imports +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterString, + QgsProcessingParameterNumber +) +# Internal imports +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from map2loop.map2loop.sampler import SamplerDecimator, SamplerSpacing + + +class SamplerAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm for sampling.""" + + INPUT_SAMPLER_TYPE = 'SAMPLER_TYPE' + INPUT_DTM = 'DTM' + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_SPATIAL_DATA = 'SPATIAL_DATA' + INPUT_DECIMATION = 'DECIMATION' + INPUT_SPACING = 'SPACING' + + OUTPUT = "SAMPLED_CONTACTS" + + def name(self) -> str: + """Return the algorithm name.""" + return "sampler" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Sampler" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + + + self.addParameter( + QgsProcessingParameterString( + self.INPUT_SAMPLER_TYPE, + "SAMPLER_TYPE", + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_DTM, + "DTM", + [QgsProcessing.TypeVectorRaster], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_GEOLOGY, + "GEOLOGY", + [QgsProcessing.TypeVectorPolygon], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_SPATIAL_DATA, + "SPATIAL_DATA", + [QgsProcessing.TypeVectorAnyGeometry], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterNumber( + self.INPUT_DECIMATION, + "DECIMATION", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterNumber( + self.INPUT_SPACING, + "SPACING", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Sampled Contacts", + ) + ) + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + dtm = self.parameterAsSource(parameters, self.INPUT_DTM, context) + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + spatial_data = self.parameterAsSource(parameters, self.INPUT_SPATIAL_DATA, context) + decimation = self.parameterAsSource(parameters, self.INPUT_DECIMATION, context) + spacing = self.parameterAsSource(parameters, self.INPUT_SPACING, context) + sampler_type = self.parameterAsString(parameters, self.INPUT_SAMPLER_TYPE, context) + + # Convert geology layers to GeoDataFrames + geology = qgsLayerToGeoDataFrame(geology) + spatial_data = qgsLayerToGeoDataFrame(spatial_data) + + if sampler_type == "SamplerDecimator": + feedback.pushInfo("Sampling...") + sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) + samples = sampler.sample(spatial_data) + + samples = qgs + return {self.OUTPUT: basal_contacts} + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file From 78f151123c69a48df9216dd21eb28c77d675638e Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:19:05 +0930 Subject: [PATCH 012/116] feature: add thickness calculator algorithms --- .../algorithms/thickness_calculator.py | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 map2loop/processing/algorithms/thickness_calculator.py diff --git a/map2loop/processing/algorithms/thickness_calculator.py b/map2loop/processing/algorithms/thickness_calculator.py new file mode 100644 index 0000000..9b790ce --- /dev/null +++ b/map2loop/processing/algorithms/thickness_calculator.py @@ -0,0 +1,318 @@ +""" +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" +# Python imports +from typing import Any, Optional + +# QGIS imports +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, +) +# Internal imports +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from map2loop.map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint + + +class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm for thickness calculations.""" + + + INPUT_DTM = 'DTM' + INPUT_BOUNDING_BOX = 'BOUNDING_BOX' + INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' + INPUT_UNITS = 'UNITS' + INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' + INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' + + OUTPUT = "THICKNESS" + + def name(self) -> str: + """Return the algorithm name.""" + return "thickness_calculator" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Thickness Calculator" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_GEOLOGY, + "GEOLOGY", + [QgsProcessing.TypeVectorPolygon], + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_FAULTS, + "FAULTS", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "STRATIGRAPHIC_COLUMN", + [QgsProcessing.TypeVectorLine], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Basal Contacts", + ) + ) + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + + geology = qgsLayerToGeoDataFrame(geology) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + + feedback.pushInfo("Extracting Basal Contacts...") + contact_extractor = ContactExtractor(geology, faults, feedback) + contact_extractor.extract_basal_contacts(strati_column) + + basal_contacts = GeoDataFrameToQgsLayer( + self, + contact_extractor.basal_contacts, + parameters=parameters, + context=context, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts} + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() + + + +class InterpolatedStructureAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm for thickness calculations.""" + + + INPUT_UNITS = 'UNITS' + INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' + INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' + + OUTPUT = "THICKNESS" + + def name(self) -> str: + """Return the algorithm name.""" + return "thickness_calculator" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Thickness Calculator" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_GEOLOGY, + "GEOLOGY", + [QgsProcessing.TypeVectorPolygon], + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_FAULTS, + "FAULTS", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "STRATIGRAPHIC_COLUMN", + [QgsProcessing.TypeVectorLine], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Basal Contacts", + ) + ) + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + + geology = qgsLayerToGeoDataFrame(geology) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + + feedback.pushInfo("Extracting Basal Contacts...") + contact_extractor = ContactExtractor(geology, faults, feedback) + contact_extractor.extract_basal_contacts(strati_column) + + basal_contacts = GeoDataFrameToQgsLayer( + self, + contact_extractor.basal_contacts, + parameters=parameters, + context=context, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts} + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() + + + +class StructuralPointAlgorithm(QgsProcessingAlgorithm): + """Processing algorithm for thickness calculations.""" + + + INPUT_UNITS = 'UNITS' + INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' + INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_GEOLOGY = 'GEOLOGY' + INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' + + OUTPUT = "THICKNESS" + + def name(self) -> str: + """Return the algorithm name.""" + return "thickness_calculator" + + def displayName(self) -> str: + """Return the algorithm display name.""" + return "Loop3d: Thickness Calculator" + + def group(self) -> str: + """Return the algorithm group name.""" + return "Loop3d" + + def groupId(self) -> str: + """Return the algorithm group ID.""" + return "loop3d" + + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + """Initialize the algorithm parameters.""" + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_GEOLOGY, + "GEOLOGY", + [QgsProcessing.TypeVectorPolygon], + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_FAULTS, + "FAULTS", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "STRATIGRAPHIC_COLUMN", + [QgsProcessing.TypeVectorLine], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Basal Contacts", + ) + ) + + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + + geology = qgsLayerToGeoDataFrame(geology) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + + feedback.pushInfo("Extracting Basal Contacts...") + contact_extractor = ContactExtractor(geology, faults, feedback) + contact_extractor.extract_basal_contacts(strati_column) + + basal_contacts = GeoDataFrameToQgsLayer( + self, + contact_extractor.basal_contacts, + parameters=parameters, + context=context, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts} + + def createInstance(self) -> QgsProcessingAlgorithm: + """Create a new instance of the algorithm.""" + return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file From 40694065ebf7a0d40a6312f2e2f45aa1742c67a1 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:25:55 +0930 Subject: [PATCH 013/116] fix: correct return value --- map2loop/processing/algorithms/sampler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py index 5962a2a..be6b134 100644 --- a/map2loop/processing/algorithms/sampler.py +++ b/map2loop/processing/algorithms/sampler.py @@ -142,8 +142,8 @@ def processAlgorithm( samples = sampler.sample(spatial_data) samples = qgs - return {self.OUTPUT: basal_contacts} + return {self.OUTPUT: samples} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file + return self.__class__() # SamplerAlgorithm() \ No newline at end of file From f67be330d9a63cb7543ab7e9b4e58e2e252e3cfa Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:27:31 +0930 Subject: [PATCH 014/116] feature: add support SamplerSpacing --- map2loop/processing/algorithms/sampler.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py index be6b134..b9a182b 100644 --- a/map2loop/processing/algorithms/sampler.py +++ b/map2loop/processing/algorithms/sampler.py @@ -137,11 +137,16 @@ def processAlgorithm( spatial_data = qgsLayerToGeoDataFrame(spatial_data) if sampler_type == "SamplerDecimator": - feedback.pushInfo("Sampling...") - sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) - - samples = qgs + feedback.pushInfo("Sampling...") + sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) + samples = sampler.sample(spatial_data) + if sampler_type == "SamplerSpacing": + feedback.pushInfo("Sampling...") + sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) + samples = sampler.sample(spatial_data) + + #TODO: convert sample to qgis layer + # samples = qgs return {self.OUTPUT: samples} def createInstance(self) -> QgsProcessingAlgorithm: From 3c465534b83e1d61789e479d49850484e66b4ca6 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Mon, 25 Aug 2025 14:28:40 +0930 Subject: [PATCH 015/116] fix: update sampler type strings for consistency --- map2loop/processing/algorithms/sampler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py index b9a182b..1011cf6 100644 --- a/map2loop/processing/algorithms/sampler.py +++ b/map2loop/processing/algorithms/sampler.py @@ -136,11 +136,12 @@ def processAlgorithm( geology = qgsLayerToGeoDataFrame(geology) spatial_data = qgsLayerToGeoDataFrame(spatial_data) - if sampler_type == "SamplerDecimator": + if sampler_type == "decimator": feedback.pushInfo("Sampling...") sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) samples = sampler.sample(spatial_data) - if sampler_type == "SamplerSpacing": + + if sampler_type == "spacing": feedback.pushInfo("Sampling...") sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) samples = sampler.sample(spatial_data) From 7e76267655033124d0434498d385306f4145ccb4 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Tue, 26 Aug 2025 11:47:41 +0930 Subject: [PATCH 016/116] fix: convert dataframe to qgis layer --- map2loop/processing/algorithms/sampler.py | 48 ++++++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/map2loop/processing/algorithms/sampler.py b/map2loop/processing/algorithms/sampler.py index 1011cf6..3feb3e6 100644 --- a/map2loop/processing/algorithms/sampler.py +++ b/map2loop/processing/algorithms/sampler.py @@ -10,9 +10,9 @@ """ # Python imports from typing import Any, Optional +from qgis.PyQt.QtCore import QMetaType # QGIS imports -from qgis import processing from qgis.core import ( QgsFeatureSink, QgsProcessing, @@ -23,10 +23,15 @@ QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, QgsProcessingParameterString, - QgsProcessingParameterNumber + QgsProcessingParameterNumber, + QgsField, + QgsFeature, + QgsGeometry, + QgsPointXY, + QgsVectorLayer ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame from map2loop.map2loop.sampler import SamplerDecimator, SamplerSpacing @@ -145,10 +150,41 @@ def processAlgorithm( feedback.pushInfo("Sampling...") sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) samples = sampler.sample(spatial_data) + - #TODO: convert sample to qgis layer - # samples = qgs - return {self.OUTPUT: samples} + # create layer + vector_layer = QgsVectorLayer("Point", "sampled_points", "memory") + provider = vector_layer.dataProvider() + + # add fields + provider.addAttributes([QgsField("ID", QMetaType.Type.QString), + QgsField("X", QMetaType.Type.Float), + QgsField("Y", QMetaType.Type.Float), + QgsField("Z", QMetaType.Type.Float), + QgsField("featureId", QMetaType.Type.QString) + ]) + vector_layer.updateFields() # tell the vector layer to fetch changes from the provider + + # add a feature + for i in range(len(samples)): + feature = QgsFeature() + feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(samples.X[i], samples.Y[i], samples.Z[i]))) + feature.setAttributes([samples.ID[i], samples.X[i], samples.Y[i], samples.Z[i], samples.featureId[i]]) + provider.addFeatures([feature]) + + # update layer's extent when new features have been added + # because change of extent in provider is not propagated to the layer + vector_layer.updateExtents() + # --- create sink + sink, dest_id = self.parameterAsSink( + parameters, + self.OUTPUT, + context, + vector_layer.fields(), + QgsGeometry.Type.Point, + spatial_data.crs, + ) + return {self.OUTPUT: dest_id} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" From c42a4ddfc86a9adb1191f9160de25d884193c502 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Wed, 27 Aug 2025 10:36:57 +0930 Subject: [PATCH 017/116] feature: add dataframe to point sink conversion --- map2loop/main/vectorLayerWrapper.py | 189 +++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 2 deletions(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index e58b1b2..7069c88 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -1,3 +1,5 @@ +# PyQGIS / PyQt imports + from qgis.core import ( QgsRaster, QgsFields, @@ -6,8 +8,10 @@ QgsGeometry, QgsWkbTypes, QgsCoordinateReferenceSystem, - QgsFeatureSink + QgsFeatureSink, + QgsProcessingException ) + from qgis.PyQt.QtCore import QVariant, QDateTime from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon @@ -208,7 +212,6 @@ def _qvariant_type(dtype) -> QVariant.Type: crs, ) if sink is None: - from qgis.core import QgsProcessingException raise QgsProcessingException("Could not create output sink") # --- write features @@ -264,3 +267,185 @@ def _qvariant_type(dtype) -> QVariant.Type: return dest_id + +# ---------- helpers ---------- + +def _qvariant_type_from_dtype(dtype) -> QVariant.Type: + """Map a pandas dtype to a QVariant type.""" + import numpy as np + if np.issubdtype(dtype, np.integer): + # prefer 64-bit when detected + try: + return QVariant.LongLong + except AttributeError: + return QVariant.Int + if np.issubdtype(dtype, np.floating): + return QVariant.Double + if np.issubdtype(dtype, np.bool_): + return QVariant.Bool + # datetimes + try: + import pandas as pd + if pd.api.types.is_datetime64_any_dtype(dtype): + return QVariant.DateTime + if pd.api.types.is_datetime64_ns_dtype(dtype): + return QVariant.DateTime + if pd.api.types.is_datetime64_dtype(dtype): + return QVariant.DateTime + if pd.api.types.is_timedelta64_dtype(dtype): + # store as string "HH:MM:SS" fallback + return QVariant.String + except Exception: + pass + # default to string + return QVariant.String + + +def _fields_from_dataframe(df, drop_cols=None) -> QgsFields: + """Build QgsFields from DataFrame dtypes.""" + drop_cols = set(drop_cols or []) + fields = QgsFields() + for name, dtype in df.dtypes.items(): + if name in drop_cols: + continue + vtype = _qvariant_type_from_dtype(dtype) + fields.append(QgsField(name, vtype)) + return fields + + +# ---------- main function you'll call inside processAlgorithm ---------- + +def dataframe_to_point_sink( + df, + x_col: str, + y_col: str, + *, + crs: QgsCoordinateReferenceSystem, + algorithm, # `self` inside a QgsProcessingAlgorithm + parameters: dict, + context, + feedback, + sink_param_name: str = "OUTPUT", + z_col: str = None, + m_col: str = None, + include_coords_in_attrs: bool = False, +): + """ + Write a pandas DataFrame to a point feature sink (QgsProcessingParameterFeatureSink). + + Params + ------ + df : pandas.DataFrame Data with coordinate columns. + x_col, y_col : str Column names for X/Easting/Longitude and Y/Northing/Latitude. + crs : QgsCoordinateReferenceSystem CRS of the coordinates (e.g., QgsCoordinateReferenceSystem('EPSG:4326')). + algorithm : QgsProcessingAlgorithm Use `self` from inside processAlgorithm. + parameters, context, feedback Standard Processing plumbing. + sink_param_name : str Name of your sink output parameter (default "OUTPUT"). + z_col, m_col : str | None Optional Z and M columns for 3D/M points. + include_coords_in_attrs : bool If False, x/y/z/m are not written as attributes. + + Returns + ------- + (sink, sink_id) The created sink and its ID. Also returns feature count via feedback. + """ + import pandas as pd + if not isinstance(df, pd.DataFrame): + raise TypeError("df must be a pandas.DataFrame") + + # Make a working copy; optionally drop coordinate columns from attributes + attr_df = df.copy() + drop_cols = [] + for col in [x_col, y_col, z_col, m_col]: + if col and not include_coords_in_attrs: + drop_cols.append(col) + + fields = _fields_from_dataframe(attr_df, drop_cols=drop_cols) + + # Geometry type (2D/3D/M) + has_z = z_col is not None and z_col in df.columns + has_m = m_col is not None and m_col in df.columns + if has_z and has_m: + wkb = QgsWkbTypes.PointZM + elif has_z: + wkb = QgsWkbTypes.PointZ + elif has_m: + wkb = QgsWkbTypes.PointM + else: + wkb = QgsWkbTypes.Point + + # Create the sink + sink, sink_id = algorithm.parameterAsSink( + parameters, + sink_param_name, + context, + fields, + wkb, + crs + ) + if sink is None: + raise QgsProcessingException("Could not create feature sink. Check output parameter and inputs.") + + total = len(df) + feedback.pushInfo(f"Writing {total} features…") + + # Precompute attribute column order + attr_columns = [f.name() for f in fields] + + # Iterate rows and write features + for i, (idx, row) in enumerate(df.iterrows(), start=1): + if feedback.isCanceled(): + break + + # Build point geometry + x = row[x_col] + y = row[y_col] + + # skip rows with missing coords + if pd.isna(x) or pd.isna(y): + continue + + if has_z and not pd.isna(row[z_col]) and has_m and not pd.isna(row[m_col]): + pt = QgsPoint(float(x), float(y), float(row[z_col]), float(row[m_col])) + elif has_z and not pd.isna(row[z_col]): + pt = QgsPoint(float(x), float(y), float(row[z_col])) + elif has_m and not pd.isna(row[m_col]): + # PointM constructor: setZValue not needed; M is the 4th ordinate + pt = QgsPoint(float(x), float(y)) + pt.setM(float(row[m_col])) + else: + pt = QgsPointXY(float(x), float(y)) + + feat = QgsFeature(fields) + feat.setGeometry(QgsGeometry.fromPoint(pt) if isinstance(pt, QgsPoint) else QgsGeometry.fromPointXY(pt)) + + # Attributes in the same order as fields + attrs = [] + for col in attr_columns: + val = row[col] if col in row else None + # Pandas NaN -> None + if pd.isna(val): + val = None + # Convert numpy types to Python scalars to avoid QVariant issues + try: + import numpy as np + if isinstance(val, (np.generic,)): + val = val.item() + except Exception: + pass + # Convert pandas Timestamp to Python datetime + if hasattr(val, "to_pydatetime"): + try: + val = val.to_pydatetime() + except Exception: + val = str(val) + attrs.append(val) + feat.setAttributes(attrs) + + sink.addFeature(feat, QgsFeature.FastInsert) + + if i % 1000 == 0: + feedback.setProgress(int(100.0 * i / max(total, 1))) + + feedback.pushInfo("Done.") + feedback.setProgress(100) + return sink, sink_id From 54a0bc98eea26f2b22632e4e8f8c03c66798c360 Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Wed, 27 Aug 2025 10:37:27 +0930 Subject: [PATCH 018/116] fix: add input parameters --- .../algorithms/thickness_calculator.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/map2loop/processing/algorithms/thickness_calculator.py b/map2loop/processing/algorithms/thickness_calculator.py index 9b790ce..24b684a 100644 --- a/map2loop/processing/algorithms/thickness_calculator.py +++ b/map2loop/processing/algorithms/thickness_calculator.py @@ -31,7 +31,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): """Processing algorithm for thickness calculations.""" - + INPUT_THICKNESS_CALCULATOR_TYPE = 'THICKNESS_CALCULATOR_TYPE' INPUT_DTM = 'DTM' INPUT_BOUNDING_BOX = 'BOUNDING_BOX' INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' @@ -62,7 +62,33 @@ def groupId(self) -> str: def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" + + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_THICKNESS_CALCULATOR_TYPE, + "Thickness Calculator Type", + [QgsProcessing.TypeVectorPoint], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_BASAL_CONTACTS, + "Basal Contacts", + [QgsProcessing.TypeVectorPoint], + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_DTM, + "DTM", + [QgsProcessing.TypeVectorRaster], + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_GEOLOGY, @@ -90,7 +116,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - "Basal Contacts", + "Thickness", ) ) From df54c1b16304bb386dc2ddb988fa4951a8e58f7f Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Wed, 27 Aug 2025 11:58:30 +0930 Subject: [PATCH 019/116] refactor: rename to dataframeToQgsLayer --- map2loop/main/vectorLayerWrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 7069c88..d68ed3d 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -315,7 +315,7 @@ def _fields_from_dataframe(df, drop_cols=None) -> QgsFields: # ---------- main function you'll call inside processAlgorithm ---------- -def dataframe_to_point_sink( +def dataframeToQgsLayer( df, x_col: str, y_col: str, From 703a89c6baf2561d0f4eee6406c43905b8ba4bed Mon Sep 17 00:00:00 2001 From: rabii-chaarani Date: Wed, 27 Aug 2025 11:58:42 +0930 Subject: [PATCH 020/116] refactor: update thickness calculator parameters and processing logic --- .../algorithms/thickness_calculator.py | 308 ++++++------------ 1 file changed, 93 insertions(+), 215 deletions(-) diff --git a/map2loop/processing/algorithms/thickness_calculator.py b/map2loop/processing/algorithms/thickness_calculator.py index 24b684a..f56b09c 100644 --- a/map2loop/processing/algorithms/thickness_calculator.py +++ b/map2loop/processing/algorithms/thickness_calculator.py @@ -22,9 +22,13 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterEnum, + QgsProcessingParameterNumber, + QgsProcessingParameterField, + QgsProcessingParameterMatrix ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer from map2loop.map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint @@ -62,25 +66,15 @@ def groupId(self) -> str: def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" - - self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterEnum( self.INPUT_THICKNESS_CALCULATOR_TYPE, "Thickness Calculator Type", - [QgsProcessing.TypeVectorPoint], + options=['InterpolatedStructure','StructuralPoint'], + allowMultiple=False, ) ) - - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_BASAL_CONTACTS, - "Basal Contacts", - [QgsProcessing.TypeVectorPoint], - ) - ) - self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_DTM, @@ -88,103 +82,36 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorRaster], ) ) - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_GEOLOGY, - "GEOLOGY", - [QgsProcessing.TypeVectorPolygon], + QgsProcessingParameterEnum( + self.INPUT_BOUNDING_BOX, + "Bounding Box", + options=['minx','miny','maxx','maxy'], + allowMultiple=True, ) ) + self.addParameter( + QgsProcessingParameterNumber( + self.INPUT_MAX_LINE_LENGTH, + "Max Line Length", + minValue=0, + defaultValue=1000 + ) + ) self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT_FAULTS, - "FAULTS", + self.INPUT_UNITS, + "Units", [QgsProcessing.TypeVectorLine], - optional=True, ) ) - self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", + self.INPUT_BASAL_CONTACTS, + "Basal Contacts", [QgsProcessing.TypeVectorLine], ) ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - self.OUTPUT, - "Thickness", - ) - ) - - def processAlgorithm( - self, - parameters: dict[str, Any], - context: QgsProcessingContext, - feedback: QgsProcessingFeedback, - ) -> dict[str, Any]: - - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) - - geology = qgsLayerToGeoDataFrame(geology) - faults = qgsLayerToGeoDataFrame(faults) if faults else None - - feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - - basal_contacts = GeoDataFrameToQgsLayer( - self, - contact_extractor.basal_contacts, - parameters=parameters, - context=context, - feedback=feedback, - ) - return {self.OUTPUT: basal_contacts} - - def createInstance(self) -> QgsProcessingAlgorithm: - """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() - - - -class InterpolatedStructureAlgorithm(QgsProcessingAlgorithm): - """Processing algorithm for thickness calculations.""" - - - INPUT_UNITS = 'UNITS' - INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' - INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' - INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' - INPUT_GEOLOGY = 'GEOLOGY' - INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' - - OUTPUT = "THICKNESS" - - def name(self) -> str: - """Return the algorithm name.""" - return "thickness_calculator" - - def displayName(self) -> str: - """Return the algorithm display name.""" - return "Loop3d: Thickness Calculator" - - def group(self) -> str: - """Return the algorithm group name.""" - return "Loop3d" - - def groupId(self) -> str: - """Return the algorithm group ID.""" - return "loop3d" - - def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: - """Initialize the algorithm parameters.""" - self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_GEOLOGY, @@ -193,122 +120,32 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_FAULTS, - "FAULTS", - [QgsProcessing.TypeVectorLine], - optional=True, - ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", - [QgsProcessing.TypeVectorLine], - ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - self.OUTPUT, - "Basal Contacts", - ) - ) - - def processAlgorithm( - self, - parameters: dict[str, Any], - context: QgsProcessingContext, - feedback: QgsProcessingFeedback, - ) -> dict[str, Any]: - - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) - - geology = qgsLayerToGeoDataFrame(geology) - faults = qgsLayerToGeoDataFrame(faults) if faults else None - - feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - - basal_contacts = GeoDataFrameToQgsLayer( - self, - contact_extractor.basal_contacts, - parameters=parameters, - context=context, - feedback=feedback, - ) - return {self.OUTPUT: basal_contacts} - - def createInstance(self) -> QgsProcessingAlgorithm: - """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() - - - -class StructuralPointAlgorithm(QgsProcessingAlgorithm): - """Processing algorithm for thickness calculations.""" - - - INPUT_UNITS = 'UNITS' - INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' - INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' - INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' - INPUT_GEOLOGY = 'GEOLOGY' - INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' - - OUTPUT = "THICKNESS" - - def name(self) -> str: - """Return the algorithm name.""" - return "thickness_calculator" - - def displayName(self) -> str: - """Return the algorithm display name.""" - return "Loop3d: Thickness Calculator" - - def group(self) -> str: - """Return the algorithm group name.""" - return "Loop3d" - - def groupId(self) -> str: - """Return the algorithm group ID.""" - return "loop3d" - - def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: - """Initialize the algorithm parameters.""" - - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_GEOLOGY, - "GEOLOGY", - [QgsProcessing.TypeVectorPolygon], + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=[] ) ) self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT_FAULTS, - "FAULTS", - [QgsProcessing.TypeVectorLine], - optional=True, + self.INPUT_SAMPLED_CONTACTS, + "SAMPLED_CONTACTS", + [QgsProcessing.TypeVectorPoint], ) ) - self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", - [QgsProcessing.TypeVectorLine], + self.INPUT_STRUCTURE_DATA, + "STRUCTURE_DATA", + [QgsProcessing.TypeVectorPoint], ) ) - self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - "Basal Contacts", + "Thickness", ) ) @@ -319,25 +156,66 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) - - geology = qgsLayerToGeoDataFrame(geology) - faults = qgsLayerToGeoDataFrame(faults) if faults else None - - feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - - basal_contacts = GeoDataFrameToQgsLayer( + feedback.pushInfo("Initialising Thickness Calculation Algorithm...") + thickness_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) + dtm_data = self.parameterAsSource(parameters, self.INPUT_DTM, context) + bounding_box = self.parameterAsEnum(parameters, self.INPUT_BOUNDING_BOX, context) + max_line_length = self.parameterAsNumber(parameters, self.INPUT_MAX_LINE_LENGTH, context) + units = self.parameterAsSource(parameters, self.INPUT_UNITS, context) + basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) + geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) + stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) + sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) + + # convert layers to dataframe or geodataframe + geology_data = qgsLayerToGeoDataFrame(geology_data) + units = qgsLayerToDataFrame(units) + basal_contacts = qgsLayerToGeoDataFrame(basal_contacts) + structure_data = qgsLayerToDataFrame(structure_data) + sampled_contacts = qgsLayerToDataFrame(sampled_contacts) + + feedback.pushInfo("Calculating unit thicknesses...") + + if thickness_type == "InterpolatedStructure": + thickness_calculator = InterpolatedStructure( + dtm_data=dtm_data, + bounding_box=bounding_box, + ) + thickness_calculator.compute( + units, + stratigraphic_order, + basal_contacts, + structure_data, + geology_data, + sampled_contacts + ) + + if thickness_type == "StructuralPoint": + thickness_calculator = StructuralPoint( + dtm_data=dtm_data, + bounding_box=bounding_box, + max_line_length=max_line_length, + ) + thickness_calculator.compute( + units, + stratigraphic_order, + basal_contacts, + structure_data, + geology_data, + sampled_contacts + ) + + #TODO: convert thicknesses dataframe to qgs layer + thicknesses = dataframeToQgsLayer( self, - contact_extractor.basal_contacts, + # contact_extractor.basal_contacts, parameters=parameters, context=context, feedback=feedback, ) - return {self.OUTPUT: basal_contacts} + + return {self.OUTPUT: thicknesses[1]} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" From 8bb0c99dadd8298bb715da0a9a263ea65aa03cf5 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 27 Aug 2025 15:10:03 +0800 Subject: [PATCH 021/116] update BasalContactsAlgorithm --- .../algorithms/extract_basal_contacts.py | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/map2loop/processing/algorithms/extract_basal_contacts.py b/map2loop/processing/algorithms/extract_basal_contacts.py index 534958e..ba90572 100644 --- a/map2loop/processing/algorithms/extract_basal_contacts.py +++ b/map2loop/processing/algorithms/extract_basal_contacts.py @@ -22,10 +22,12 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterString, + QgsProcessingParameterField ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer -from map2loop import ContactExtractor +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from map2loop.contact_extractor import ContactExtractor class BasalContactsAlgorithm(QgsProcessingAlgorithm): @@ -63,6 +65,16 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + self.addParameter( + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='unitname' + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_FAULTS, @@ -71,12 +83,13 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - + self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterString( self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", - [QgsProcessing.TypeVectorLine], + "Stratigraphic Column Names", + defaultValue="", + optional=True ) ) @@ -94,22 +107,31 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsString(parameters, self.INPUT_STRATI_COLUMN, context) + + if strati_column and strati_column.strip(): + strati_column = [unit.strip() for unit in strati_column.split(',')] + + unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) faults = qgsLayerToGeoDataFrame(faults) if faults else None + if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: + geology = geology.rename(columns={unit_name_field: 'UNITNAME'}) + feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - + contact_extractor = ContactExtractor(geology, faults) + basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + basal_contacts = GeoDataFrameToQgsLayer( self, contact_extractor.basal_contacts, parameters=parameters, context=context, + output_key=self.OUTPUT, feedback=feedback, ) return {self.OUTPUT: basal_contacts} From fcdf4a2934176cd5f7ad3d856361a432659f629c Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 28 Aug 2025 09:29:44 +0800 Subject: [PATCH 022/116] fix import in vectorLayerWrapper.py --- map2loop/main/vectorLayerWrapper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index d68ed3d..7502ab4 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -9,7 +9,9 @@ QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink, - QgsProcessingException + QgsProcessingException, + QgsPoint, + QgsPointXY, ) from qgis.PyQt.QtCore import QVariant, QDateTime From 7aaa076306b684fadb65125d93809f1d4482a416 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 28 Aug 2025 09:36:39 +0800 Subject: [PATCH 023/116] rename unused loop idx in vectorLayerWrapper --- map2loop/main/vectorLayerWrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map2loop/main/vectorLayerWrapper.py b/map2loop/main/vectorLayerWrapper.py index 7502ab4..d89f6ff 100644 --- a/map2loop/main/vectorLayerWrapper.py +++ b/map2loop/main/vectorLayerWrapper.py @@ -394,7 +394,7 @@ def dataframeToQgsLayer( attr_columns = [f.name() for f in fields] # Iterate rows and write features - for i, (idx, row) in enumerate(df.iterrows(), start=1): + for i, (_idx, row) in enumerate(df.iterrows(), start=1): if feedback.isCanceled(): break From d0d9e7790f57ce59441470f2f351f62489d28b00 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 29 Aug 2025 12:42:40 +0800 Subject: [PATCH 024/116] rename directory to m2l to avoid import conflicts --- docs/conf.py | 2 +- {map2loop => m2l}/__about__.py | 0 {map2loop => m2l}/__init__.py | 0 {map2loop => m2l}/gui/__init__.py | 0 {map2loop => m2l}/gui/dlg_settings.py | 6 +++--- {map2loop => m2l}/gui/dlg_settings.ui | 0 {map2loop => m2l}/main/vectorLayerWrapper.py | 0 {map2loop => m2l}/metadata.txt | 0 {map2loop => m2l}/plugin_main.py | 8 ++++---- {map2loop => m2l}/processing/__init__.py | 0 {map2loop => m2l}/processing/algorithms/__init__.py | 0 .../processing/algorithms/extract_basal_contacts.py | 2 +- {map2loop => m2l}/processing/algorithms/sampler.py | 0 {map2loop => m2l}/processing/algorithms/sorter.py | 2 +- .../processing/algorithms/thickness_calculator.py | 0 {map2loop => m2l}/processing/provider.py | 2 +- .../resources/i18n/plugin_map2loop_en.ts | 0 .../resources/i18n/plugin_translation.pro | 0 {map2loop => m2l}/resources/images/default_icon.png | Bin {map2loop => m2l}/toolbelt/__init__.py | 0 {map2loop => m2l}/toolbelt/env_var_parser.py | 0 {map2loop => m2l}/toolbelt/log_handler.py | 4 ++-- {map2loop => m2l}/toolbelt/preferences.py | 6 +++--- tests/qgis/test_env_var_parser.py | 2 +- tests/qgis/test_plg_preferences.py | 4 ++-- tests/qgis/test_processing.py | 2 +- tests/unit/test_plg_metadata.py | 2 +- 27 files changed, 21 insertions(+), 21 deletions(-) rename {map2loop => m2l}/__about__.py (100%) rename {map2loop => m2l}/__init__.py (100%) rename {map2loop => m2l}/gui/__init__.py (100%) rename {map2loop => m2l}/gui/dlg_settings.py (96%) rename {map2loop => m2l}/gui/dlg_settings.ui (100%) rename {map2loop => m2l}/main/vectorLayerWrapper.py (100%) rename {map2loop => m2l}/metadata.txt (100%) rename {map2loop => m2l}/plugin_main.py (96%) rename {map2loop => m2l}/processing/__init__.py (100%) rename {map2loop => m2l}/processing/algorithms/__init__.py (100%) rename {map2loop => m2l}/processing/algorithms/extract_basal_contacts.py (98%) rename {map2loop => m2l}/processing/algorithms/sampler.py (100%) rename {map2loop => m2l}/processing/algorithms/sorter.py (98%) rename {map2loop => m2l}/processing/algorithms/thickness_calculator.py (100%) rename {map2loop => m2l}/processing/provider.py (98%) rename {map2loop => m2l}/resources/i18n/plugin_map2loop_en.ts (100%) rename {map2loop => m2l}/resources/i18n/plugin_translation.pro (100%) rename {map2loop => m2l}/resources/images/default_icon.png (100%) rename {map2loop => m2l}/toolbelt/__init__.py (100%) rename {map2loop => m2l}/toolbelt/env_var_parser.py (100%) rename {map2loop => m2l}/toolbelt/log_handler.py (98%) rename {map2loop => m2l}/toolbelt/preferences.py (97%) diff --git a/docs/conf.py b/docs/conf.py index 6d7892e..d7cc5df 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ import sphinx_rtd_theme # noqa: F401 theme of Read the Docs # Package -from map2loop import __about__ +from m2l import __about__ # -- Build environment ----------------------------------------------------- on_rtd = environ.get("READTHEDOCS", None) == "True" diff --git a/map2loop/__about__.py b/m2l/__about__.py similarity index 100% rename from map2loop/__about__.py rename to m2l/__about__.py diff --git a/map2loop/__init__.py b/m2l/__init__.py similarity index 100% rename from map2loop/__init__.py rename to m2l/__init__.py diff --git a/map2loop/gui/__init__.py b/m2l/gui/__init__.py similarity index 100% rename from map2loop/gui/__init__.py rename to m2l/gui/__init__.py diff --git a/map2loop/gui/dlg_settings.py b/m2l/gui/dlg_settings.py similarity index 96% rename from map2loop/gui/dlg_settings.py rename to m2l/gui/dlg_settings.py index bf6c8b1..351a008 100644 --- a/map2loop/gui/dlg_settings.py +++ b/m2l/gui/dlg_settings.py @@ -18,15 +18,15 @@ from qgis.PyQt.QtGui import QDesktopServices, QIcon # project -from map2loop.__about__ import ( +from m2l.__about__ import ( __icon_path__, __title__, __uri_homepage__, __uri_tracker__, __version__, ) -from map2loop.toolbelt import PlgLogger, PlgOptionsManager -from map2loop.toolbelt.preferences import PlgSettingsStructure +from m2l.toolbelt import PlgLogger, PlgOptionsManager +from m2l.toolbelt.preferences import PlgSettingsStructure # ############################################################################ # ########## Globals ############### diff --git a/map2loop/gui/dlg_settings.ui b/m2l/gui/dlg_settings.ui similarity index 100% rename from map2loop/gui/dlg_settings.ui rename to m2l/gui/dlg_settings.ui diff --git a/map2loop/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py similarity index 100% rename from map2loop/main/vectorLayerWrapper.py rename to m2l/main/vectorLayerWrapper.py diff --git a/map2loop/metadata.txt b/m2l/metadata.txt similarity index 100% rename from map2loop/metadata.txt rename to m2l/metadata.txt diff --git a/map2loop/plugin_main.py b/m2l/plugin_main.py similarity index 96% rename from map2loop/plugin_main.py rename to m2l/plugin_main.py index f803c9a..a286f1e 100644 --- a/map2loop/plugin_main.py +++ b/m2l/plugin_main.py @@ -15,17 +15,17 @@ from qgis.PyQt.QtWidgets import QAction # project -from map2loop.__about__ import ( +from m2l.__about__ import ( DIR_PLUGIN_ROOT, __icon_path__, __title__, __uri_homepage__, ) -from map2loop.gui.dlg_settings import PlgOptionsFactory -from map2loop.processing import ( +from m2l.gui.dlg_settings import PlgOptionsFactory +from m2l.processing import ( Map2LoopProvider, ) -from map2loop.toolbelt import PlgLogger +from m2l.toolbelt import PlgLogger # ############################################################################ # ########## Classes ############### diff --git a/map2loop/processing/__init__.py b/m2l/processing/__init__.py similarity index 100% rename from map2loop/processing/__init__.py rename to m2l/processing/__init__.py diff --git a/map2loop/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py similarity index 100% rename from map2loop/processing/algorithms/__init__.py rename to m2l/processing/algorithms/__init__.py diff --git a/map2loop/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py similarity index 98% rename from map2loop/processing/algorithms/extract_basal_contacts.py rename to m2l/processing/algorithms/extract_basal_contacts.py index 534958e..f329229 100644 --- a/map2loop/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -25,7 +25,7 @@ ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer -from map2loop import ContactExtractor +from map2loop.contact_extractor import ContactExtractor class BasalContactsAlgorithm(QgsProcessingAlgorithm): diff --git a/map2loop/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py similarity index 100% rename from map2loop/processing/algorithms/sampler.py rename to m2l/processing/algorithms/sampler.py diff --git a/map2loop/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py similarity index 98% rename from map2loop/processing/algorithms/sorter.py rename to m2l/processing/algorithms/sorter.py index a159855..5c4340a 100644 --- a/map2loop/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -177,7 +177,7 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: (units_df, relationships_df, contacts_df, map_data) """ import pandas as pd - from map2loop.map2loop.mapdata import MapData # adjust import path if needed + from m2l.map2loop.mapdata import MapData # adjust import path if needed # Example: convert the geology layer to a very small units_df units_records = [] diff --git a/map2loop/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py similarity index 100% rename from map2loop/processing/algorithms/thickness_calculator.py rename to m2l/processing/algorithms/thickness_calculator.py diff --git a/map2loop/processing/provider.py b/m2l/processing/provider.py similarity index 98% rename from map2loop/processing/provider.py rename to m2l/processing/provider.py index ba879bb..4095979 100644 --- a/map2loop/processing/provider.py +++ b/m2l/processing/provider.py @@ -10,7 +10,7 @@ from qgis.PyQt.QtGui import QIcon # project -from map2loop.__about__ import ( +from m2l.__about__ import ( __icon_path__, __title__, __version__, diff --git a/map2loop/resources/i18n/plugin_map2loop_en.ts b/m2l/resources/i18n/plugin_map2loop_en.ts similarity index 100% rename from map2loop/resources/i18n/plugin_map2loop_en.ts rename to m2l/resources/i18n/plugin_map2loop_en.ts diff --git a/map2loop/resources/i18n/plugin_translation.pro b/m2l/resources/i18n/plugin_translation.pro similarity index 100% rename from map2loop/resources/i18n/plugin_translation.pro rename to m2l/resources/i18n/plugin_translation.pro diff --git a/map2loop/resources/images/default_icon.png b/m2l/resources/images/default_icon.png similarity index 100% rename from map2loop/resources/images/default_icon.png rename to m2l/resources/images/default_icon.png diff --git a/map2loop/toolbelt/__init__.py b/m2l/toolbelt/__init__.py similarity index 100% rename from map2loop/toolbelt/__init__.py rename to m2l/toolbelt/__init__.py diff --git a/map2loop/toolbelt/env_var_parser.py b/m2l/toolbelt/env_var_parser.py similarity index 100% rename from map2loop/toolbelt/env_var_parser.py rename to m2l/toolbelt/env_var_parser.py diff --git a/map2loop/toolbelt/log_handler.py b/m2l/toolbelt/log_handler.py similarity index 98% rename from map2loop/toolbelt/log_handler.py rename to m2l/toolbelt/log_handler.py index 222fa3f..284365e 100644 --- a/map2loop/toolbelt/log_handler.py +++ b/m2l/toolbelt/log_handler.py @@ -12,8 +12,8 @@ from qgis.utils import iface # project package -import map2loop.toolbelt.preferences as plg_prefs_hdlr -from map2loop.__about__ import __title__ +import m2l.toolbelt.preferences as plg_prefs_hdlr +from m2l.__about__ import __title__ # ############################################################################ # ########## Classes ############### diff --git a/map2loop/toolbelt/preferences.py b/m2l/toolbelt/preferences.py similarity index 97% rename from map2loop/toolbelt/preferences.py rename to m2l/toolbelt/preferences.py index 85986b1..8742313 100644 --- a/map2loop/toolbelt/preferences.py +++ b/m2l/toolbelt/preferences.py @@ -11,9 +11,9 @@ from qgis.core import QgsSettings # package -import map2loop.toolbelt.log_handler as log_hdlr -from map2loop.__about__ import __title__, __version__ -from map2loop.toolbelt.env_var_parser import EnvVarParser +import m2l.toolbelt.log_handler as log_hdlr +from m2l.__about__ import __title__, __version__ +from m2l.toolbelt.env_var_parser import EnvVarParser # ############################################################################ # ########## Classes ############### diff --git a/tests/qgis/test_env_var_parser.py b/tests/qgis/test_env_var_parser.py index ce4b73d..b968008 100644 --- a/tests/qgis/test_env_var_parser.py +++ b/tests/qgis/test_env_var_parser.py @@ -1,7 +1,7 @@ import os import unittest -from map2loop.toolbelt.env_var_parser import EnvVarParser +from m2l.toolbelt.env_var_parser import EnvVarParser class TestEnvVarParser(unittest.TestCase): diff --git a/tests/qgis/test_plg_preferences.py b/tests/qgis/test_plg_preferences.py index 23daa19..8ac07a3 100644 --- a/tests/qgis/test_plg_preferences.py +++ b/tests/qgis/test_plg_preferences.py @@ -18,8 +18,8 @@ from qgis.testing import unittest # project -from map2loop.__about__ import __version__ -from map2loop.toolbelt.preferences import ( +from m2l.__about__ import __version__ +from m2l.toolbelt.preferences import ( PREFIX_ENV_VARIABLE, PlgOptionsManager, PlgSettingsStructure, diff --git a/tests/qgis/test_processing.py b/tests/qgis/test_processing.py index b52b3de..6f5c719 100644 --- a/tests/qgis/test_processing.py +++ b/tests/qgis/test_processing.py @@ -15,7 +15,7 @@ from qgis.core import QgsApplication from qgis.testing import start_app, unittest -from map2loop.processing.provider import ( +from m2l.processing.provider import ( Map2LoopProvider, ) diff --git a/tests/unit/test_plg_metadata.py b/tests/unit/test_plg_metadata.py index 2baf6ed..e6373a6 100644 --- a/tests/unit/test_plg_metadata.py +++ b/tests/unit/test_plg_metadata.py @@ -18,7 +18,7 @@ from packaging.version import parse # project -from map2loop import __about__ +from m2l import __about__ # ############################################################################ # ########## Classes ############# From 34f3d25289fc12d44b5752832ef27e90355b7309 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 29 Aug 2025 13:40:31 +0800 Subject: [PATCH 025/116] fix linter.yml --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index f87c4e5..a725d82 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -13,7 +13,7 @@ on: workflow_dispatch: env: - PROJECT_FOLDER: "map2loop" + PROJECT_FOLDER: "m2l" PYTHON_VERSION: 3.9 permissions: contents: write From b72fbf4ae519cab812818beb7326791f625aed18 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:01:43 +0930 Subject: [PATCH 026/116] fix: add QgsPoint import --- m2l/main/vectorLayerWrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index d68ed3d..f981737 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -9,7 +9,8 @@ QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink, - QgsProcessingException + QgsProcessingException, + QgsPoint ) from qgis.PyQt.QtCore import QVariant, QDateTime From 04d3937dadab0945f11ccc45700b25ee7c0395c7 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:04:46 +0930 Subject: [PATCH 027/116] fix: add QgsPointXY import --- m2l/main/vectorLayerWrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index f981737..98597bb 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -10,7 +10,8 @@ QgsCoordinateReferenceSystem, QgsFeatureSink, QgsProcessingException, - QgsPoint + QgsPoint, + QgsPointXY ) from qgis.PyQt.QtCore import QVariant, QDateTime From d4a1d7df88c7f0bc4510a7065eebbd556308d6d1 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:57:36 +0930 Subject: [PATCH 028/116] fix: enforce str type --- m2l/processing/algorithms/extract_basal_contacts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 83f42d0..59793f0 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -28,8 +28,6 @@ # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer from map2loop.contact_extractor import ContactExtractor -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer -from map2loop.contact_extractor import ContactExtractor class BasalContactsAlgorithm(QgsProcessingAlgorithm): @@ -112,15 +110,16 @@ def processAlgorithm( geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) strati_column = self.parameterAsString(parameters, self.INPUT_STRATI_COLUMN, context) - + if strati_column and strati_column.strip(): strati_column = [unit.strip() for unit in strati_column.split(',')] unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) + geology['UNITNAME'] = geology['UNITNAME'].astype(str) + faults = qgsLayerToGeoDataFrame(faults) if faults else None - if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: geology = geology.rename(columns={unit_name_field: 'UNITNAME'}) From cf8321e5fcc2adac476d09e606c387e308b8784a Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:09:00 +0930 Subject: [PATCH 029/116] refactor: handling of string fields --- m2l/main/vectorLayerWrapper.py | 64 +++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index d89f6ff..9be3761 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -23,22 +23,68 @@ +# def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: +# if layer is None: +# return None +# features = layer.getFeatures() +# fields = layer.fields() +# data = {'geometry': []} +# for f in fields: +# data[f.name()] = [] +# for feature in features: +# geom = feature.geometry() +# if geom.isEmpty(): +# continue +# data['geometry'].append(geom) +# for f in fields: +# data[f.name()].append(feature[f.name()]) +# return gpd.GeoDataFrame(data, crs=layer.crs().authid()) + def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: + """ + Convert a QgsVectorLayer to a GeoDataFrame with: + - Shapely geometries + - Pandas nullable string dtype for QGIS string fields + """ if layer is None: return None - features = layer.getFeatures() + fields = layer.fields() - data = {'geometry': []} - for f in fields: - data[f.name()] = [] - for feature in features: - geom = feature.geometry() + string_field_names = { + f.name() for f in fields + if f.type() in (QVariant.String,) # extend here if you use other text types + } + + rows = [] + for feat in layer.getFeatures(): + geom = feat.geometry() if geom.isEmpty(): continue - data['geometry'].append(geom) + + row = {"geometry": wkb_loads(geom.asWkb())} for f in fields: - data[f.name()].append(feature[f.name()]) - return gpd.GeoDataFrame(data, crs=layer.crs().authid()) + val = feat[f.name()] + # Normalize None in string cols to pandas.NA so StringDtype works cleanly + if f.name() in string_field_names: + row[f.name()] = pd.NA if val is None else str(val) + else: + row[f.name()] = val + rows.append(row) + + if not rows: + # Empty GeoDataFrame with correct schema & crs + gdf = gpd.GeoDataFrame(columns=["geometry"] + [f.name() for f in fields], + geometry="geometry", + crs=layer.crs().authid()) + else: + gdf = gpd.GeoDataFrame(rows, geometry="geometry", crs=layer.crs().authid()) + + # Enforce pandas' nullable string dtype on QGIS string fields + for name in string_field_names: + if name in gdf.columns: + gdf[name] = gdf[name].astype("string") + + return gdf def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: From 60bd84eb3796b7d4a856fc101210045f2045f19d Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:09:38 +0930 Subject: [PATCH 030/116] fix: add missing import for wkb_loads --- m2l/main/vectorLayerWrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 9be3761..ca610fc 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -14,9 +14,10 @@ QgsPointXY, ) -from qgis.PyQt.QtCore import QVariant, QDateTime +from qgis.PyQt.QtCore import QVariant, QDateTime, QVariant from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon +from shapely.wkb import loads as wkb_loads import pandas as pd import geopandas as gpd import numpy as np From 1c7266793a89df434f97ee1769ea8bd06352f65c Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:16:49 +0930 Subject: [PATCH 031/116] refactor: implementation and enforce string dtype --- m2l/main/vectorLayerWrapper.py | 67 +++++++--------------------------- 1 file changed, 13 insertions(+), 54 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index ca610fc..339ae0b 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -24,70 +24,29 @@ -# def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: -# if layer is None: -# return None -# features = layer.getFeatures() -# fields = layer.fields() -# data = {'geometry': []} -# for f in fields: -# data[f.name()] = [] -# for feature in features: -# geom = feature.geometry() -# if geom.isEmpty(): -# continue -# data['geometry'].append(geom) -# for f in fields: -# data[f.name()].append(feature[f.name()]) -# return gpd.GeoDataFrame(data, crs=layer.crs().authid()) - def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: - """ - Convert a QgsVectorLayer to a GeoDataFrame with: - - Shapely geometries - - Pandas nullable string dtype for QGIS string fields - """ if layer is None: return None - + features = layer.getFeatures() fields = layer.fields() - string_field_names = { - f.name() for f in fields - if f.type() in (QVariant.String,) # extend here if you use other text types - } - - rows = [] - for feat in layer.getFeatures(): - geom = feat.geometry() + data = {'geometry': []} + for f in fields: + data[f.name()] = [] + for feature in features: + geom = feature.geometry() if geom.isEmpty(): continue - - row = {"geometry": wkb_loads(geom.asWkb())} + data['geometry'].append(geom) for f in fields: - val = feat[f.name()] - # Normalize None in string cols to pandas.NA so StringDtype works cleanly - if f.name() in string_field_names: - row[f.name()] = pd.NA if val is None else str(val) - else: - row[f.name()] = val - rows.append(row) - - if not rows: - # Empty GeoDataFrame with correct schema & crs - gdf = gpd.GeoDataFrame(columns=["geometry"] + [f.name() for f in fields], - geometry="geometry", - crs=layer.crs().authid()) - else: - gdf = gpd.GeoDataFrame(rows, geometry="geometry", crs=layer.crs().authid()) - - # Enforce pandas' nullable string dtype on QGIS string fields - for name in string_field_names: - if name in gdf.columns: - gdf[name] = gdf[name].astype("string") + data[f.name()].append(feature[f.name()]) + gdf = gpd.GeoDataFrame(data, crs=layer.crs().authid()) + # ✅ Convert only QGIS string fields to pandas string dtype + for f in fields: + if f.type() == QVariant.String and f.name() in gdf.columns: + gdf[f.name()] = gdf[f.name()].astype(str) return gdf - def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: """Convert a vector layer to a pandas DataFrame samples the geometry using either points or the vertices of the lines From 3dd9aea7304ba872f77c87eadffd68151887081b Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:25:29 +0930 Subject: [PATCH 032/116] fix: QGIS string fields are converted to str --- m2l/main/vectorLayerWrapper.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 339ae0b..b02f373 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -38,14 +38,12 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: continue data['geometry'].append(geom) for f in fields: - data[f.name()].append(feature[f.name()]) - gdf = gpd.GeoDataFrame(data, crs=layer.crs().authid()) - - # ✅ Convert only QGIS string fields to pandas string dtype - for f in fields: - if f.type() == QVariant.String and f.name() in gdf.columns: - gdf[f.name()] = gdf[f.name()].astype(str) - return gdf + if f.type() == QVariant.String: + # Ensure string fields are converted to str, not QVariant + data[f.name()].append(str(feature[f.name()])) + else: + data[f.name()].append(feature[f.name()]) + return gpd.GeoDataFrame(data, crs=layer.crs().authid()) def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: """Convert a vector layer to a pandas DataFrame From 86e3f7b2154f707835e5ba454adbbbc1f9afa62d Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:28:23 +0930 Subject: [PATCH 033/116] fix: remove type conversion issues --- m2l/processing/algorithms/extract_basal_contacts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 59793f0..4b95ab0 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -117,7 +117,6 @@ def processAlgorithm( unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - geology['UNITNAME'] = geology['UNITNAME'].astype(str) faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: From 2c376671f14081731fc8cf1d4d8f5d99450a24ee Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:36:03 +0930 Subject: [PATCH 034/116] fix: simplify string field handling --- m2l/main/vectorLayerWrapper.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index b02f373..666df27 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -38,11 +38,7 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: continue data['geometry'].append(geom) for f in fields: - if f.type() == QVariant.String: - # Ensure string fields are converted to str, not QVariant - data[f.name()].append(str(feature[f.name()])) - else: - data[f.name()].append(feature[f.name()]) + data[f.name()].append(feature[f.name()]) return gpd.GeoDataFrame(data, crs=layer.crs().authid()) def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: From 510d82f1a3003157cf00bc8b45e932643431f6ad Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:47:22 +0930 Subject: [PATCH 035/116] fix: ensure string fields are explicitly converted --- m2l/main/vectorLayerWrapper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 666df27..72ff281 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -38,7 +38,10 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: continue data['geometry'].append(geom) for f in fields: - data[f.name()].append(feature[f.name()]) + if f.type() == QVariant.String: + data[f.name()].append(str(feature[f.name()])) + else: + data[f.name()].append(feature[f.name()]) return gpd.GeoDataFrame(data, crs=layer.crs().authid()) def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: From c0282376fe49aad108c133a92f560ff13ea78b69 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 1 Sep 2025 13:08:11 +0800 Subject: [PATCH 036/116] sampler --- m2l/processing/algorithms/__init__.py | 1 + m2l/processing/algorithms/sampler.py | 118 +++++++++++++++----------- m2l/processing/provider.py | 3 +- 3 files changed, 72 insertions(+), 50 deletions(-) diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index 618a57d..f3dd275 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1 +1,2 @@ from .extract_basal_contacts import BasalContactsAlgorithm +from .sampler import SamplerAlgorithm diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 3feb3e6..3a6a1c8 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -11,6 +11,8 @@ # Python imports from typing import Any, Optional from qgis.PyQt.QtCore import QMetaType +from osgeo import gdal +import pandas as pd # QGIS imports from qgis.core import ( @@ -22,17 +24,21 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, - QgsProcessingParameterString, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterEnum, QgsProcessingParameterNumber, + QgsFields, QgsField, QgsFeature, QgsGeometry, QgsPointXY, - QgsVectorLayer + QgsVectorLayer, + QgsWkbTypes, + QgsCoordinateReferenceSystem ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame -from map2loop.map2loop.sampler import SamplerDecimator, SamplerSpacing +from map2loop.sampler import SamplerDecimator, SamplerSpacing class SamplerAlgorithm(QgsProcessingAlgorithm): @@ -68,17 +74,18 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( - QgsProcessingParameterString( + QgsProcessingParameterEnum( self.INPUT_SAMPLER_TYPE, "SAMPLER_TYPE", + ["Decimator", "Spacing"], + defaultValue=0 ) ) self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterRasterLayer( self.INPUT_DTM, "DTM", - [QgsProcessing.TypeVectorRaster], ) ) @@ -104,6 +111,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_DECIMATION, "DECIMATION", + defaultValue=1, optional=True, ) ) @@ -112,6 +120,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_SPACING, "SPACING", + defaultValue=200.0, optional=True, ) ) @@ -130,60 +139,71 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - dtm = self.parameterAsSource(parameters, self.INPUT_DTM, context) - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - spatial_data = self.parameterAsSource(parameters, self.INPUT_SPATIAL_DATA, context) - decimation = self.parameterAsSource(parameters, self.INPUT_DECIMATION, context) - spacing = self.parameterAsSource(parameters, self.INPUT_SPACING, context) - sampler_type = self.parameterAsString(parameters, self.INPUT_SAMPLER_TYPE, context) + dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + spatial_data = self.parameterAsVectorLayer(parameters, self.INPUT_SPATIAL_DATA, context) + decimation = self.parameterAsDouble(parameters, self.INPUT_DECIMATION, context) + spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) + sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) + sampler_type = ["Decimator", "Spacing"][sampler_type_index] # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) - spatial_data = qgsLayerToGeoDataFrame(spatial_data) + spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) + dtm_gdal = gdal.Open(dtm.source()) - if sampler_type == "decimator": + if sampler_type == "Decimator": feedback.pushInfo("Sampling...") - sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) + sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) + samples = sampler.sample(spatial_data_gdf) - if sampler_type == "spacing": + if sampler_type == "Spacing": feedback.pushInfo("Sampling...") - sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) - - - # create layer - vector_layer = QgsVectorLayer("Point", "sampled_points", "memory") - provider = vector_layer.dataProvider() - - # add fields - provider.addAttributes([QgsField("ID", QMetaType.Type.QString), - QgsField("X", QMetaType.Type.Float), - QgsField("Y", QMetaType.Type.Float), - QgsField("Z", QMetaType.Type.Float), - QgsField("featureId", QMetaType.Type.QString) - ]) - vector_layer.updateFields() # tell the vector layer to fetch changes from the provider - - # add a feature - for i in range(len(samples)): - feature = QgsFeature() - feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(samples.X[i], samples.Y[i], samples.Z[i]))) - feature.setAttributes([samples.ID[i], samples.X[i], samples.Y[i], samples.Z[i], samples.featureId[i]]) - provider.addFeatures([feature]) - - # update layer's extent when new features have been added - # because change of extent in provider is not propagated to the layer - vector_layer.updateExtents() - # --- create sink + sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm_gdal, geology_data=geology) + samples = sampler.sample(spatial_data_gdf) + + fields = QgsFields() + fields.append(QgsField("ID", QMetaType.Type.QString)) + fields.append(QgsField("X", QMetaType.Type.Float)) + fields.append(QgsField("Y", QMetaType.Type.Float)) + fields.append(QgsField("Z", QMetaType.Type.Float)) + fields.append(QgsField("featureId", QMetaType.Type.QString)) + + crs = None + if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: + crs = QgsCoordinateReferenceSystem.fromWkt(spatial_data_gdf.crs.to_wkt()) + sink, dest_id = self.parameterAsSink( parameters, self.OUTPUT, context, - vector_layer.fields(), - QgsGeometry.Type.Point, - spatial_data.crs, - ) + fields, + QgsWkbTypes.PointZ if 'Z' in (samples.columns if samples is not None else []) else QgsWkbTypes.Point, + crs + ) + + if samples is not None and not samples.empty: + for index, row in samples.iterrows(): + feature = QgsFeature(fields) + + # decimator has z values + if 'Z' in samples.columns and pd.notna(row.get('Z')): + wkt = f"POINT Z ({row['X']} {row['Y']} {row['Z']})" + feature.setGeometry(QgsGeometry.fromWkt(wkt)) + else: + #spacing has no z values + feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y']))) + + feature.setAttributes([ + str(row.get('ID', '')), + float(row.get('X', 0)), + float(row.get('Y', 0)), + float(row.get('Z', 0)) if pd.notna(row.get('Z')) else 0.0, + str(row.get('featureId', '')) + ]) + + sink.addFeature(feature) + return {self.OUTPUT: dest_id} def createInstance(self) -> QgsProcessingAlgorithm: diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index 4095979..b6a5a97 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -16,7 +16,7 @@ __version__, ) -from .algorithms import BasalContactsAlgorithm +from .algorithms import BasalContactsAlgorithm, SamplerAlgorithm # ############################################################################ # ########## Classes ############### @@ -29,6 +29,7 @@ class Map2LoopProvider(QgsProcessingProvider): def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) + self.addAlgorithm(SamplerAlgorithm()) pass def id(self) -> str: From bc99ec9ba2452addfcc29c3279d09253bb894387 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:01:37 +0930 Subject: [PATCH 037/116] fix: enhance contact rextraction tool --- .../algorithms/extract_basal_contacts.py | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 4b95ab0..947e823 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -23,7 +23,9 @@ QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, QgsProcessingParameterString, - QgsProcessingParameterField + QgsProcessingParameterField, + QgsProcessingParameterMatrix, + QgsSettings ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer @@ -37,6 +39,7 @@ class BasalContactsAlgorithm(QgsProcessingAlgorithm): INPUT_GEOLOGY = 'GEOLOGY' INPUT_FAULTS = 'FAULTS' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_IGNORE_UNITS = 'IGNORE_UNITS' OUTPUT = "BASAL_CONTACTS" def name(self) -> str: @@ -83,12 +86,25 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + ignore_settings = QgsSettings() + last_ignore_units = ignore_settings.value("m2l/ignore_units", "") self.addParameter( - QgsProcessingParameterString( - self.INPUT_STRATI_COLUMN, - "Stratigraphic Column Names", - defaultValue="", + QgsProcessingParameterMatrix( + self.INPUT_IGNORE_UNITS, + "Unit(s) to ignore", + headers=["Unit"], + defaultValue=last_ignore_units, optional=True ) ) @@ -107,25 +123,35 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: + feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsString(parameters, self.INPUT_STRATI_COLUMN, context) - - if strati_column and strati_column.strip(): - strati_column = [unit.strip() for unit in strati_column.split(',')] - + strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + # if strati_column and strati_column.strip(): + # strati_column = [unit.strip() for unit in strati_column.split(',')] + # Save stratigraphic column settings + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', strati_column) + + ignore_settings = QgsSettings() + ignore_settings.setValue("m2l/ignore_units", ignore_units) + unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - + mask = ~geology['Formation'].astype(str).str.strip().isin(ignore_units) + geology = geology[mask].reset_index(drop=True) + faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: geology = geology.rename(columns={unit_name_field: 'UNITNAME'}) - + feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( self, contact_extractor.basal_contacts, From af2056d283711d0fddd0f2a6364540189e8a2e9d Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:09:41 +0930 Subject: [PATCH 038/116] fix: update algorithm imports and registration in provider --- m2l/processing/algorithms/__init__.py | 3 +++ m2l/processing/algorithms/sampler.py | 2 +- m2l/processing/algorithms/sorter.py | 2 +- m2l/processing/algorithms/thickness_calculator.py | 2 +- m2l/processing/provider.py | 11 +++++++++-- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index 618a57d..e3201dc 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1 +1,4 @@ from .extract_basal_contacts import BasalContactsAlgorithm +from .sorter import StratigraphySorterAlgorithm +from .thickness_calculator import ThicknessCalculatorAlgorithm +from .sampler import SamplerAlgorithm \ No newline at end of file diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 3feb3e6..99086d8 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -32,7 +32,7 @@ ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame -from map2loop.map2loop.sampler import SamplerDecimator, SamplerSpacing +from map2loop.sampler import SamplerDecimator, SamplerSpacing class SamplerAlgorithm(QgsProcessingAlgorithm): diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 5c4340a..7d9873c 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -22,7 +22,7 @@ # ──────────────────────────────────────────────── # map2loop sorters # ──────────────────────────────────────────────── -from map2loop.map2loop.sorter import ( +from map2loop.sorter import ( SorterAlpha, SorterAgeBased, SorterMaximiseContacts, diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index f56b09c..ae2baa4 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -29,7 +29,7 @@ ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer -from map2loop.map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint +from map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index 4095979..318e944 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -16,7 +16,12 @@ __version__, ) -from .algorithms import BasalContactsAlgorithm +from .algorithms import ( + BasalContactsAlgorithm, + StratigraphySorterAlgorithm, + ThicknessCalculatorAlgorithm, + SamplerAlgorithm +) # ############################################################################ # ########## Classes ############### @@ -29,7 +34,9 @@ class Map2LoopProvider(QgsProcessingProvider): def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) - pass + self.addAlgorithm(StratigraphySorterAlgorithm()) + self.addAlgorithm(ThicknessCalculatorAlgorithm()) + self.addAlgorithm(SamplerAlgorithm()) def id(self) -> str: """Unique provider id, used for identifying it. This string should be unique, \ From 546724b6849a9a3bfcef93473ee261e43c7f97ca Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:18:44 +0930 Subject: [PATCH 039/116] fix: correct parameter types --- m2l/processing/algorithms/sampler.py | 2 +- m2l/processing/algorithms/sorter.py | 4 ++-- m2l/processing/algorithms/thickness_calculator.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 99086d8..69eb644 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -78,7 +78,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterFeatureSource( self.INPUT_DTM, "DTM", - [QgsProcessing.TypeVectorRaster], + [QgsProcessing.TypeRaster], ) ) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 7d9873c..3b9919f 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -74,7 +74,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT, - self.tr("Geology polygons"), + "Geology polygons", [QgsProcessing.TypeVectorPolygon], ) ) @@ -83,7 +83,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterEnum( self.ALGO, - self.tr("Sorting strategy"), + "Sorting strategy", options=list(SORTER_LIST.keys()), defaultValue=0, # Age-based is safest default ) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index ae2baa4..313ab90 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -79,7 +79,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterFeatureSource( self.INPUT_DTM, "DTM", - [QgsProcessing.TypeVectorRaster], + [QgsProcessing.TypeRaster], ) ) self.addParameter( From 671fed23f60bfc06991fdcc64fb9692d66e47520 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:21:36 +0930 Subject: [PATCH 040/116] fix: standardize algorithm group ID --- m2l/processing/algorithms/extract_basal_contacts.py | 2 +- m2l/processing/algorithms/sampler.py | 2 +- m2l/processing/algorithms/sorter.py | 4 ++-- m2l/processing/algorithms/thickness_calculator.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 947e823..d7beb34 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -56,7 +56,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 69eb644..a4b61f6 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -61,7 +61,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 3b9919f..215e79a 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -58,13 +58,13 @@ def name(self) -> str: return "loop_sorter" def displayName(self) -> str: - return "loop: Stratigraphic sorter" + return "Loop3d: Stratigraphic sorter" def group(self) -> str: return "Loop3d" def groupId(self) -> str: - return "loop3d" + return "Loop3d" # ---------------------------------------------------------- # Parameters diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 313ab90..71f72eb 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -62,7 +62,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" From 8cc49af8f4a9fd2ea6a9d5dc72b86ecfd7710f98 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:45:04 +0930 Subject: [PATCH 041/116] fix: add user-defined sorting option --- m2l/processing/algorithms/sorter.py | 31 +++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 215e79a..ee081ab 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -46,9 +46,10 @@ class StratigraphySorterAlgorithm(QgsProcessingAlgorithm): Creates a one-column ‘stratigraphic column’ table ordered by the selected map2loop sorter. """ - - INPUT = "INPUT" - ALGO = "SORT_ALGO" + METHOD = "METHOD" + INPUT_GEOLOGY = "INPUT_GEOLOGY" + INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" + SORTING_ALGORITHM = "SORTING_ALGORITHM" OUTPUT = "OUTPUT" # ---------------------------------------------------------- @@ -65,19 +66,41 @@ def group(self) -> str: def groupId(self) -> str: return "Loop3d" + + def updateParameters(self, parameters): + selected_method = parameters.get(self.METHOD, 0) + if selected_method == 0: # User-Defined selected + self.parameterDefinition(self.INPUT_STRATI_COLUMN).setMetadata({'widget_wrapper': {'visible': True}}) + self.parameterDefinition(self.SORTING_ALGORITHM).setMetadata({'widget_wrapper': {'visible': False}}) + self.parameterDefinition(self.INPUT_GEOLOGY).setMetadata({'widget_wrapper': {'visible': False}}) + else: # Automatic selected + self.parameterDefinition(self.INPUT_GEOLOGY).setMetadata({'widget_wrapper': {'visible': True}}) + self.parameterDefinition(self.SORTING_ALGORITHM).setMetadata({'widget_wrapper': {'visible': True}}) + self.parameterDefinition(self.INPUT_STRATI_COLUMN).setMetadata({'widget_wrapper': {'visible': False}}) + + return super().updateParameters(parameters) # ---------------------------------------------------------- # Parameters # ---------------------------------------------------------- def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + self.addParameter( + QgsProcessingParameterEnum( + name=self.METHOD, + description='Select Method', + options=['User-Defined', 'Automatic'], + defaultValue=0 + ) + ) self.addParameter( QgsProcessingParameterFeatureSource( - self.INPUT, + self.INPUT_GEOLOGY, "Geology polygons", [QgsProcessing.TypeVectorPolygon], ) ) + # enum so the user can pick the strategy from a dropdown self.addParameter( From ffeb1bc3bf3fa8daae0112a2650bf6e7ccea1b2f Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 3 Sep 2025 12:38:44 +0800 Subject: [PATCH 042/116] input files --- tests/input/faults_clip.cpg | 1 + tests/input/faults_clip.dbf | Bin 0 -> 49957 bytes tests/input/faults_clip.prj | 1 + tests/input/faults_clip.shp | Bin 0 -> 9308 bytes tests/input/faults_clip.shx | Bin 0 -> 380 bytes tests/input/folds_clip.cpg | 1 + tests/input/folds_clip.dbf | Bin 0 -> 9713 bytes tests/input/folds_clip.prj | 1 + tests/input/folds_clip.shp | Bin 0 -> 1804 bytes tests/input/folds_clip.shx | Bin 0 -> 156 bytes tests/input/geol_clip_no_gaps.cpg | 1 + tests/input/geol_clip_no_gaps.dbf | Bin 0 -> 305246 bytes tests/input/geol_clip_no_gaps.prj | 1 + tests/input/geol_clip_no_gaps.shp | Bin 0 -> 103704 bytes tests/input/geol_clip_no_gaps.shx | Bin 0 -> 588 bytes tests/input/structure_clip.cpg | 1 + tests/input/structure_clip.dbf | Bin 0 -> 45216 bytes tests/input/structure_clip.prj | 1 + tests/input/structure_clip.shp | Bin 0 -> 3404 bytes tests/input/structure_clip.shx | Bin 0 -> 1044 bytes 20 files changed, 8 insertions(+) create mode 100644 tests/input/faults_clip.cpg create mode 100644 tests/input/faults_clip.dbf create mode 100644 tests/input/faults_clip.prj create mode 100644 tests/input/faults_clip.shp create mode 100644 tests/input/faults_clip.shx create mode 100644 tests/input/folds_clip.cpg create mode 100644 tests/input/folds_clip.dbf create mode 100644 tests/input/folds_clip.prj create mode 100644 tests/input/folds_clip.shp create mode 100644 tests/input/folds_clip.shx create mode 100644 tests/input/geol_clip_no_gaps.cpg create mode 100644 tests/input/geol_clip_no_gaps.dbf create mode 100644 tests/input/geol_clip_no_gaps.prj create mode 100644 tests/input/geol_clip_no_gaps.shp create mode 100644 tests/input/geol_clip_no_gaps.shx create mode 100644 tests/input/structure_clip.cpg create mode 100644 tests/input/structure_clip.dbf create mode 100644 tests/input/structure_clip.prj create mode 100644 tests/input/structure_clip.shp create mode 100644 tests/input/structure_clip.shx diff --git a/tests/input/faults_clip.cpg b/tests/input/faults_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/input/faults_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/input/faults_clip.dbf b/tests/input/faults_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..35f7f0eb882f291f8a96c7470670505b2ea251e9 GIT binary patch literal 49957 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

sPgE;L>n#AP!K$JA8#-%^CHcexe0 z5g=fjra@{1+jZ;>uFciuV*>)Wr^{zpXl4iadSjX^xVFg?pLoqBr1qMpjWzosWeq0m zz~i-j^qzwhg0hwDkRa1m5d?59KLW27L11T}dubL-=BXDZP;~j6@~0qyXl;x?25AIG z+4Px3!OG`8#C!(gZthK|DHk!ZAh27I7J=Q$^a@kKV6rSomynPl6h^LD9OPci)8&Fg ze61Jp7F^pU>Dpcd;|-p~j@MRkpRGCFI9%JlTm~6DF49H_ycGn9YeV`z51nhnp0K2AlS8yOU7HTVHPh8y z6`V*nb#1C<*@_8lo1t&`klK!okU0aapmJk|s-OxxKYxR8`QIn^5&`WuM*L6Clew3s z9{aL*=8+(jq&9eZiGf@~+X{Oa2~yWBYI>PMe`lUw^jPde-+?uQhycD12_cFU5qpy* zoWhmK!cdI%KPOLqh?94=i3Pl7R#(fVOm2jV+ri3vxgH0}IVg)d)h7gS5V$n1ZD)Ov zgmK57m6dcz=#oO))ySEZH!QC2iF>7AE?deL*Qa_BYcL;fgEyg zLuv^AZw~K@k{h-?$l-wMqlxL>C!i{FOh{tZu8zv9fZ>bPwSjKU&>TgzA(7`M*9KxY z%Q00=a~!=LuT3>IM?U4Jup+OHYo-KoZ4DM(_K0h{A+D{<#GyKbz^fxbT$|hwH1EPV zzYP%ACO00WYm>u|Y7Yy6W_=;9Em^0I9M%X?YlemZac#u4b()jJwRM86uGEvRO}_$P zPFIJdy9Nl{Hn=v;Fg05@O-m7h+}b{*ri+k5R0S)Tgf4Jx@&sTFH)Dm;iBJ5nbxdaW zJ?GIp2xcDpJ%v2Z($v<_b#1?bTWIYa%I@J)1n!_-943&;dd5NkVRA3Dd8L*vUd^(c zCm^%}|I&QDD?CsAMUdvbOWcO8YXcwHFb3yXFMVb_2S@}iap;4x5&UVVA+J9kH%A5h ztOfsZwWt&!FiZr9Ym*z7=3N+vYs0?V$@Q-P+N5jiA7i@0coAsU7vkE8Ya8!8K4;5` zYa1@thAQ_jLGGNcvJem(KwGod4h{Fa18$@5@aT#MROhh!c5Lh`sFZ@m;V42EFf_vL! ziLO}@CTj*?G>Gi>-iyFZ*JFQM$8rsn)UZ^;GELjq(Y48^rVzR|Od*=4 zVdqBp?Is3U8&)P#$eaW$(5)9wag=S?Tw7reF<#pTp+{qEZzMLx8X245&uS?f=+_FW z&G*tN7XrB61#RoE{|yA^`#5=e$dQDM&F{Et4m0B4W#YB9xwawd+SbF2y(*JELf|L_ zs{7VagzDV|I$DZVRg?+b^>|XEHiEHZ}V!FVH5op$zOJ&*^+vZM2Je<_@ zGazuWx;E@w2H5t7o_jzuU4(%8%?!uZ-C=WM*gCnMxHjcVH5~`&G+^5bvTcoUk%v?> z4Fis=ny#u_KBV&ApMq<{97N#SP$!sRUuKgBG-2YOVV(yG=drx|XfZ}Z=i0t5!UViV zr@>5!&MMtnkqKe_0fJs;@S6;%+P;VRV$HHmBhP;*9Y?{Pv^?G7>so$~1xeTFD|v%V15RwwobAc2{sn zf-EYJ-aSW;kdpHxX&Fo~)J&u&UHph)+v`JG1YbKSb`yY9)X%vDZR?(liEG2~A+42! zzcdQNBL;P}MQ+0DmT6IS1M>;@SqwwHfL_=S43gky<>Dp|^Mi8Wd70fiGjC@GtzW&%;8~Ko?Srj~1vH2u@vWCv> zkPoTcw~uPeAyD17cXA^rwyWud7ipVoGc1fiYdBUKP@x<0j;)Qk zG7l+)%Et=W_CDqQLkuA-ang{MBDkp4V98xmTSY#Wq@rD;@A(G%OW3A zxvyz&T$^IquA?g8Dk@w^t9KURL#mk|YI8K$ahxq(8-n*I=-LphG@yd64fP99D4qVA zJo({-VZCV_Ri;V%tg|Kzr1W2MqwWMkd(TSxz!RkR-{o# zR&kK>RVL!N1!&BpcgMJ>yj<1GouQ#qu)m+U1p2~)}Y@1x0?Lx)`*HK*1y|uYE+g9XLBQUUn z>hw&ywwKoJAD_eE|Jqj}T^s4z+VkOc_p!OQp~ugAU9``C^+7*d034&^+9cyklCDke zJ0Q3_cC*Uz)Qdt0Jn0->Hc+G?)4ukQ|^(jZ5t8#ObCnzf$F|B)E1A9Ym?NqVc+eBI<8f_NY_>&L4OH>T?B}0Bd%?C zR?v}`5V%;b4O@qXZBH`{1OlWPdQ@Xz7Q1~-OL6vo&v?$4;17hfjfQJ;4Db{M|54o$ z?xNK@OTo3NK(^_crDzIh66XPrzClR0hbyR26_Wr9tf2aQYMP3wle?Mx`U%H-v;+yb zu{_B9RZwN7l)z3@g$c&f*hBLo#n!d`t#bODJwF7Dr-k#UWS{%1UX1qBbdn)m0(-Q{ zXAx;&$jqBW0O5wzD=yev8}|CrYv(V(IB{(kFv!%!&ImN?)#b)(>kJ-MyhsH8K8sh$ zOsJYarFHH1>DtsQ-82-%RZYu<3FYnxxEIF0uF@cYr7Q>@P}nwD^AMRfhSH`6J@A@Z z%`$L%fe==$5NsJ_9bF&wc`}bUL}LS)8x!H!cnsm#K)@CzK+M4{CJy~v1ZZPX7=uTR zFeP%9io~YuUUWVWd2(IG$`Tn*r_b!Y7tO^DqvrXF;BM_!9ooNv;l6!80k#c?ZO4AM zZu=}kr$RsUBQJwscZWJe#|eQ8Lg3Q4Htf5}1x=M+x=LJIFR+r)H-bQWy}DGkZJQW* zBSTBw=m~-C`69TsE<{>S&FjxBwhj&3-q3Ypz}KKsA=o30&5dE(>xpYqt_;Y50OXlr z>xR9-wJC5I*l%0cEePe-(zU4uq)d>;Us%BuqG_5b->6ovZYICo#Na|YZ5@-@z4cOH zfo{EcisMC!&9xQw1brpb$yVJDiy+Mzn*u-fqRXh@&uTWVqS|~fopRx9+Pm}Uy2WC& zKTe(=av1SeCpzyoJ`*^3{n}g`&jDHnzO}$cg02ym$!W;zkH-Kwt_4 zmf?sq#I-eZuhSlJZIfD!)=oT1T-!0ziMY07P@*!Iia>SW8fyQsb*|03>zd1#?D&p7 z;@VC^l(@E&;G)78hCs8v5Z6{Y{9V`-pzfZC0C8AkM6I|PD@&GwB zakf9hJP#7iV}|3cIdmV=uZu8&%%js_CVWV%cx*+;we<%GHkLuqPKe0%JvR-(&d2#OeZztkfmVU4O10l!k2sJ7A!S{71<)vOvKNNgkgkY)uZ7JFG1J_ZoIE%PD~ z(f=OKW!KGT$a~}ap;r{H3qA74_Yy(Lfum``4At@61Q&PBy!d%O^CBL?KWkg1WyXDf zl|tSGwpuPBxY^Q6^NjKPhN{~N1SdxO&Vbwq8P1Rc6S|poHVgi>3KBnH-B!WPJft-+ zHQJ}{lo+2{>u~vHHQI-Z&>2FYHv}$?Yuj00B$3^*C%Il>-|Zy3*L!truI;0j&I4Ah z`pc9*hH&%9%Acx4V(;v`m&R}h6&GeVt1M5wNMxG(0J(yW=bPdA3TPc(Y_WYlvF&NLiW(Xo z8&qf?w%aFa6S285Y+SK z2;TnkG9YL|Al`m%hi|LXB(UPWB3VJEQt?s%@~a>fX-SLJqFYRv1dQvd#c2Pt80~v` zYm7C&Olt)mw1v-2)dKCV9Fl$mFh9sHb;V0~cQJ z(zv#r^+j_0ckD^7SJ-zu$?o-D9h+-Al0xgfUvvRN00V($eZk?{Fxqr(8{2s$&wYKm z-nbCBSX~=--cW3Nnqi_+4cOBX+BfLuVQg*;+g^WMo2r{OsMkzLh9GhvlpEwmP;?iP zBDj{KskYVT+Ehz*t8(3^iLfHCj%%)O7*;pQP-9|a?`D!sK7aYg#xa=>U%3Ae1(1U@ zi9%3X&C?)A3awVw%_>YV!Pud(Z8$Nuo4(uyvoH>l+)JNX3Vmm6mCVA&5Ijn+ft>K8 z)eL^8=TSK4kWW)^ZUwov3}Rx&N$U31O|Wracu(_gdUr)y@qYwqffr_t(VNb zl!Hq7Q_AN-^32|;mZPx=6YtLK*8BOIZO*_#hHbAWu1&czK+t9y zx})d@xQbSDNN{bM3tvb@QBBv};@T`#-d$6*EL5c@GIruH=2XssB(&2^GAZ41OJ#f$i0}S%LNCj>Es5SRAJ~` z+oq1Hg6pI%Qei^i%_30Ux87_Aw>q}<954OL&6aukcp?JCwVepqR`Kok)UDI>kq{U? z0^7XE=sQFm*g#-=4cod$ZQKxwfJiu5G9-A5zs;RCz_3u!1Q>Bd+c66VwXSKM!}na9gF{}B|30BGpEM78sD{FsOXd4jsNL~DgW&Uc002Z zQx}Wl7-Q$Qs7j2-&V$vfXMwfHe>h%r|L3#pYt>H($n?YsPMx(#Mk}8Ddu%wD9ZR{Fqz+(=H*RUF>6V?@tMA_h8W6X;)0H;M~dfmcx?% z47$_%g2^SYkH@08jvxko!a`0X0?fYlc7yj)i@|zFcv@70F`gQd(W2n^O{)wX(Vwpt zF#nFDEz!o;1a&d~?6&O^UaN8p<7@8i>&A=o+4p5QaOn=;znI-VG@A$i{QiBPC2AUZ zIq{}+<@^p77P}qnYqNQ$y|Tp|T>Hf$LxoVZdk0fUYA*PsmjN>h_jS!c;6L2K z`+P?~)`aT<&+*MTesg7{Nf^f9L3v-}Nw7$hmVv>wf7-f^G3resG(tE;0Xizp&Q?d(N}x5^H@5{I_4&_r;!97QG?qf5umoxQ{@kZ^y6WT-!9o<9^r&yt74KS z^Y?e8pUGj+-TvO5Y4~h{er4&$w+ycdhtvG-zPq#a9rsLgI;h` zVnP>e(v#hm`IJFh2CRDK3f8Jo{q-du&yzn|GWWpGH|qplf6kzjMeS}Df@?$W86GNN z&>0%?9?jsy1>5%P65Kfs1H6Yuz#a+mGz=u=Fh*6250j$7HYcQNsK@7_c>30j#B|k|2Wp>D9UW z4VqxS-*SS{n2(#6v^BbdLlyhnbTD74-kur1238fgBy}8n-*Wf;`yYb~p3F(Eh+)v% zB;)N~g1MA^U9006wC(BC=FebV-lVUelNogOSy2xGwC%`x;k(qS z2>mps80oPdT$ZU9`3d7T{g=Rto#1cDOu;&gv(ap>7xv()5Ke`v@_+7?eebom{5W>t z_)Lqk)0(xoAM+dQHi35@vyq>HIo2IC{RIQuJUPg$L_aTIJC|xi`|c<@>+oY-YM1c0 zz6HyExK~$@$DmU^c50P?vz4lSjWNz`J|?XN;F48i9G@RCXd4xS=c!=-wcb1V((oJ# zY`f(KRy?p(Y!dyEcydB>1^D39Jcj)Z20h#KsQV&tu}=J>VXW`69j_W^flq9nawGK$ zo)7z8H%?$I6@FDYYQS#T@qznnCD{1+orLMw7d5K2Odo*R)8v zXxA6}(+z20{jZPH20R&bUP@ZSP4I$N4jUIdrz#yHZkz+N+r|EFHnaB$avvf08FC*Y z_o@H2j}iMExet>2}1yqYxC6vFxoqdP5sVV-tZoN%84X7?3)Kd_lC_EqvB zgU)*WOS%&CD45C6vrlEvS}Gh9N#Jnz)6GBe%qZvh?Bfkyu|U1&D)!xx_@q2(Fi*P6 znQrXkQ!Fwxx>@~~R?%07anF)_qQRPXWdCDWU~6;!$Nn2Sz!#UoFZ;i2>R2DcurmMu zS?>$~JC4-rq;FF5|2cQg(t!Xqu%uL~xH0VUt+~>St_8BReAw&kTCi|($d(6S>04sq zTIXR`onO244$i+}@x&+z?6x?k;36eU`>nNJDe7ZNd+G_^I8wEuH5`iODS2p->YOrC3w!Q zcFj0^{`Y9}ij5ac>2$q&o~-NLI8`_D87#S@x^dSWS^9L)g{-X?P3gSIV`I}KW$FD> zjm2()Kef+^NuMc87pDiDIOJza|6M;f>g6<9`s~8SVR7`$ixZO;T0*k)s2T6oFfh9> z*?p(adq?9l>-^cD9vb2JdGQ{fC{bD3d|Tsi6|VP?rzPh!zE3|qcvl2ieD!5sWwifk zsQpz%uw3Js`jhC7>nlcnRHHrK?m;8c=&$^UIU{ahd8bSh6Ioe$eXRLU9&qXL#gkfc zvb1Pvs>=@CpZE*6#Afiv<5?@dp+8ogGXKx`D4$+q6bYWHe~Q;|p)B1zr_o3o%w^!_ zDkm>X?{V(P?nHlIms_M+2VI9DFVF^W^GMw5N1+H_NWmD&?uzQbo}VZC_dM2kUNM zd4W+&(I1!IEN0mg%ha2TI6hMJU$dvh*TCGrW8(B1DSGo>`R;tMA=mVhnih&~xx9pb z1iZY<-go`B)Ix|bnfAA6uoLazik0{<{Y!u%ixM-!%X=UQ@W$& zZ=xO8VELe*Gx$oNQlB_je9(ef1}++Gjs1x0MeW;@I4{+do_|hH_X^l2mB;@mxbVyA zrSoup>+LbYWog(qM86j|fq$Ai+`kImqobi11NM=3O`VZ$N+0GlQcS?-|E|d0q6bbp zk))7`_E+uCv3CGJ%g_m*+D*|5c#`^3!H(Y_jX1PZwDjT*`#$h3g}0t+Z4{lEd9Z&G z;t&Su%406z*T$Wx7T|5A6`pB0|I|#y%U8fIhxZ0VcTjZE&NRi_;N(q@B3aiz%c1q{ z9+={E==e^9~Pe`8m?f7$(=qL)oGT2>D35Zd>j;~|?7lr)CrEcuiSSA{Kxjpw|iifw-+|=0mtqUDqM@} zr;jR^4q!fxYjdTW<9-{S+)26u{(R{tk1N`@`lcDb9@uF1A;XzqFFz(z930K{&ioPX z*P*)4uN3p$P=9?=<`6}Hv+GNo4^B?AKj{Z9sXUNai}iEl&;v~qoUd|6Twe!#^znv= z3&7E{|M+o(s>v3S7Ic1^U7E+dF%(N77jM1U|qfGvj^+3Ke->(699icz2{`sdx}o(yT$h% z`%zg^c)WQvMdymeSpNdgemW%9SwYd)4#r*_0$WcW+hbcs(Z6Pu={A8+J@a^Zzl5T< z>Ri8f53FW0v*{T2zX-L1C9}Y_+f(e$Vt;I%wNtMN`=_3JT;bst6umvXKRFYu{-m-a z_8CQg%3Eb)1rC2^m+zKG(f4P)?H<8?uW6uR>GzbP6YWhC%fY_a-Sy^y*E(%r7J}C_ zj_8_$g>`k95#T_HF~xrD--)w06nw!mZl_uLV!vlUi^%5<9z()#p-NcKtu;A0J_lGf z`A<0w^$1fsMR1|@S*&IDT4g=ABv}80Kk_m+-{`ji-#ceVyU*Ywica_` zm$V!#>>|M{nN88N&vo!~fxnF|yB+wDqBYB+lR6Tx531JITRouY5IqzAB5;9Tqe)i= zMZ2CFvk3#ANdGB0nNHC^cHhv~0zd8=sv8Ao_cbS$$77$HFq>Ties*hSk3X328kg}E z@P?&X`a8e_o44x*rcw092N|}{;!Np5&JfexV1>q;J;%W7V_PS#rea@T?W(^7d||db zV|NP1mG=!(0=)0d9J|BG6y2Te{iZV(?GdMLNhD#OD!sD34&Hd`m&?FCiq;WIwOs=) zKWVRe^e#nceAMeLw6dfR7)m@G_)*Agg zo}5Q1x})cC;zJgvY}Yh9LeW8YcKcli=RHb~cxp}28V2k6{lG`InT=Liz%E!R+jRu| zEHHmi&kWD;p9y?Bz!&6}iMi~f=$A#)x;BD+xATuF?uH$6qgq}QywY`>TaO{e`v+@ig$h1fDS8h7>AQB|!+O@1e%r8*B<|{Do$s}#k;iQlMR)N#c5Vjql-bAV>QZ#* zqL;7Dz;g!Dz3kS*Zn{vpF%Ue@W4c$g21Oq)N_)*}|Kb4gJ=N;?yjbsoIB<(3m)G%? zm@i!7|6xnjt?!hVW1p=(_2n*(S5My`oTW_BwNvCf9)r(pavO49M$uodQkL(*uC8Yz zHz-mx|DTJmTEMauYA02fQgr>`+5hm)c{YoR7gO}m{EoOP9Czw}q4H=E?5t;t|8uu7DEi#VlzCUcy9^myZ_mQ`<@Xf$ zfM0$%WZFIx>!Bu&>lj!)`037{Z zzk*gTgEqMuDdP$D`*W0Lwm>#Z6X?Ve)=U3?ctwf$<=lhU-5swky zl*=^D1pj>WE=T}zugnI$yj5Utsb9x7;CuzYvcF;2TkHCsTK>lIJ*yY|Jqk8p=EjC0 zp7J%d)pQ$}>(i$)JzS5y7w0eX-nbx(LFf1{ioS;9!7c%g+3>SfYJ5#v!R8A)&i#g8 zEpBpV&<5Yf-pl)!_qck3Gi$P9LlK|KuJax%0RM3k;KD{l|dI4JI4%w2d+I$^}&2HU!XDh9lTa0(_rfloFBY_ zA&mWLs6u+j+24puEEImB2-eo9QZeJi{`7XdW&%kgWH$_|T z5Ad=F2fq4SpfrV|r`^(*JqgzA`pDzTkMkexJLd{+Qxx-66{cw2WCIIFFm-cJM5qY% z>!sg0kAa28gtpqw!G7v0t9=5T86-}1U_It+7X9oDR$D5)W&1q%T}KudI)G>TuUB}6 z^?NFKULmVJ?}IjmJ1(GTS>yWotmDH`j#r+{r*OY>C6mF)1;-3yWhpv5c2N5}m}Bba z_1-vtb#j6BF!-3=*7@(SU&I($y&41`JpDs%0rsEj$dC=6!9VD`sovPHRIe|7-3k^G zDRkvqP0__m@)Lf6+hqeC9%28JD(dSP13UA#yFJGK_%`Wz$0+!~3T>+y8*sn->Ujn5 zJYoJYYS<5Ue`tSqDmZY4?!iquxPQB2Q!;V>Ow(GsG3>VnPdM(q0M~2FwN_(4HWcK! z*aKEvclyZrE%0l-$8~t{`8yH^i+^B05C6$iECQ}=>e-!PK+$VnMC!7*VsvuJ>75kq zxMHG{58RYfS7*PMq7_RXUu?qlT0(^9EZU%@|l9M$#BDZ1xlvix7LmTmQ| zm_x9m1N3cH;`4U;UwjT&V*k6(rEnH(a8Q=xJy_lEy?5 zDjnd{cSMrpz(J`F6_!>M%@A&5>VpGLm(DB0^KR8>V2>S`uiQ8BtSv>`TVK}q10U82 znKQ+nqAkZeZ6d(}Ll0AL<9W+Dk)UuNJdaO0Sn3q^=fMX(@4%ZyI2oVtybj+I@P-HW zfnVlKuRZP*{oW(6XE%65_6nwv7e!muDkNVA_pDsr_S>7HMIL`=z66IdRBxGHfL)&A z+cOpRN7C=-lg0tKK6keNUU0?V<6HHD;fDsE&@Tj+kEp2fg;4Y(ZcqPj;2p1@cZ7xE z`I~XS#}xL?HuWzWmUj3EdPP(8w!j+#N5G-$4pwx+J{p!UOiBSOe7xNy5BsTh({+VO z@UZQjEpNeMSr-*n!2Wxhb>Um~9gOEzPTN56*3?v$SFse`wkWNq87#o*B9;;dyZ>fN z(h}I04u9ep@8c;tWnCA)57?gTinYaET<`9f-aGKov*yfQi4-lb^^1Qt?Ai58!d@D{ zUKCrIEMNz2jJ5T8luFTpRShNK;5R4FCY^*m+WyR2fw2-L%;2L7Dc;k=J3x1Z=3qv^#$y2yNiGH`oOD>b!o1Hy)Kn&o;V+V zkW$>k$vyBBteaA z{u;2FNTb2LVu}vlzb}ywepyOZ^!nRxD4NSvPfrtU`0TdANICo>ll0>4;IE@)IT>#$ z+W%I6rvdoos=3v??wT~!g_vrs@o78q)}I()r`1=%QL>a;5SXFd&=Pl?r(Co=>R`% zPR?1|hWYSp7ynfFrRwh7owlFhAL!fiuK^E;2Y4;`g1D0PF26HiK7WhJL*T|^PcHrh z2k0!dTa5G1_kQ}h=eyZkE(N5G|Sopoo>-Y3#K zdhdcA&b~L@@d5Vv3B%rnEWbWQh;sq%_gByFWOH!3cIMSZ?=fHcn+0RQI!h+$@EYvz z+@F$5z){=om{e53K67sE9Rs%{C&`RfV0_-*y2Og(+`jDVV}bF=`E4t78m!vkZo!Jf z^`Dv{914Ek(x`U&B}Gr2qh5X={EX{jr#}2rU+x9=tT;=;3y%hs=ZN#v7xcA)b51Ym zrZN6(U(i(Ral0n^gUw4e?;JhTPSKxdSZ)7-^Dk#yd@zpwc%}4k+W^@5;9u89-2a1t z`IRj8_*88fhVhJ(kT}*0K6B{TY(eyg+s@v=MsVkG{q_&FugtgobW^%ssWa;@9Mio0BuT@!N{*E8jEyp#q073w4Ls)3@p zryLOq0duOhY>vSA?Z!9%_-iuPo@>{|hT#;>g&2JYcvrYr*YPUkCs4c_OazC{k4pLIRwD|qK- z`7Ki515VINOQE zT>h;2rv8{Cej|>nOdjbj!F){hU$|(vm!hc$Jxtboo#Lro>2lb@uf3+nH=C#u?6|f_l^ly>tF7u3SqyP)tlQ@2A-?!6CaE? zONVRBy%cb~^Qd$H=I@G&rh2yE-Q3Z$=VHFzTr$$F1D=yHo^w8rqVL_^+Re)UtBG$@ z-GTkilp1Fq!v6QugtNTx5&SEI0seX5)s%g)JdP*xIoXV0KlK>gCKrX{Q}Yh$6@ya} zK6|~!{;4H-_9-h){^>z@zzOh$pJv_3V71uE-P^F=O4+vfd4ON0MSU&DejL_vxI`PQ z8)WZ$4V-eL*{=)x|LcSqTdso5i@6ojz+^lHAr?ruc)b3duQW*upQ40w|4!FGQ6los z+GB-eg7A;^N9O0^h}1 zYe7EnjfQMGm`mx8jA}cB_IP_pwiX;|Q);OMzwE<)t8=XQT1NYaoHt$2w;cM$DT{GC zdd^Q=5`J81+JJ=(_?z^psBZXq`o8HeSkIlE=C?Mo{J!ru?yOscdGt>GzFq-%=THR4 z5pWle<%mu{{EBR^b3FOq}x+^A3LWo8dCXu0=+x6gsAx8#4_F$}-Z z_JVmm__k+%eIL=6|9ph`bmeS;72@JulGC)eDITTWIfJ5HP_bD4(pPqq4e|a zUkrNvw?3^TaEEDm;rvlt|I}@cE-+v4lVjh<81y}(S#5Gy*X?4Zl<7Ewej#CFVGs6p z+CKRUd|&NtK{z<^X=LRXSn_qlof2@%`@JbEC*g;kS{Jtg`-oS*SFZ+mo2cTU>tL4y zU1yRe7_>$1tGEx~WBr2d?f87nvdF`d*e8vJc&#JB;e{>Sn&58biz?5+VgrZg90qGP zpQ+l4>xJ+KA9ez3d|qMd4!`Hu$)FAP;FT*}CZBL2&eor?fOY=dM4!Pw+!Sr$e$2`p z9Jgv^$5z%Pr+$poW>fw6fMM+S^N!b_<6%(DKPuFLC)pr+S3l6$J}7@CsA`=O{3_8KSP8vz_$grsxO&}ePPC@BN5=pN4Mh+31D9o zJ7{AIp4<0bMvD)2-nEe8wct&ci>>xdp=du^zwQ#gJIyK zt|c2;@%hrK1?DpNe8HjQ1RpRp^}^3>@au+dpU`mvYnDl!Y(pHR?%?6OTftv)!+$ml zQ*@+Iez7{(LUHN*^Jw3(N=e;WU_*av^B}O)){9f>FrK=C8pgvoUvi~WQ5M+$TilN2 zxL%Wx@J3(oncfhqbGV;-`4Mt7nA3~jd=J{Q*n4AP5ayFZ|2@w&Xm7A#Yk@5I(wp3T zU-a*UQ7z{y%ukJ@o7eYXJo=_yIA{U>D&{nQ1mmU0Ur-5JExfj4 z@k`7Ew1R$#{lKPjl=%?rH|W<*E;n$K_RDk`>W=it*qy`3kz^uih6gfY;!&GhLo+t7g#D`4DqIuu^aI^^1u6eEZH=$Fj>sk^&-!5x1CX-Em+ym~-ms zg6+`7?6vw4BM*MK!LM)#aS)%Gub;_+d!KI3anE4TGrvx{Ph#&5nVS*x6*?fXBY&HI zfIZ&76aN{*pacGT=eK~ZC{=6{)i#by%cMi_~#(6FM(a z2ezEhiIF-oQfEf$&~6htHTHP2bxv$%>x<+F{gED_Pa^e8T!g-f)Zge4`W$XTzeDPK zt`hnmQXfR>he-PcCJiLSTolU=Vr!Au+M`af5M9nP#{d5;Vc9`6{;~eZ>##q|)-jPf zC$pATk zMDSLw^58A$47xyIpL7*CLdn>0F4jHA>=vmpFmGQNuN(OI+)GyCtbMY_z-tnpZ{p#~ zR{~$tU3N4H@!T@GcY-W`_}r3@=NIDqZSg6+j^NXmRo~4;yp>ZuSmP2{ufJNM1YG{G z=0+a)l-sZFbI@Tae=@oB13WcNEWZbA{iDTk5BBLn&OISK_`ZS?t8u?u_k4-j0&dOTDRmQkw_+l}6P&#C=&!lBpHP`D|8HQ0$B|;FRo_!9PHz4Hk&;Zu{xhY7k~bv)`s&1K5FskK7$@nJowjDaQs=xatp+> z)BGFj_kq{S&N+7ttZKn6D-B-1KmM;7cw5YMzxQa*%lq<|4!lBtul+MO47|v5Q+-@1 z`ZwBOpE8&|mwkCZJSoEcxi;>KKZfIUT^J(_obtQm+8=!W2uJC1X>fEzA@eSDvI5s% z-FuJzRQz>)_{(z!y-H*8#B%VD)=Mh2Xz#9u868a+ub73qgy-Xa(zf2@a{?zkFg)Od ze1mxQo1xj@$Zx(2b#ec}5g|%pm>=8w*4EeK_~kttHSEAG0?f2W;QWN!XZgVgo^;lh zg72#>|NDY9|D5JNZU?`8XWf(vCUv`{uD6cR{gS$1Qa4QMib>rusY@o;KemMFY~3Kc zZEOv-;H3`BJFwx|W6#=0*t$W|zhQrtt*L7m0dpG+qtfVqq6o z4nDs;E?Nuo!04!21@#g32UVvL3+{~F^wJsjjF6_t>N_&X-t{xBTD$@;cX(DV9I7A?7=O+#vL{t;sL$=EZOJKXH93NKVe4Pl+b!uRB<=Zd4zA@-S zoCjh);(l+B>VJHI_W0&>N}mNE_*u%#?1ug4B4D-FB?j%Up(+ zU_TZaewbjbi?vfl(xY*|b_Wv7{L%mIUD{W~nz0X5pJ}PZ_*v&AOrAu4ivH2pG6MI_ z*(QCtjzQlTtMq8YzMs8-ArM#to%F`P2abcu`Glz^f`2xw=i6#p`H{tP_)hv~(=VfM z`scfw*sS}gyb>he;s3=yM47$a1%b8@5tY_5XV>NNmj9RQ7-Ufr0hdRg{(D>)OVsm~pl&(U2%eQYk>wI%h!FYLf=+=hE-{ykl@BgtXT#0zm z@!xN=!R)rO$DM2sn#w9!SjW1r>T4;})-9!I{olsRhVfaB+J$SKRR7=mVx9M&KjPT6 zmE3zwkQZlvwpjVT>r&*w7l)P%{KU1`*Jihayf&^m6DGty*7e!85nIE_)_Af%%PK4@ zVRbV8F5$m96+djc5hG)tA8<0nqzkNTrDdda9$K9lVJ1&#tlfVNlwl%N8TZ5aO`@u6z*BQizJ|hQ;8X zH`W!J9}!~@|C=cn0AJ)i=d)16Xdm_LRsIN;u)gBF{vBebbE?`#z$!=2|Jhj$tzu-D z2z3e1n8iFv5{PL>Ec~%d3H<$MHcwC`a%R%w4(q@bMvqNymmx-Xvt_yiScIo1qV^SH zgi@gyZ^1&LFB`fGkfYQ2v1|-%c4LN~{R_mzpXh8C4MhLMXM0K^rq@!pAw3zaUuWqg zm4W_$*_{3dtlQ))E|fyiu}%*Z=LMP4d)-@IXC@-2L)}+g1+L)pN>YtS>>!LUZ7+D! zMNRR~G4Q3QS8NFaU#?aO-w=)d=zneh68tWjv6Yq6azDIOnDa7XL2jig16Pp)OkmVW-KT?Bq6Y}b^q5wVf5?6?^r z$mxF;9okJJwv(lJ-WjaA@?m0%xMZvUKJ2rgZA{QW^y=zua2G8HIhY)cu4ckDX@rQh=sT`#xLmd zy0hTgnOu42=0MA;5Kgg<8-A{f3z9%i=k&ANYH06en<15PXma%We=ru}ergUYhdUv5 zc6X=$?GHFUJK=3L$26>;qYG}QgEOL?U!E6)#_ZfzPfIZWZz=5<#FDZ{9+}C2-+vu% z&_`^q{rLXLd-y&-x9arLNyJY2(mhS`Usf>8_jYg&Nt!m{W@23++S7U)>NJ8(Mr_1k8SZwrB)0xcr{A({&BFmf3Fl z8ttETP;K9JaQyLLgBIMcn62UJVXzkG@Qdb4u$8-wKP`pEDaouy$Nw^e)~l9y@*T&= z9`5zgxyqo=o^E~;3vQa0EcOune^R_IY8lw1KGrnuHiPcH*zVeb_EeoeqiP=qTRb%X zQ#Y%<)(fK;7|)Dsr-o@b-?5-eQ{yg!Zv5-gumybgmbu_Cc`HP@sjo78OoFHJY0`6e~tq-LDdl#`lsQj<<<)=5n}sd*z&XKisWbGYUn@85}k+pqf?H?JBVYBmU&R@mQY!`(x#SECT^a8&P z>CeH{oqG+X8L~8w^P|flU{&6PVmH)O7nkNMo&vA>IN`W&7HS38n{Q{;aCpvAxurZE zHOn_&R82s${^Q-n@Rg{6zI5b!dOg^xDJuLuY7K82e@=f2-uY#R$q^1&`na2@h&8zH z*OcjcW6;ED-4+>zJvYH2c}d_8{N3Hxl~|hkq&2#Rn!liRI6m?19Qe@Guziyedplvkk25ZoG&H@$HH{_)w5=Y7Cd1+PT45O2D;T%Uz@Z+)0=L>IdiV26Mb`D5QT7!JO(QKtrEj3Alw2X~2e#^eX3z5)H9DnJ$}Pb& zBO;g+&nbEb_sPwg;7?_b47u{*kMHtctN^}jJiVm(3C1I*T~HEy+Ox@WEEigh{Dwv1 z-~-1FT5U$WCr_|Q@DJ?uC0da(-49U9DBRxr7;N}jK>8IlxJY(wasr=~w(#OZ{AXHk zkpEI}ap%fqQTL!p&butY4_;reFHSKIpHB%B5Ci{^j$F@;_)oK%w}K+LW#?5di5nFC zLFK!R5?CT!%Xc5*Kea;FljealH$Jxfjrh;Vy)Px4;6%Q0?)RaHzkHbC-voaoXxZ@k zWmgd2S;%dB4b0s!+f@HD;thk%dON_E&7}`Y1YrKG`(Bb5Zc4lR^Z2?VUi3(Sw)p|( zS!`x~%!i_v|2Fpf2v(92X>9jK{Hsi~=P~%pn3-S);z>VNOcjU*t2~cAxy=Ldytzm8 zJiw=?Y#&+d22EXRW1>0uPyAML!*xZ#a~e`7XV_tW^TzT0LVTjt<6ac+5yY=X0*h#@U_pLPQB z3&FXEkB#Yqr5z6yF~FhQH7rICLDQ%j)Abkei)QgRt<~Vj^ZB~@;NL47eMeZw(>tfI z;xjecmg|))p{bv@kWT^3*D9L-3g6eRDWLlW@rWkZC6OP{KBK?Vd`aMw-)HV;-QU)f z`MTC%o;z`|Gtpn|g<-`q;Lx&yd0Ob-s^{u{KM}83C%JNtJH}(W`!Abh@ZNmg04`7T zXJ@Ld7g)Ogd*XJCe}VB$0Uhwk_i|efV16)vIhW1_cZH0n4h2#4)3dZ|FDf-1dZjlpUt)i!%K9y)s z-S4-fh|hlNFv;JB{UnDu^GhDMTc#&32m6tIh|$nVFhg-)=lL&)uR4u~cq9J%z+po5 zB=*Oji2@HC5pOP=F<21y8}ZolMIjT2XD6OGHoAV4HUC#PH!O~#cD>}3!+Q4QpX)W3l9mZ#WygG*#|Tt+Znhhje^Z%x7TCiZ4o z4#unFv(=_VaG2_;R8frAscU6NhQNp7FD>%}H!V;-BA<%**s6m`OIhQ0K5k1gSXXB#jZCt# zmrKyT0Qb?LUL1cn{5@bJ?#H%CFpc%@hoL)dUN6CN`LBgp?}q4&DhObm@9S@{iS_+8 zTmv%Pn4hZGFZlC=U;Q#nvPS>;7EYGrqy2?N@`B$nzs0K>n08=KPmzE$@S)!?Y%cATF#;{<&mQGFH9NpIr?2{-04Mp}cbozH=hCBafjQt$ z=ZcQp$M}5_yry4(@i}(2KY;b_fT>fn1VX_dYqfZ<525BXecXmsQ&KDb+_wkziiBuO z_ge6d9mWTFF<(rs>?m0P&h}~HX+{2Ocm|jL46vV$RB>uI?15Sxe%8Cq;tTJN`oo^- z+wi^Q19+ibRNi;kGd<;pna9D>>cX1(&Dc*DIVY-u69UFAM>aq!dhyOhVesYY_oq46 z<9TOhV)G31Id^i4oet)Ie&=#O8?cPv@-Mt#ciF=J1K|0lsza>xbK;V)|5vP!jqMWU zS>@Odw2YbC!G8i0CB3o!q$KJSd$8VA-^`yqqXhT+JwaeUI9jjvKio3+)|;+)tex&uQ%oQ|CPIB=Nj3$3|88*k(Ic_ADKrX^DJZ@hRoBDc^op& zL*{|VJQ0~kBJ)gS9*WFUk$EgK&qe0J$UGUDMeZmdw+Vd0aBjOXh*eJTaL^CiBc>9-7QklX+}1 z&rRmR$viokM`OKF9fM+=oIOpbNNpZhN&1SXJIQ zvm2b>vC**O6W(1AwbseR7j~5WmH1XXr_DUWs|a?*uF8s2a0)({xJN2yC!=yWaC^yo*C_Q^*^zW$}XY zM(lgFH93v~ez4oC{mcDOt0S46lW`CneQjEaD)zBk@k8me{E^3&7|&~mAGctmRu@CM(z z$eRvHRi=SY>(`lP$KqWH7N=5wgZnhQ+f;7Bj#k;ebVDGtgX{UnVk6<#7RK101HW-> z`+D>$_W9S6!Y{$|-UC7yTI?2P8eFdQ}p2eKm0CW?A9S7Qb^w9y?&8 z%^UErdHjQw4)D`Uv-w8BPA~1$y^dghm_~FlFT?K8Sf}P=!K(dqzH1KlQR)6VYK)qN zPz~Q(;2GNM%I_Jm{7AV4Przwya~x+G;yK7|?b8Lml8~Yzxe2wa(k-85VCSd&*qD1> z7dn>mzWHk4WoI%pXKg^ih5wgTVsFj!@{4!V*yr9um^S_0!C z|60t4;Q(((u=lB83s-fDKCB%&kEJ7+Y!$d*wF2X7R$q_@-d1<9aDxio1;f#2@fqx) zK1GIa8SekbFefkkmZ1CrBd^8qTSBxrSAwf9ten1bA!?^yX`G7zpPHC*_1ApVYStN_ z%LH2%Mx{NP2fH-JobwmByMOi9^$hrhvyEgg!>{_V%XVnjT)a!<&Xd+-;9qHzgWlq( zH4J)Wz6PA<)(}k1#P~lL(h>vf+t@I^OoNW2`0#wbxqCw;NDjMT(Kz>tzF<0;|{K}GE{iLjrHSrzu6U>{<-^WHU~bhlf2d(YUN9@enj~tM=Wj1?z-WjCevb7k%zz{&TR~ajOds(5M+Fy5#$T zT~zf1vyn%ialbWM6xW+S(%MTQkDh+G(DVzAOD+5$UklCP0$r7}Uf{nJZ+le*YA76} zG-5FR8@`Ih-hj4LOE2~Y4bGTCFIrxLcbSH7i{=3DUDjh@jXZn6ufVYnn17Ef+pcFp zdpc9U-Zc*#OC1os`GP?|i*;@A0P8P4#KC~JbkVx1)my+jG*$-h!n-R1Hn0CA1>VW& z)<2MgcjKCvtmX#~R7%M>fcMV&^JD_+>FH)EpBZ?!#czG7sKq$GPHIdz724#+%-BC2 zIKE0k*fQikw5Y*n_PT&C%$pUr4|&*zqhYhTS;vKZK2ad0lX!k|~y{gjXZD?>7LItaQ0ivto5u%DS8 z9~WJZymNuW#cO-OMNWSn&BHsmYuCPTX~llH?Vez6NDT|AA|Thb!2$w&P_K?r+3tikAnTCuz;YAaw<#?ts)Kkh%p@*FfqXNL>V}n;>-+r0#;$ zWstfJQrAK1K1f{%sT(16C8X|z)TNNR6;jti>Rw1)45^zTbv2~!hScSdx*by2L+XA= zT@a}oB6UTi?ugVSk-8;P*F@@`NL>`En<8~pr0$B;Ws$nA|E239bYG+{jMR;hx-wFC zM(WZ?-5RNDBmFhFaANTLBw2nRdoN+HWw!0eezvjvz8Cn%`XisKQmfg0wqGyM(k`NV|r#dq}&8w3|q~inO~(yNtBkNV|@-`$)Txv>QpglC(QX zyOgwBNxPP`dr7;Pw3|u0nzXw~yPUM!NxPo3`$@lm^czUOg7iB`zl8K#NWX^kdq}^C z^qbgggY8$5ei!MNO80Y6B&>eshuJjdr9)rn`tTZ2tSlzj2rH$Hwk<_h9NZxh7^ z--6>y>Xf4{&%u$6 zcM1k69jk<$6297{fgADm(k~(dXJOYwk4{83#vy+g;2ks*cF%`e-S{t2$WKkY4GaaB zSPXb*fo-xxg(JZ?rGDgC!_OI7r{9+VE>cJ+xDG#uH%YbcJ^1kGwuamO@N@L03ax=% z6(28@Tjm8nNNZ`SFPO*0I%$C`Y6@N-e^UUS`6S586MoA*fpEV6=%<4@)}KXw=XmP$ zi!!j|vQ1*^Par=sn@d*y4ES_WQ9*$j-qA3UurU=J)m1u}whMN0qjUTOICepXUBnjX zt@97?=)ew@3_QELNDuMl#lBw-v-t7+riB|RddklahupzlrH5u)z%MZ=G(N=AN7tOR z%yrSE=&Tj`+_~VtTtBz4Y8q0Pxg6{UTk_e*g{;Kqiw4d~!!F+7E2NnOyiluK|xVC1bT@3Fa;axMi3_Lg6&rTbD4Oh4M^Ec>^gAK7q zI(d*U36Szw2Hwb@Taf_0@1Kfq<3BMT#nTo^cl|(ZP+nLQ%P&1Bo8TsbTAk$lwzG;j z|AWtfWeaL+S~+X}4&%5>FOTYI7yN>cHv7}Sp2}xGxT2P5v%8W8H~6RA@*PW2J9DXF zb6zv%Pr$Qf)RHE=D}Qvp#uM<1Bf0zbH{u=t>rXaZ0l!WERjviT^5)E%yz5{;B z{1rvB9$`I8eG1o;fZlYU^^JOMaL~-Y*pNF6`rPEm?x)xfRA?I;P3ToQiYIq#fwksu zj(ik@-ws$BdFmtfkJUCC>VJcKq68w1!Dr{alVITU%PjBCF2erfX;nAe6pMFF#DqOv z39cSJl=BeZS7CGYMilldt_No))$}yoP~_TkZ~F^jzh+I$T$!gCnDoWWSohNLy>VRGLA*YxyU#e z87Cv{eYgPPhG@;Bqi-;^VNvrUrtO*is4 z-y9G19~XnJ-*V(sgDG@Y8S0eGMbt$!3;x|V2;C8>n1*JZGoJ-!%v3A|e>g5_Td z1`=Goz^WIk7MFDKu&B|6ny}W%w8e4 z0KZ`}&3UzI99Ztk8o5H~^*H^aj%R>1Pp)ek6_cfd$^&Xx%)Q;tYiI`a5&iKhm%ygs zZS~$$W$D&~LOV8qBMV=zr|>%)&WAgyCBTc^>wN^F=le1y^L+&Rsp|PW7E$=^fMwQA zGg`s6SN$DNAs-)7sgs$_;{1e%Q;?6ptM_pz7`#_!`J_Jb=Z5@BA?v{-b_+~YknfHS zHy>hv`2EGN#^RKg znbRFV!vFXkmsSJzdG?_E_dCS#>sr#v!0(59*93vXo?R_uar=w+TMofLTA0S6SPu46 z&B^&xi5lUc*~i|1t=?9tHJ3r3Xy)2~7o2{p!bPtH`W+9Ssm|a{GtXvO!M|y0=?&Zn z7RroK%zc44|M84+IdGWA6}4-~1JS&OTV{gm65qT$@dSB@!^@V;0RMcmH@Fpfpu_=> z`%A#jCpXI7%R>LkUN18Q=Z1&}gk{2i87SX$1pNC0SH5r>^dS26rN_aOD*W27lcDcw zm`FYb4!T~=ymk+I3M;W*1F+VOUR~h?_(yv`1t^0z4nDUCjlnxFy_Ax>pg%J>c7Vqr z8vf5yrP7CBrbOERN7jFU_4tPW|9GLDvT0CRm6R40tygGiOIAf88d@q!R1z(P(4f-N zpfpgC(9n=nC?k?eLsUk{==Z$e|4+x~^FO~G2gmU^?$@|q_kG>hb)D;Z1{hpK-0g+w zKpgn;->NBbu&1Qj6vF+%Zr8WyMaRKji@WD|9=v<4#{%E;$RE_u9XJDyi%@i#bq;YV zuPEsNu)?7@Hp>{izvThXR`9E<%^JUu52$@scVHQ~>fh_u0dVmr4X1y|PkXPC&S3+d z8Zk5H5jgixUi{iK$mh{j;!Ou#Ma)SqXdZ`tD$97jWjOFLq8R5l2ahk+uRm1wBX#34#40n%l1cRIZX z_eG&e{~|bCM=wDM=i@H&v-TeN;ZONwew?qa?v?Bo@XnL-D$-9d`A~^GL*QHH=eMhe zqTX}fVaLh1zEO#NX_ol?oH9eT{NN+c17DBg`ULd4h6#eLHP4>g8wq^uLc9gw1=Tn2uEhQO>z!QJ1b)YREz1P=ceiCn?PsuwzV!Q-7#~`*&N^~p ze2xSb>TSgMTEU}}GY_mB^Lx7}#-qr?RpD~rO-s8HZ(zKd@LJ@Uf*bs~0*sSzy_Mw* zkAbh=PTiw;8ToQXN9(e|e4FKS&!=KMd+Irrfz_-mc(bp-o=&u^e+ZUwNn86j8{_e$ z8ZR>+Xuk^1o(}(mMgFJyYVgvMQ~~~c%opFQa*DvCVa-DpMR;C4U)86B&qz9bKXV7Y zF9sYR#e-#k-cNW8|Az7vwdDn1|G{swx-lPQuDCsT2Yj?i->4e%h4QLLkxc$7@19Pc z_8R2V-0WCB1M_QnxU7>r0rg+$y!?oWa8K zpR~_lJ}bMvU)~Np{rryNh0k$3bBWwm@Mpe-KOA2m-&yh3Co8aI#6jb4ov6=s3cS7@ zyl}Z!TgV64~AbpH1rMgUu3-d0dRQV&yr7AKYqoh44(ymY={&5^%MSw zqZPxI;O{oul}`V`?~_a$o{#k`*k#mh-#@HR^DYeQf?1nS1@tia>CM&(yTDcTgFAVU zFF0wF$H)=z>F(u4V_5H|mW_`jf!A?GpPPyG@PNpAg%4nd>)Eom!HT(jUpTN{Z}+eD zt_9zyQZQcu4*X)(X^UPw38#Ne7XyD=ll58*>uYM^5``_`r2(v+df?9mo16W?PYdRK zUjhEor7}_o&Z`wpeu4Lo@3I+r1Ln3a++2?JyJ?=ac_Y~4?u*iKtoI3H0%!BVLRwcA zH=&1^iO{2hNU%!Z+68OT^X%U3x{(BM+9b~GesF5?!|A2q{ROMiCW%R~eog8xcmY1= zHoVvb`-@L8h6SI&1&{ru?=F&H1-tzyn1Q_v-^-$%cIfGrHuCSxe6alN*nuQT~U)G9FhCJp94`~?~g_Y9!-ShIe- zJX2@z+BQI71n1EmpWMXsEq=+t<#HasBk|jxC(pr44W%FVqK?5Hx(AJ5XvYdJnj^vb zwo|kz2YilYA3uMZ1WQ&wJl_eNCGw#&k6(g?#wtw)U>*0}$qu;gzr}*hRKU^aPgmq{ zq8FQor@S2a_stb|pJH5`3r?K21Z$1D& zWpgX`Mae6|&)9c-Aa1f47Z-0C^!1eRz_MlFq>tjU| z(_4XC-Baf*de3c+Q}=iVUQ+quV*$p^%OMBz=ioDCk#;|Ey>!kL=J$fPS3Tg^hU+;Le?IH2Q%jw3qG z=s2X~l#XLM&gpYNpA-5V(dUdlhx9q6&oO<@={!K^2|AC^d4|qIbe^K~7@go{HK z={|t&6X-sI?lb5cj*j0~TD##gwyVO=&PsYA3UgEwSql0l(y>jO*?9&3HUER#U`+1KDZNk3mhkSZa z7X1Fp=bieGSI+(s%rNYGKSk`rO83c}W^{8DoJ~ zcY|L)GU!ElF!oI=b`2@)f?s}StdL1BzjeY7A9=Py^&$A8OO5wIu-ubvtt;{RqVW3Ja^ROLb| zp0~p<|7XUp#AftXjq=;ODiqvlKl$)uAqm#L(>>31!5@9iDvPn+x#_y!%7x!Oe`M{VzT39U1sbttq>h1RjqIu}|8 zL+fN{9SyCsp>;U4PKVa<&^jMl2Sn?HXdMx)Gop1!v`&fEG0{3FS_ehzq-Y%#t+S$a zShP-y)^X7~FIopi>%?ds8Lcy;b!fCsjn=WzIyYJeN9*Kh9o@uUdZNyb*5T1QJzB>{ z>-=aPAgvRmb%eCekk%p6Iz?K?Nb4MF9VD%jq;-_E&XU$)(mG9A$4TovX&or76Qy;e zw9b^)q0%~4TE|N3TxlIFt&^p7w6xBa*5T4RU0TOW>wIY)Fs&1&b;PvJnARcFI%QhN zOzWI!9WiTh$4%?JX&rb*NKOgU8{|RC5kV2yLm#5j1TOYt{pR&v z@e}r$k93mMDy+ALn|+>ifxTnt-1$wfl&LFB?T~e~0w)#TDM`Zq z;E4M}A%@qTkuo-b-)8&N-TOCzZ&b-DB{ahCXzXWf1nyB*Z!w3TdCzI@sVl)EoyUf` z;0LNmzkN*{th0M+cuzHYd;FGhVF&*doDou4fjpM02c3uE=UBaxv%uh!&fow=Z!H<@3N;(U?>d6LOBkafK#Qx;=2Xn@H>+XYJ zP%7kWDC76iEq_6KCNyYMx69E zuz*z6csBBoss(Nt90mUo8SUAch&nr~!2US!g|z8nci@+-eB-jTb#XtbrpVp=LPk?V3AiH&8_fDme)!0GJd~Xf5sYq;QlPx zt6aYa+;FHo_hk{rmv@TuKJdkA`6;!CE2zHg$zl9%hx^-$VoKqc7dZB@9BlhNFxMO7 z<<9$+gUtPIu-?6==mC0@?vz;W3ick6DXfK`(oXeOB;#kxag%c~twnE5@ybXauwl}Y zkur?eM=KvRc!EWWO9h>tpjV&pA~}Y4q{`REVf>qTWiIvw7iXl!eZ%uo6P7J-416F| zSw#naNcGQWoQKJaSpwhY5T=uR|Dvtq{o zkG!`RqKBYkDEC!#499)`+8IX=!;kzdf^RvF?;I9ev~~n_o0qMmcY}w<%m%-L?|Q3G za|J)RG87hw*Qcxv8!7?c9n<_6_XTmNN?!M`V5(z5buOq52Gz-+IvP}GgX(Zloerww zL3KW;4hYo=p*kWH{+|io-2|IVjTGsCKZyG8;L@9i_~{nL4>)0qz22r8o(J1%;=L0- zL&gOdihs=C#IcPDzO#66UyMpWPWc1BV8Sl%|G2@nty32MNRNSg8-HRPhOax+-UL6E z)Srh+?cg8FuAFm#op8)*fzo~Ob(39fnuw1!Dlhtv=e-&d8h~GrOFr@JZ5%gN<~+O) z@zLGYv4z=SnMSS!Rq*q51~!??fkm~GV*KIf+orBxzz_CO3rq`wAFo-p-JE&O&y6p( zxiW_5W|{N!a_}A(p32kTQFkP8%AL`VSomQ@J3GdWv`We82(aH~(H7N@s9#<+=oSce ztX44!?}h(c8IvB7UR(b~zJ&oYQM-l8WbktTX2$G6tuBf5=Oj zJ7S)&IX-RpFXA_;+KY^jm*f0Z+Xwr=4OQj*lffI!O3wC!d*AaLFGRf4P+~?T6Yr@J zY-20Hyd>t4tnveF*`?F5_Xg^jpQOB-gwOx9{bs5r)*-LukrBM$YYo%h_ha3;k^eB9 z;kTR?Y2Mcm|2y+J=NI1Z7m{Plj`-&#{We2Jet5fV~83bTv{C|15f*!|0=`2{(IWCnNsdt7&KtZg^hDyAt#Ef$R#WmEd=OjU1L^ z9lL#NXZ>Pu(xC$Z{g~$qbIo&1!JAK-C2vK1bX&Y~&P=fQ9=qRdSl0&nLL4eb;c_wB-;nPzI<6*>KTm3xw($c;A<9T3*KYh%J;D}GzHr( z7VFK#x|ZfEQf~m>TRyJ3F#_}Urj$BSut#vc&|nyJro$F3`wc%xWUADx5cok(#bii# zfFHK8b8a|^eMDWfBctyzy5XPt$O**bM>f}A1c&y7C{}@UJ3^)Xz&D@1jcLZ~4;z|< z3xWfhKk012=cQ!8tbw;=e(?XFeL2dAFM{=km&JIS*ucm?={ zUaj9rKg8`tGmfc)HE&;Wc!A&h<7vWW1u&1!(#{*+$O}28VK)t|yX3`U=L688zqLcF z1O8a$gnK2+Jy93pTxMGaUK3k-_pm$m4Y6gSF<{q>TeJ4Kq8?fI^`)cWB@f$#bX>3w zhPC}+>OJ4~dRa9)L03y^)ujk<%h#7P2OVL5vng920m~bOl&FEvcZYAc2ZveR7#-Y) zynyzBF>Ub84V~?u_9AZ}@SGYa*yB=m(;WxcfBQ6E)ZyM9^7LulwFmchZ(ea6_*Qp+ zbI&gPUe~l(rk*r0KGiG8{(r~91^om86+85{RT`ah2FKU_;{3RNC-OkrM_1K=Bh|_q zW`YA{ZnZJrci;Tw(0;IVrPx*nFn3nrWqUAx@yylC^^uf{wn*Or-I_eZu{pSY(zpJ} zzO=>ln& ztSH6xztfm|SqH2UB^W5^hCCG!>rPd$d7~ZgSKPnE8BIYe!JpTtd*rslA%2NxXQHk^U$KQsQVrc;eaKcYD~z56ls^B3BEquhM>A&cr+P zKigCeTf;syoGQ-5N3vEZD&N8NHMu$OvLK%QS4jp&Z*l)7=7Qz_be_(}czSy9BWnz= z-?(f0?*$ltw-w$7F?}M2?e#;pIzzYV>Z{7j;8*nQ*{WdvXL0?9!LBz>I|YOHU%bh)8(h6*tw$jIM0qAx>$Jg% zzT)G|{(XJ!YyE}biSHhcVs0Zth#)-esIZ zGT=!!|At-zA86d%u8Ys_eeCBL16JN#_HoWF*!j0lWQT%9=}43;g$|i%{it_@#8N@XP|=&^$KF7JiXYG+}&t)tYs?HQme!_Q4UEP@V*^ z^33cK0r30A}k_~-9-lYTl;6Ky%TRqLhyssh~ zE&>kL?^o~2z__*zGY|$J+#fpaWg2?gstPUR0qg9o&z_QmxM2%=%;%s7&EjRXRTp8u zh5jDU1ncQ+H1SF&~fttEvyZdIMhQ zbTaH-7UuE!f7LU=Kc5!x*nrR2ZW$Sez46mJAWRDUrFZ^s>sXx6+K9Rt;K%8)nvY;_ z*u+K}tOdL637h3`8uf4utLlTmdkRh-t__FZE=$X>7yR(;F4?8}pu}KIbHGQPHv~yR@(m5IwMTDwuEZ+{bkquotyW9X5gQa`4_AUMp6NUfASz}QxIPOGeyxV-Nb21H;7s37P zwiVWR{rTR$$)?~10cR^_;P^VR>P1FigV$b_|E6Qz`<$br2^Lw+xAzhc{HRwl9?F7u z+G;NPz=rFkbn>z&*dgMQ`o*8fz`v)D!NyF=OXyu+wr$Tf0 z(xH(z(X(x6XvF$O_M8qU*qqysu1hg04upu$nb99(GFzRaE1+$)z=a zT)?MvJldY2-$m(@%0H*Um-HgU*F(4E-kyfCVsONKo{JlCKX|M*9%%y4G+$I~U4i;U z(@f27uuN*soRiRwum5l-2k#{^BAw+c|-+sJ6TLb78%2w30XrR`M<^ zdN~96&dWpI9|y~twkGAyfo@f1-lPcdSN7Dvm9w#4AJdG#Bca~ZSf2j)pNqg{!JY zO$+vkKZ-1!?t$gD>l(kr{QJIht>IHJ&(X~0J1$sXB;PIL#(KcF;EBT)%;%;z^q1L! zHCgtXuOGzzqE3tF%xJIgzR+7Xy(Y5^>**=iAuA!QKPS3RevAPpus=1^@WK4*zGko=+);P` zA#?uY>my|ty;Hs~XTLK#TsOv=2ZO*{&n(|RhV|&+BQd*Dur}+Q@m28oIn^$A!C#vS z1Q@%+;Z(ovT44FB-pig)_3Oq+ODr{{D{vVuTbZ!V8QM@E+kL| zZXHZ0&;i#5SGkvghhO?TuxlWW_-l^IVQ}*sb{%Vsui~fQA8UcT`NTD(S3v)^%e)Zw##aK_4e;>%&1K#$S&3^>r|C{U`N#=SE8oFyp;rWq& zw2!k7*ZcfcePI&;tlw(WGvdIWAD+I;;>7(weQfbj@GqyOcINDOean-s!(dUNExGM~ z(a$HyIM@wrQN7#$Xb*IgbRPtXgX4v7>zj0;mtyTFmN59+!Ypg^PBGTCEt^?$!5RZI zuwzF5C5^5icJS;;2bFm-f8@Jd(`LqtUyAOnr>IB%=_e4_f$`M)gX4@K<|8q#>zen$ zai5gj1^Qa8r)#C&ZU5@^}-f4Ejfy{s6ok8rE@ zID`3rFHhTleEdiAH?tGyE!k38)`H`k=AJq}4Z2V+oF(ml!B<~L^ESnxKK%Z+H%j<@ zjf+-}HztX()|qlIH~>zK-Yq&A>zB3*cakMo%B!wI7X7X?pKRZu3;wz9yWiK_Vywz# z*2!DJuIBI2B^a+?Y`S{~_()h6rxfbpAJyguhk!$cMz;5%p5FaHOke_7ZN$8l?-kZh zb&m{2Pk8y=+3ary#8~_%PapdTF6>gO9D+Uo-=!R#OSsr#a)>nymYC&!KNAJ)_T8!C^1drXoyYJv|JbWc73uH8QDG#&H#i}9gF=fE2c z6g$^q{N(OgRapdXS7~g|o&;UPe{GfBVEqi_K~9Fwo#mm$pTQ!J|AYr{K!3tWZv8i~ zT+RN$MOY6)5=1h%G2Z1wxks$H@w|QSY4C@*gw8!Q(~2hnMn+v$lF< zmD~ec7KuML1Q$-{UeEN37w2ovbejrYyqoTvU%{@gBE8k7VZT@BT<6Sx{wkiwg898U4&Z_nB9&fPZx)=9;Xew7Jyytz^(Om_%<`MyYbP9e z>#$!GFU%eM3r;`q(tMXJULU^WlR50E-#JMw>EQJ*Tx5g6#!^OY4&VzxzOs4XLjw2P ze3s+-)JinAfls$6wsGO}lttD&=7Rn9a_*64!)4;EKgLrdRlwrkpDb1apTBd>#T?wP zeON~W%y#hU@~z-t|6lH#!CFFlWOsr&w!hAc17~lFmfZ(lYJ0(31)qQUP3JYH=k>QI zJJf5L^SL}vAOO7a(wl%ySU->45otUQ_8K-?{2B9CM#=Q3i{N`-SdXLSaXnKnMWurI z3nz`_DTuR-l#*Su!M~jn?lxjS-MY2@`c?4#e=BunVg23Gqw|F6S)AOZzf&K)pvZox z0Bo7dUbzc=;n$k$CE(@U;r4sM+I7mN55VF#cE)W5Z~JW#T@U^=)9<(_xFuRA`W1MW z)50WQe4p$d^A#iDw!9?KczTg@2&%K({tGUjK1JqU&~W_m+s{E%-d+=u3^4z|Yt$ zwtIp5MN*^Mz^8j_$L{0xOMY9J^1|P7=D_DH1!d?YhH^}k2QT0j-8_30_BVVL`6^)N z@(dLpRjfybi-xCyCHF5?ex(M#lX<*5^M2W_uQwlny~D@OnLiI466r3N1#UP#8odFW z^JkZl4S3R#$@1>t==j)=nOM&ngvy62!Nc-rrNUuf8TD|wKLy7rx{unx{>qyxYSs>B z^NQn0H9#JQ=?AyhV81ktnlD&y*DYZ;YXdLbCBEkh*5lA53sdIxa{F?V%HaoNC2owa z0+(O0Pj|M5A9ZN+ihJNgWAm){?tz~#H%qPwtWkDcN*L>X)QS5h{CTh+(zI$>;7M$o zhUCGlkqn)Fu;q!HuKU2p?tiT~-~{`6XqIa{SYzqU);as(=d&w}x(Rm8`tdIY`wfB1 z9iN|pwL0#UsCZ(3IAc=dU+`$WM}jH*WTz{p<;h%!za>O7&f6RHvJUKW8^QT9rRnWp zSFYKgJ;8iS-vlo65ogU<-|{&T+)}gMYCX7F^@Yhju#~s*6kTwx^9{KcaJu-J?RT+X zGt&7Xy9mBBOX;i+p4dOFe#0SW2$qyw93+bU)~V5%p98_%9g`z?1OM08w!-XO`p;u{ z{SLiTry1XycE!R}#82BNAC+a^pQkUQdO8g8>4PD;eBg_Tzs)_PkazNGjQ=^lf2C~p z_HAbok8XWoR1FS!|Lf?z3)ru#FNwSh#{D|a_G21EWtW5{^Cu%7d}3pyHh7M6 ziJV$0_U}pE{L)~nzicmBGof!jcEDK(d?4Waw>8)Be)Uk9F2K_f!X; z>f}=$eX6rhb@-`HKh^Q4I{&l}0PPb%`v}lJ1GEpp#8{i?Q$YI|&^`yW4+8CzK>H}r zJ`1!D1MSm5`#8`(53~;i?Gr)!NYFkLv=0UCQ$hP!&^{Nm4+ia%LHlUXJ{$D;#Um}s z#x_}>iC0u-OgB1L1Rt(qFSix;q_Q)=)Vvw`|L=wFz)S-8ll$nWlJOb!%fq%Ke=%Q* zbGPJv|A_r$=IYTar znUBq`EByBEguislhg>^wlJt`R6>vd;{ikJM_b<1;yW#coWqMo&F<+MF>z-#mf60{W z!CY{jSGIJ)AokZb#V&5(-Rzs>nDhIyvcyOqyjfRWE*Zb&xTIh1bnwJ)8{O%4mFZtm z*?mZMA&$Sh{F|5QU$H1iU+*;De>`_e*nZ5P4~O5Sz62+1+nmM3Q%*<956;HxXAj#+ zJ;MH@r{hnSGuTXdIP?H`hOq6sDDdMIU!^3$&9RqCc7xS)xE&v0zmwwA_uU_#m(npU z+Xp;&MZv=g=YQqIpp+2!_)_<=@P4d!Z+_NRVt>{zcun~J2dua8D*J-K%DpZjrd?RS z`L=T_gUu%i*0ywD{f}DC{T=(^$v>0R4BDZ8u~o9~E_j9N+lOo~aXoDB)La1TnXWn^ z_8j>{vWYc*V6FNokByp;H|ton*9Lr{plf_}J?!!N)qT?7PdDGspH~BalZIj+vtM^! zb79r}D)?(IXiG8ihBFGXV@oQrUe@lC@&c=GwNFrbfV^Ol2YXk6eFR?&v)_Z>M#@(G zN#OA3b8k$(i|b=$-}egkiOh@o!j{{JPxR|&G4Y3UvMOuS!E7siwF<$aE|=3bmg0Q! z&GpZMZ5O}!P+9^V_Q$PKUf|{%|7QAu4_U9&cL6_f+8NEPf96kyYE8jBuO7{pV%~pz zZXM%K5O!_JS+&5))%uEpo8q>fveARZ>_*b#aZ{HpMLxEI#z6Qj0$IpAv(|hs$H%ckt+T_J&Fv|7iA^8?FYH%4`!Q z3mmr=|0}f%ES&$hIJgY)fh)#+GjPAGCSMW~!SzmD{=H8ZoPJsH`v;9dJ(@{xu$jw#{1?5=guaK_qTtoq#VHqQy<)YI{^D@M~i+Tcy~Qt=T6Ko zvNHyx*zi1E<6XMfAFO7gBee?58L+m-9lZP|OWz!v8@6;AQ&%8f*e~S^RxM>qUysjM z9Czxg1%Kd;oK=VU!%JpT*a1Axc~VPe>EgJYp5`((Jpb&Qp2Zvo&)mTIXb;%)t6~@z zIBc>1hDTu3y%&GP`ET4HX(WpIWI&Lsp&Gm`P}OKLxSV^fY7Dr4lGL?j;E{m`MO@$j zv5LW!;BD8N6#wFS_;s7|>w;}84XoL_VSil?cisvv`&u?Q1@qmbf+d5y!R$&M8{*z# zzKzt23A)LC0}I1@bV2C$9!@AUDF**zk#i_ zO5bbX_f0RjyZaw_=9vPC>zMy0n64*SV&+3<_z$x#8S|~z>P4((qDJsul+*BubkuD0 z=PzJ-GEBy6Ci=@T>(h1o`;uQ2@D2W$3zt5(Cx>Fa(Q@_qGzsyx-11AUVDu$p`@{kJ zBga|e4|wOAd3wuvVZWc;V51g>@fvb)P+}&2kErwA3*bFGyB-F~{O@;mEM3u}2R?Xl zTgPP_7Yegf(N;y?jHz+Q4RGE@)$rTvV6VDPyWa<%!+%RP$PD&*W`OoEIC=VN_ph+$ zY?9fJrGwcn&v~=M5&jIDW8FKz@4~fD)VgAR5WUhP558=n%gf~PJP|&m>jO@o;=9NT z_7ST&Me-(i>70%jU-*~5&UW2b1AcvI@@P#Ep4aNNIR)U|dbeZpVed3OEO%o1UbHCQ zY}*@-JQ+{j0V(j{7YnN!r!n8kb49$x^_YD&yxrm)p8qNFSFeG0dmWp#7xqrXKetB* z!EP!2iqGPZmvP?ekq+2sastoEOV~Tep1nG767izWIlN6N@V5u`3>bmeg~}8)r{aG9 ztK<~`hgwRFd`-jiH=}ES(WN)+&$hgG1mJs+~qF)vk**Mxq0w=I&Y5)v2ZafP<>k+ppq!w>VvO+zUSIxQK(%!_D|s zP!|riSr{QF3VXDFTd;H*xbo`7fSI^|OK->4m4eSbty$!G3jW21DgBM$&8O?!Cr7}3 zmGbI;0d61FklPr>jK}f5H(;+CgNDy1QRgz6U)KgM&Ah4<3jSBY7WM%Aog>gX%Tslk59<}cH5RsP@4=Hx zR&hKI#(c%cvavXizA?Le)}05xon>p+58kO>UFpmmPvkkxjQ68WAJTgfw9MU zK8za{Wy}KC-qf7MjISl0i!&H~(#FshodXzuuZvzTQ3v0Ae#7iBSX_5W^%n581EFH< z$#~vDhnMZn%ACb}WuEPj(#hBw!3I1vM$rf@poCAc?do}@QeFk5A$7UTax zgSUeQSi(K)+;Ti0b#GrS;Q~LI*3`Qe95!B^QGX2m$zngXvcU3NuPnI$K2~t6WE}55 za*%(?CUD;Rmc>uO6LYe4X|0^-l>g;M)xj(-%(o%_BU>u*dXHVL;R~D-JxnsP{m^tUnTCj;q`|W4oi8Ui>@Z@0&j7PI;d95ipzS=Qr&3*7O zH}pTn_g(ya^|dVKBfmVuQ#)`z4+8eQX6oz<7vzf8kBhM!1C+`pVSc0Q3C-)Id7m^d zl;(}nyi%HXO7l|xpS)F)*GltVX*9xyh)l@N%JmgUM9`kq#nQZ4npaEnZfRaF&D*7Uy)^Ha<^|KdVVYM=^Nwj=GR<42dCfHM zndU{)ylI+OP4lj4UN+6!rg_~o@0;d@)4Xvy|JWZ$J3F=&>xpvA4@=CC7q~424{t-f zCG<|Q7g(UUPqqvEHE_M^4X}_7w{$r;GS}iz3pk0#I4=|LPafV|@*TX~t$o44E!Z!P zURIle`E-l)O81b>@c(yZZDsh|xrog%R`6eUzB=>^ubED*!xDob^f}!7Su+~udcQ+SDH z=4>(>uwP$)RJjYhbipT~mH7Vkm)vGO0>5ERwNt|H6_2r0eh5z7`0)E~rt~TGf_Xju#c1O9&)DR7V*$RuaQ#X5y&(^b!l zz-x>GVlr_5&dv&6^$|RFIneJ2#*6BD<5fKPy=Gap-mVy5+=<+6i@zt-I{XY;?-P3s4yoA{AC@AwgwL4$QPlr0k9sR!$NUw4c=WIvc>xozl}y_5Eu&eC zrMtX++ZLR6p1tMm70AO_c~<&%6xbuGd|*?(7_0i`3gI{4fUM?=tE$CV1IS=89uJY1pmD8xHM1{$wmLyQ_cih3_MOF@1yT$Ur7(RGJ#)mb}8}} zv_=oD#XjWtEFbp<S-z*nn?YI`FHV^xf6j9&zYwjbD?%9Q{$HAWa zW=|4?uKly8Cw=?C_KVX^AA)m)75wC2=lyiGdY*?oMfN#sUWI}WKQRmOL!R63_obzE z;Ikj{H`d~Mw9lM;kqfpp``XKc^0;0_*Siv&!4+YBY5$s#N1^&t_!fAP_$o2OH^|F- zCh4CQfjFe?26Y4E!E|a#SdV}ucdmUx*^5{Y?SRtf&sXSbgJs~D?Fe3Q**@C<`PPf|yb=f`{7U+@(C2K|eqo5zsXCv;7H z8CZz@Q2qz(+ZLAlB(4Hmd~vV}XNO%Y9IjypHhkvb{d_X^_1-Ma^WdJ^S#7^y_n-EZ zT5ty}n`=~)&5b$9K&O@!c+0i>0)g}KTveabwgZQKSrBC<4nOIgf8$KwaskJY`P>U(Hx$pjp8|%S zhKB8u{~h~W{xEZ;}g$Dm$JUZ74Uehw08(zf5cun#S_fA&TZ5bpQk^g zSB&Ya;q-24r3$`JE$!aUU-*483w_l- z{DwP!?zjvt{hq3nKLdGL=~iNAz{P93huH;TS3KQt&jk#T;Dm>oSJLF7Aa3# zmvSOb?kQ%w5PYwqtYrXp!tyOXN7%tb+`?aQEBUr&oSP-j(69KU2Wme=^xr`Z4~ahSjfO{CP$c`%Xb#X5)jE z{`%l+1`9))-iony&I$Sb1mk!6hwX8}$QzPb?|0Y}T(mGkdjaMTos8r=kMTS>US9aF z_%ZT=zq3}mgU7-SzGuht(RF0QHpXwH*!^PZY|IyDY>S0h;8hl@c_Q$&kN71 z?4@yCH9ViUkBI)73l=OAY5j-i*J9O0-$Xp`r$f0vz5#!X((_~btd9y@i08uVZ)%?w zdV%@E&1Rn8Uwq!o(d?H;!D}wA(_4h^GuBzLZ3cMhiNVr$cz(4$t-qas`AAACt8@X* zH#dIQD@E{r_2PHSF#kEoDGR^G{N*NcTk`TVG1h_*fo&JSpV!nDAAE`agex|VG5TCW zYtJ=?B5y4(U4BduyzfmBw{nLVYhRY?SO?}uE}x2bQ<3+!SM*!S8SphezNWW$z9;5e zI{#uyhLr!LKxQ7F(Egav`k2sa;P!tRd>q;@Oq|&m|CqnGP16pi4ny~XgQv+H$A%PA zqY_6D&tR>*&H_*S`litI3!cNnv!|^EYy8;qhW9J{PmZ_b8NJkr&%W+EJAVb3>O@c- z392(ebtrs@P6gGmpgI>+2P2c{WKbOqsV!}o5vnsnbx5dA z3Dq&7Iww>Ih3ceG9TlpxLUmZEP7Bp>p*k;A2ZrjzP#qbnGedP~s7}qqSf0?ap*lBI z2Z!q9P#qnrvqN=ws7?>n@u50DR0oLa1W_F!sxw4&h^S5x)iI(vM^p!i>LgJeC91PT zb(pA56V-8|I!{ywit0pB9Vx0aMRlmCP8HR$dKX?M*!~&&+5ef#==_B#O`MI5`@j9H z*8lWQnwT>udc95b?qcRaA?82)(Y{->FBk3GMf-ZuzF)L280{NI`-;)NW3(?B?OR6s zn$f;zv@aU%n@0Pp(Y|Z6FWVu~w~h97(<6P~XkR$mH;(p|qkZRSUpm^ij`p>qeeY;r zJlZ#p_SK_(_h?@}+P9DP^`m|NXkS3uH<0!fqpw|g1p9-sBPP~Y8^l@m$6ie>$9`fZ>w63fJi?ZqXN~HN3uJX-AX>_A{^b zlRsM5A@8C5zQ8beuL8euc`f`FW_E5`*dJ}P4R~;~26=^Nt^ebzqc%Kq!KlVPWNA!mxu3blnCSvtVEulK;UC$f7Bpxx>XY#cBbBi zu}k}=tSjOH7vyZ2%ESrA{+hQ8R*17MUR`g*_}%}7^9xPJaaT{pi4H{0hytveg_d3 zcsGsbljTnFwq)=(S9`~P};UFruOb($@90GzTfp|=3s*s&{V6mgBm zbJy=11GX|hi1BaR3pxGBIXR= z2G|=ZYvOvW5w}wLbWWxSJb0_wxa$n!C|{d%M!-{^d*nStoG(@I+(#Ddmoqbi^e!Q; zXCIv;a}exj*y^<*1a+$18x2#y0g2ki;z97MNoYD%f=id1+L<1~{o{BqH3{~SCRcGt znlJX<(_Tm|248ZVvSRE2{CFvSS5|>L6i?(9IKuB!c&}O$e7;joW9Ay^!7=wA- zt_iN)i9S0wswaDc=M+x)9SpzXspk)ZZ-aMn)U?cjAIQwGv8x6gl;5`VAN)@7!m&Cp z!4BsRdBwmFrgdCm@^|nW->`I@^@!t_oUmUA`>uZ1bB%c{#Iej{>@C4fHQW_DG!Pfe zi_|#`_SAZjuB3+Nt-6uL_!nB71UMw+VW0lVWitos+RcsFxeRsm(xvy=zz6o2)ohYN zUE3GI`yKeccW+PgdBC2Q)85!o3iiFK7#O)2*SB@%{W!2z*{zCi3*qO2y?-3s!@WAZ zL;~~I92XEN(pWkfi&;b`4pXe!qKfua3_pK;6@yeBtuCSLku$pXZas4^F zl-|sRz5MB3t!5DTt=hS@!u*IkALX&t0FNxbz9<^@v)o*7o2R(n(MhK&1YtirPTD=+ z7d(F0C4D90ZnMYq?k)h+JO>)LqH!%6_o8tz8aJbHH5zxLaXA{dqj5bN_oHz^8aJeI zMH+Xc_9(SisXa^WU1|?gdzsqP)ZV7{IJMWQJx}d@>JOm)0_sno{s!ugp#BQ#&!GMe z>JOp*66#N({ub(wq5c}`&!PSv>JOs+BI-|~{wC^=qW&uC&!YY=>JOv-GU`vG{x<55 zqy9ST&!hf6>JOy;Lh4VX{zmGLr2b0k&!qlN>JO#JO&= zV(L$({$}crrv7T`&!+xv>JO*>a_Uc~{&wn*r~Z2C&!_%=8V{iH0vb=C@dg@?pz#VC z&!F)R8V{lI5*kmT{yy4Qm-gMIeR*l$UfS39|LOZn`U2Cw!L+Y1?K@2S64SoL6Snz8 zUt`+$nD#}ceUoWlW!iU{_GPAhn`vKX+V`3Eg{FO@X*9A2w! zX}R+~_U)&60*!Zpbpl?0NQ#CISDy1HF|hGTS&qUB(2>6EF!ej~d0hfO2Jpj<;1T3- zX#)FqwZ4l?h5tKo(clSij{J1LTbYQ{Y?VVD8RGLk_QR}P?91oui_ieinJ!htQ;2#) z`O^{nV8t7Xc~@^D|8I%$z^y#kZByKDTrWj@T|~{0sTWLK6)(H*4)W2tlj`2(!cI`! zJ94TF`&##C=`&!lzNb#Q<WqTq>Zb133l=+kS^@t&?E z?SkVUgJXHZ!F_L@6_?}l#FeKR>;R8nTl3*|3HHG^_#KtOGOq``G;d*_``~212>6qi zg2v7w#LZ^d40KEsNHt zW|%VdI&&{)$aeH(>`Z{E}%<0iGi9TyhQA@PqBPo8WvE z3q`d!)Ncj|@dkj^jZa3s$9PaqNX=dYw*ET1<w(^7Q+N6NT|DS;=;k2q_`V0`G^4EvLgdIj;%9~qtXl336F zE8sQvpS!DLJa(5W<|u+wBVq&jz0@z;1V`oAH@;h&b zX2+!>Kj2AZO(({;Rce;wVsLt&uatBs@<%PVhSx%`Bela&>P8^yO+9|}@q)!THizFi zjC@dm^U~L$50ddTQ0Ju=zE3}nhXpP@$YH?gfw-$rqVycF=bIlD2F|Dth%c{efd0tg zvo)oedtvv=o|Mi5j~gkxVeP{AKNjZ60{5E9G$-#s{_C}~j<>;g7WHlqu|a<7rn&ze zPnBwVd~rMKLHOCTcjLHwUx7d-@@3wgZQy3~Q$*OblJ9Q8^(?cJ%7wm)ZFp5%%tq*$ zSv}&?0tY?*q2Yx5iA#l9Tms;KJ;6ModdSyI6_k7f{g~GeJ{phcAYXNSeoZ}i$O)ceD5@_-^{1#l71ghz`c_o`it1xg{Vb}lMfJC+J{Q&RqWWG`|BLE_QT;HgFJ?mY z$EZFT)i0y^X8(`=8PP|h`e{^Ojq0yaeKxA!M)lpO{u|YYqxx}FUyka}QGGh9Uq|)r zsQw+*$D{grR9}zk?@@g|s^3TT{iyyQ)d!^dfmC0R>JL(VLaJX#^$n^1A=O8u`iWFu zk?Jo}eMYL^NcA15{v*|gr23ImUy|xiQhiFQUrF^Xss1I^$E5n1R9}35h{j~m{>H|>y0IDxQ^#`av0o5;{`UX`0fa)Vq{RFD7K=l`>J_FTnp!yC} z|AFd5Q2hw1FG2Mus6GYNub}!CRR4nNV^IAJs;@!yH>f@b)$gGC9#sE>>Vr`I5UMXi z^+%{a3Dqy5`X*HWgzBSE{S>ONLiJauJ`2@vq53XV|Ap$qQ2iLHFGKZbs6GwVuc7)j zRR4zR<52w^s;@)!cc?xO)$gJDK2-mQ>H|^zAgV7!^@pfF5!ElE`bJd$i0UIz{UoZd zMD>@bJ`>e%qWVr$|B32DQT-^YFGcmIs6G|ducG=^RR4Vr}JFsd&`^~b0_8PzYN`es!BjOwFN{WPktM)lXIJ{#3a9|z;pZ7I_Q}$nH<;vKE{9M!TdHcmKkdmrgGSS66|H;}FT9e-8#S80U{?+JhS3b;?d<3e6VZ~!*vo?UdwIWbpuY0!rYVfxfOfjS znFr!(HbKTe89g=L%mrhiywI0?KioYX_PWc-k>R_@1FpDowOR-K#Na?f$V~VXbfvih z!JbcaIhP~PS5)_~K_&Q0tV+fSapXDJit_e>XFTty2}c}J=1}5QPWT6&rUnW!_SlWY z;}I%gcHYz^32EdZ$CMc{`qFlrA3OAb3+!$zXX@|gKfj;N99Kw_iByHZAz$@v+A!jl zFW2+&GyVwM`%5jdVeftNww_uFKC{6p{iH1PlyiilCc)p)RPB{)1s;!75eNhGnOvB; zUk3gPXM@MH;ZGSI2z8%>_rGsUA3OpcbJa}_#`iraZMpUmEVOU0&MO?}Fmv9&68@YE zD>V}O@p*dXYfNInJ+X_O0~N(t`<_m9Zv>|g4+!w9!d@-NHX8@Ou5O-U2K!aAY>FA< zPqH|X`dEJ*>f5#X|Klw2_7BT8K>s3%y-6R(J!8%Ggjpgl`Lj~AI=Cn1w)0MF_`l41 zqxXXq`Shiv?4Xy>p}V3I?7oMi!Q26Q0rJmH6yVQ#$C+`r!V&kc_p*Q$IPS*EL8blB zD_c_R>IzofscF>d20iArg;5#cxnC{z4tt{B!ES{s7yNDe%8n-|dc(gkzoe1ThgvkJ z@qrfN;>|y2<{buSj4o{G^oKvpoGt1KnELnN`di4x#{OR)u8czS9UiC=crNugbK}064r;cwfulCBS9b=-4}~n%!*diJcSb%2JaI0wZ`O?n;l3s< zwd7xfdWJmXL@p2Tw|5bW&ykO8`FeWK8?fw;*stC9#8_(TGXIouA1}pKN?k!c1W#;L z<}>iMpS2f$p`O5rtzLOP?t@>uWx6Z!qgkynVlm(kv(D(TL$@)6;BPbDthum5kVd_q%KU5{Uup*rfllMgFr%a%|``u%?7?&;_uP-DHXRxX$VR^BU~H zd138s+rT_8wWjFde80|){=xWBZG=YRR^#{7bp2B41P4rdD_M-+KXI+;wU0e`qcb1% z3Vp2t--M4&nQcMNdwvNB;B8z{57|;CaQ-Qg-0kJUiz%F~2X> zu;Kvn#}#znS_^m+J!CN)I1Nqzy z53MxVzGAvbW>@`;e>v8$+6$#A>ioVscL_5J`>l@ylb6| z=>NHv6W5k+QbElSUZ1A6D*hSrI`;Zc(Ypf{$vQs#YqB`Y?ue_y5%8*&-s86?VLtqn zzhpO9y=$fQ5v*@Nd`_)%0c*VVZ89E5T>^LXZVzyu!b!z))XB{Hyjz%g{T4osz474l zl^v5cz;2&--@ga9tcc&uoWJ*uUf=A0Vl2CoslcW*D1BQUdKF4$A5sYsyumc9{he*y|^_P@drB>Fn9EA*Dt@t zSecvVb}k0@ST|HILS4_|gO}NQaep{o*LPX{Kpn==pjI~6{?(TFHr$VcB8~yh;L`@# zyG3#RZ|msayMgK<#pjzo)8K^&bpE-+UyF%?Z45XVUs1@Ry4z z+RMO0{~ud#9!_P{_WhG7Lx!R<6EY@4N`=~_5QRde6hcIyNGOENL@87hrOYZtC9_CW zrY4jjGF8SV%I|yiKKs4z-+Erh;g8R8?S1XN*R{@Zo$FkpCsGE%hM@%&EttBIiK)W|oPKF9*$$3x zv-b=7hiWx8g89#|)NqEO*P^=+^{7^lvJ zb&_({!)K{|teIWQyjEOBJ=c6Tr(*EiiSQ@OsdbZ!(rmJ!R3E4_zMUA058GVi_JQeR zlhw)=(Fo>9<2CQW_01x`=YIf`eL2X!9b{h*vhN4k7lf==BFw zdhIN|_LX{${P?H-$k&o}r(|6!S+`2owUTwOWL+#-H%r#ll6ALaT`pO-OV;(0b-!d? zFj+TD9~*jIF@a>~=tQ~GmX;+Iu@7vz}CGComG`(!j z`+PVXk{b6Q{8N8kH{Ha~EtkZ4Bdf*_oa4BhwNvh{f+X!Zdu4kknEu|jpS}M-4%AjP zbHnvJx;h-pczyWD6M=(Z#&xV)QspFROj+*}_k&v!7@Y!e&g$l(u7AwPwc2(+KF{5< z(aRp!%N515eoITzG`4nlxq>$;osrwVOp(5Fvccq_ zgd{ClSJ5j9yvmG0P9NvCX6A$zMS{cUxr%j(O46b>skMiK#TS-!_=rf-425l0odR3E z*Qgd1mZW*Eww`hacQ4=`@mwrPJGwt&)jIHo3BR;jKAh8?QkzZ>Vej<~#Q zWdJWwx_j&Cd`VjN@@Kd`l$1N?nB%o#yPeD zTsJ#iS=fy}c}@bn%HZU#sFMCJ)Svgtm~wzGUp*n%`W}5%H%xq+#P?my5_iI<6@7+7 z#Et90Ii6YnIHY81JHd(!JW4D1j)1tnc)?%G8aPbjQaDUI8!@f1x zPsuLwq8;aWx-R-#d>q$zec^p^8NBeOUveAn$D&sLVmmndj;{T$-vGkoRF_y%S?S->mRx}P5Z0R25s{i-t#e#O;UM}-lbv*f42rvR=r zTU2*t41JV#yjE?Gh2Jp3+O*-f1ns1$UyC1j&q@cO0L+J`^U*D?;H7>!BeH+dhn)GQ znhiMZxZZf}JW1M=sg)P7usjam1MIVEYrd_(GAA>01yH_pGn z{8TFTUey5>U>??VUkG`6et2ggIQGFo@0W`p&+9G}WrAO}ZM&x-AW6%05Y@O2wwpDP zAHw{9qNr$*27bYoET$$XNlW7W(MDZ2+W%CI3-V($e&>D`SaxlSc`fAe;~Q>^a`30w zXC2})lC<|H6Ym|9O>{|Ha~OUM|}S{>IM1`y29q>fYeR&ER7eg<3&klK)p% zB@Qd{lq{8`*&hDQzY*8vmIyvhf;@_yytrU5SbZ+^*BF;1jnVhZjw9g4@muUZ7$27w z%N;e~Y##l%12foP?2?sIdTIVrR`5o)VZ*p3w{lbI6^&Iyn zBxuai7Z+{^qi^QObL@LMRoBXwK=H#(^4suyjhWVu=YrW+w6FvZqEDXtzJJg6$fwm( zERerkp2ey>c>ewe6taVQk!Q%47+(+Oj+|S@19>~o(qE7cc6fJLijucgH(19Sz`818 zcJnabCU&zFeg(6MJ*7pqVVzraY`Zk(+o{}TqmG!Lr*=Nq(gvrS7b@`OX%C%tMS&282ke4jZim&6~rMtFzk5r?N_LBu) zS3y2f)>xVqRH84&!$(K_!H2VV$GKIYUcKcW_hT^KmKbMyF8B-PoLeZg9M^F!*$V~8 ztITKCE%D$+X{CSiyzKVdS5CNJ(VE$R^6nS@`l>Dpx7JTs# zET;V7^}c7Q1KC`{-3$K8`+AKO=o{d@pv3t z!6*PuG7VTX1NQ1aQLqFo)1@bL=`H%S-Lj372Y>#}Y`quq+raZ_r6t&=rRMLwJ)cmQ^I_^}7`SF-qo&?x^f8=QQ5Xw8XwawD3jOQ*C!042%$S)qXEhH0 zOvmuf5%9a?;ab!=-M8L88{Y#C>YO;=4f)Qg3w~w>Uj5xi$`a>(mkJ$HGX~3lT|Ldi zB1zl6E68&zm_P38)Jk?qnvnC*qy<<~IXdJ4`*f98##q@Bs4V`TjURCv5+Z zh~cLWkYBnlK-zz}1hViW{>97mGc_~?u5XJ+jfI@)U%@}e(1Y}`)C=m27RryfL^%=o zqrcXEzSaV7OfPHuiql5jwq3CTPsJbBeh?sz^BR>)4}&i*UzcnXhPd84WM2?i zH@j)-aSYy-QTmcH9%Af@dWj15z1VU60n?4G!5F{9GtX`pA)lGe(q%M<=O^utTH+R4w_67mED1UKAMV=>%p_-6V^%?FR{BP z4Es^P)x2w_y>AP~Z)$Y@1+Y-9S(7&MyPS?PK1~trPYOqW8jJFiQhHf?nDk|6~v}Jbre6YvD3*8zk(cdomP^KKXWnF}r zGxF0H*iH!nZ+skyAS3VfwuSLJd+=+$3M)qY^k z4PAmg{HP~%5$tmSC#~vJlUa!I_?*y7afSzHrycVDiHXX=)Omczt91{5VnbgM!Ru!z z<{1_IuFinCQ_hek4W1vhawHb{9=mlJ43nt;^08Jlf88NYJF?9_jXIC3e=N62v=RO9 zYHDomfPZOMs#u{<%+>PXTu}zW{Qd7woWWCWHs`pmArHIngs49Fn0@L*sUANKM>}%9@ zHhlh0E*-Ta&|e46g!)nEpIzB?kz>ml^l>rqUq6HQXBe$Dbq@#2$j$x%8@MokRF8xH z&{}$B5nk_^y;PhZzh}E?n}{MfMMe^|ScCTno4RQ|F(l3dcCu z;r(CCnie;K|McB&GEWkxu_&r{j)ChYUhZ|vKp!6_lT(6t9+!_?>e0$9;0m#Ow8Z$O_7N2j7P7x2d2d5iYo`DnUT*qjG%`rB%H7 z!QEfY=06=k-;r2932J_^t&tZmh5j?hR%Dz5zm)yWvXT}1!M)#I*T8&y9+%&8ZvpzE zaHpnI%sx8z-Ua&Y;O%>T)?ltC_0DMMv-WLs2k(LJw=b!-kVU`8Q2(bdz)UgBz8|6A z=INc<+y>Usk+dDfeBW3p%{C3bZI>uVt>@T(Sr{e;`T7{fX2J}4c{O=6%mu96!2Z^A z73xoUZfwZ~@0h5JlUaxQ!XJi%@4#DbspUCC{`8{`$_;^c&)KVSLqAT%S^KT}?JLuqzXy!-!VcuDeO zS<`lH$NCr}jl&80=E^QJ@c)*PdJE8xg4Hfdi zi%i~Yhg(2@?R9(J0bV;#iqqc``}i)!Ki?1icgCoWxf1=!t|qhPUI9N@ACp1ptGk2F zQEgx=-kE?nYxs?^{)*IjRT-a_gfKwA$=kW)*g>DJKGWSUg7u^eW$Mvs;HNqleSLA= zwsOPhG`Qk>&6@Kzc%Jh(r>CYE zzm+PZYw-FKB>{gA6ZnxWy8o^p$#d@bY=pRO>hr(*v6`8`@!pL3@VNTMF-pG={nGBD zpe2nKi*`Q&>2DVO>;1H9Cec4o)^#Q+cI2z(-scxoTs+#WL=GN{v_Rh?sIMC;Tvt2N_Gj<8ksP&hxM@^GQm2GsZU| za5#M(`Y!dxc<7dZH?U}xXkvX;``tiLBzR54llAZ559A%usHFTA+a9Lpm$0soJ3UrT z*^`EOLDP?laUR%%Rr{&=kiM)X!#q=*cH{7~zg^(&st$E-=(C%dKSgW7ScJ+AO~AU# zszaxO!HU7>zb}s#r=4pH|8oEw?KE`qSt$C@lvZeM1@}gtmt#ML^T2uqN;SZAeYEX! zk&fbjG2Op{%Y`L2rUO*IhyGf+E}=hr`m>_^G-v!%fArVV?@N9z`Mnf~?@j)$kHqgL z$Aug>a$L!AC!Y)X+{oujK6i3nkn@I|SLD1S=OsCB$$3rAdr~e)xgq81f5{yom!#a% zpDkUkNx3KW0;xAhy+Z08QZJEui_~kR-XrxQsW(ZzO6pxwFOzzk)a#_)C+z}hH%Ple z+8xp^k#>u;Yoy&H?ILM6NxMqgUD7U-cAK>8q}{(l_ywfjK>8J=-?54COZsafM0F6S z9f&Xyq5QzZA^Y}jIFEJ6iAQ@WKcjF7l1LUmZ~Vtt=;rt2}dOZF3o z64tAgEGu=|!Q!D@{65v6>>&L6`RFLt<;EWzu!J8_ zP_6H+0l#$7g@K+taN%0RC;sqbgR(}etHI}U#n$eWz&YXvRca+*=dT*oZ21d6+2`*5 z%y6*Qs%O)4E0Aw3$o#w*di2wo;>Hu|=qnl@bjTjO-0G-%!&<~`dhhFBfdzLgdc&*- zzk0-?Apm+mN`0u=4u1aImxC^q;H=PT_5pC_yEiV2D7&Qp)P(Ab&nNei!vg$n$K^HQ zc>nsbKMjH4(Qv(@5Gegyn{;S1|zE`wEB zf5ZfFQRgN!*&hV!3G|E3XFz`U>mFS>aJ1rfQz5M9%S*&|Q-01R>tn->cd)M4mQ{8P z%$8x$(|j9#eTd^6b*}87b^EH>RB_rRV+I>laOFcUhr<`)hbty@EdcMn**a_rKlbKi zpB5D-RbLDm*>)O!sY=(CQZU`#gvAmCg(jV7o^;faz)A=IX^LF73lR-g_;s#VTfhFtQztS zKrcVd8uye+M)a7`+f;cAXNgnuoL)c0AidpeB1fFYF{CMSHU;`%dGVI4TZm&HJh@^G z{^h~Rkb}5zV~|j%DELlGQF;R8_5SNKGnKfvR&jC2e&o&QpGE)d4;-_5#&F%h*~eNE zadu?n_4O=x|E26AZSFLjgUcq>=>s-A93DFVCi=#I3lS~`znuvcFhkyLwchf*!{Bwi zIq80d;$J9jAMGXC zGMf!R}bbzM*Em6{EnMgPk`AML^8O`_)hKVjdNb5$sVb=2;dnoJ=N7`I8B z3;gZk=(Es;Pjr9R@B{`xcy-g9DLkqD!d7G9PZAdAHd-W+FIW+ zKElFkHWgsDXKw6j7{8N8kDZDJKRNVHE94RCe$)fIEWv5d+d`Hh57XT6WbbnD^K);S zOrh`7Z6{~H;Q8@37T!#Sz3$GI6nO+D=NqOXR4yX}rJvpHzj*Q^=R%)5^szVKeIPc1 zv7qKH4!QaFw|ec>q-xZ4-SMmz>%_H4EyH1^*T^UQ&udSG?y5HI5~o$|aF~n5b^5*N z&yjr2-%6YlY3QfAg3AgHkbYdul*9;Ck(l^kxhxf&(p%yMFUvs-qFpDn?>X%hb61YS5&KKc#w zhAnxSSPsPlCM8roa^OaevS(oO`Qf1|VICw=^FYIO{;Kj2^o0JO-y#-}P_+;~NlW}L z@g(HJsCl6BZx%2xSPYs~`y$Rg^ChX{6Mh%{y#qT&AB}+>Ox@f!MvBwCt)_(d@V$>T zPn@hzLS1x>?!Vu8b9LmLRXXNB&-Tt2xE`lN3%v{d_hwP)Y#X>h>;pd|es@EGZ`TO; zTa2Vu7xa_iY1z`{_#GQwde5DJ{)uJu7u^BQuW6Q5y@$Fh_qy2(uz9RSjW+a=w_R}8 zC$QMBfs73JtINNQcg=ylo^zWyVjbc7iPay3@Y^nz?yM4RM}4HjUORm-o879jOHlU| zCu_Ce5$vKQYh&~Ub!Y!G7M?kNaedGi_C*U1tiyH3s4H5Rp-&1O`3|5r7wv$Jj?@7B zGa2=;Ya(Fbb(~IDu#TXadGqNCuwlpX>c4#0=leBVR|iZV>rLXdQpVs#&#p^FFGb$G zO}g(dzV8b4#JV}W{;t>GpkALB=TX{!y0qnJ$bWk0nUsi%f5zH*`Z8X54y<)j zaNJej@7zkQzKTL+Fk*>NF%2p>&asCw$gkE0`o{?;@_X=taVg?Ptr#Mcqnzh}rq)5y<}?pR?V7I)dLe zEEmp$XLW^Bs<wWj1;Fonh0+QT+SL_A9=8ui+>v}_h8ydQ`xfltzZ!;hWb@|E zokEf{pO~{nCg1{tTLH|dySaSg^>p7^tdFGjYG;6d{{En$58fgzHQ<0cf-iyf8ZScO zXN0NC)nT2oQNFyL>I1#v=o;x+aK|G3`@-PoH`4s5`^{rAeNY>M^&Z2R9vap$eFGl} zID`EZiz~xW2NbU3w0aTPAXvxbx)eU&_K`(aF!FEN@y2&CwxX&0iBunG`dG`Juf0$8 z{})>EHF6DJzf>eofa;?!QJeKm1WfO1sIJF#(F>nX=L6=F@@K)%miRn)hu0U5FPM3Q z-(&mP?g4fGk&~v?#rXb5H@iQ`1NYc5`8A=A;?TMN_RaWyvkQM+ZN~V;`Z}!+2ftf0 zx7limB+a9{w7nCo{BqB04Xo?li);{3#_x|#sM36jI*rS&bzUdHJVQcqf54&h-c9v@ z^OCHeMB(*!I`4VOV|Z=N&E+d;9@-K(N;<5_K2Kdp@={f=i$AOIPto(wsUY zcTR#M-S)KH#C+A|kh?^03EX{epL8b~m^+$TM|i zIMxek8zUW1_du6jx~xWN)zVCGUEIpMuLjqToE}Y!jzFKsJ(`jE;KWV4S>MDWPIokD zpz4%9Xt<_UB%wdYHn**LxWCNoTJG1^&FBPE-by`UGl58SDIGls$)S$}oyX?~gx|=4qH)^co&psSlIR7ypl5y8J(S ziGGd#+GTQzx`U|yE4lhy<160lZG1-PELbpj&ui*Dh_Am)IiAZ)(j@&^tn$EEFAA6k zHh7vTK+QwF!zO_{P%rqqrBq`p=9Nuk#uh=;3o_JIdgX#22&)UUK#qs1N)tJtheqFo z2zp3M(lQ)7rmewq75cwrA-4}EuT2MoS1j2(!-D#T#K5=ruY$YdtJLgKA9$>C)%1O^ zxH|WY6Xe*a@onNq@X1)?)U%LVgF3$ZJebD^XDm~bAh#Uc>i4&Rc{f+9=|ir9KM5v= zfZOf`2Wmo22IUgB&&LuJ>XXL%&Fe;pQQ*et&$w->3vF|LIxt6_Asy zeiCLKuoJlq6=vpOFYcXHtewi$f`(`BJ&&Gtx5*yPDmk)6@FzRiCvM<6(XpQX$R z_fz}6!l(v(e{?RDAr9;QMFq~&;O!g2ip$QS{^*XO0x#-sHgF2w@K3x-DjDGY9t>|{9)z4m3%UzcZiJ#$;*NJJXWyZF%S@&#=3cQ1<%^o+lN zngUh-Gn%eYzcd}^po(rbQUPa2PsXX-K;6b6)kq8K{_#q-wOJUiw_?Nj;7;wv*oS$j zdr*Fy-3U%PBNDgz4*C`?2;po42bW)W(JDfpou?wX)O8m(p^;O?Scm0X+4uxpZ?c@l zrwnxsvpbt8)-Uj2cZDANTh^&m3vQAx9{F8^{=>KLMR$PLUsG#d2)!Qp*xhXy%$6`C z*YylOQ`zAKOA?_6_dKyOc!B(=(*8U>aL0wyV<({Z9QB1_&A~MS{a+)X_wM&q@tc89 zcIufozrnec>_=8^1#e_r(HZvveScnUE#LvGdOj`@Lw(=<$g(jRaF}h}>xgf7-V)8{ z#K5(m8|J1aC1^Ce6!#Ti*6MTG2Y$lNv}W<{0eia6lq{Xac-_`)J^}ubRCAyicJM*f z*E?syOu8e@5lpDBv^h4B1HR(w==G3Il4i$rQmq+mdaRIPhC`AT;L+dw2OM&Ccd`QP ztWRg1rvh|uRz_gzP1xazwFc@2;IV26>HY8n&hFPgrUm9ar!1%szv!=C>G}O&%dLWC zTTwqv*WDLxMU_^g-cr%0XUA?_r|a*YeasTu|BC~DT;+!SigsP_~VFecz@nrvvf4Wn%mGOkloGJ~fQ%eu@1S zN7}ht!R1>zJKmvwxYftfEgyX5f>&~MIo6pUS;Pc_8-M1-ij-oX-6t&`q4kMIF`ejnVHQ|AODT zncZ{H&)Ysk@dB7P==a=FFb9tqmkxO7^QG8&@Y5U1T+5+v9Qda1M&kXSnnrRrf={w8 zCC=&m(@7Nj?`?$+qBhC-~{K*E- zf5)l30B_}UbuH+^KH$JJW4FPJZ~Qz+)obpqlaD_I-tjt1ekJCI<I+Tft#-JB2l zA&(`iRH^fejGmQd{DFNFI;EjH2zw^;BC?kW^1VgOd7P?O{*u%6I^rw7uk0uvRo^_g z$8byR1nl9N_U2UZZ(7v3MLR&(}0J}LdZJ5o;avtRl@ex4WM=h5{9 zsYggXL+T+?Pmy|z)N`aBB=sbzM@c

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h 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 S3*P&=bk^1Ae|?$kzyAT%W_Sny literal 0 HcmV?d00001 diff --git a/tests/input/geol_clip_no_gaps.shx b/tests/input/geol_clip_no_gaps.shx new file mode 100644 index 0000000000000000000000000000000000000000..78d33f6fad506c30cb2a02c956aeb3c09e2ed983 GIT binary patch literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ literal 0 HcmV?d00001 diff --git a/tests/input/structure_clip.cpg b/tests/input/structure_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/input/structure_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/input/structure_clip.dbf b/tests/input/structure_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..7f15994662bc5ff545f2de71f06d3f62ed447adb GIT binary patch literal 45216 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 YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- literal 0 HcmV?d00001 diff --git a/tests/input/structure_clip.shx b/tests/input/structure_clip.shx new file mode 100644 index 0000000000000000000000000000000000000000..cb743f810babb48dc0aae0199ad824ac44b006f8 GIT binary patch literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu literal 0 HcmV?d00001 From 7b1140802d0d08fe00193554d0c0faa89b2b0c0d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 3 Sep 2025 13:37:06 +0800 Subject: [PATCH 043/116] fix: change stratigraphic column QgsProcessingParameterFeatureSink description parameter to string --- m2l/processing/algorithms/sorter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 17fcfc2..849a18a 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -92,7 +92,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - self.tr("Stratigraphic column"), + "Stratigraphic column", ) ) From 27d2ee5b67434fd5a68972b6bc1b63f6c6dda3d7 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 16:17:23 +0800 Subject: [PATCH 044/116] feat add formation column mapping and validation for strat column --- .../algorithms/extract_basal_contacts.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index d7beb34..c78653e 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -78,6 +78,17 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterField( + 'FORMATION_FIELD', + 'Formation Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='formation', + optional=True + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_FAULTS, @@ -127,7 +138,14 @@ def processAlgorithm( geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) - ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + + if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): + raise QgsProcessingException("no stratigraphic column found") + + if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): + feedback.pushInfo("no units to ignore specified") + # if strati_column and strati_column.strip(): # strati_column = [unit.strip() for unit in strati_column.split(',')] # Save stratigraphic column settings @@ -138,10 +156,15 @@ def processAlgorithm( ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) + formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - mask = ~geology['Formation'].astype(str).str.strip().isin(ignore_units) - geology = geology[mask].reset_index(drop=True) + if formation_field and formation_field in geology.columns: + mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + geology = geology[mask].reset_index(drop=True) + feedback.pushInfo(f"filtered by formation field: {formation_field}") + else: + feedback.pushInfo(f"no formation field found: {formation_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -154,7 +177,7 @@ def processAlgorithm( feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( self, - contact_extractor.basal_contacts, + basal_contacts, parameters=parameters, context=context, output_key=self.OUTPUT, From 03a6ffcd2d1204b58553210421000f3c84388ba5 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 16:39:04 +0800 Subject: [PATCH 045/116] delete input files --- tests/input/faults_clip.cpg | 1 - tests/input/faults_clip.dbf | Bin 49957 -> 0 bytes tests/input/faults_clip.prj | 1 - tests/input/faults_clip.shp | Bin 9308 -> 0 bytes tests/input/faults_clip.shx | Bin 380 -> 0 bytes tests/input/folds_clip.cpg | 1 - tests/input/folds_clip.dbf | Bin 9713 -> 0 bytes tests/input/folds_clip.prj | 1 - tests/input/folds_clip.shp | Bin 1804 -> 0 bytes tests/input/folds_clip.shx | Bin 156 -> 0 bytes tests/input/geol_clip_no_gaps.cpg | 1 - tests/input/geol_clip_no_gaps.dbf | Bin 305246 -> 0 bytes tests/input/geol_clip_no_gaps.prj | 1 - tests/input/geol_clip_no_gaps.shp | Bin 103704 -> 0 bytes tests/input/geol_clip_no_gaps.shx | Bin 588 -> 0 bytes tests/input/structure_clip.cpg | 1 - tests/input/structure_clip.dbf | Bin 45216 -> 0 bytes tests/input/structure_clip.prj | 1 - tests/input/structure_clip.shp | Bin 3404 -> 0 bytes tests/input/structure_clip.shx | Bin 1044 -> 0 bytes 20 files changed, 8 deletions(-) delete mode 100644 tests/input/faults_clip.cpg delete mode 100644 tests/input/faults_clip.dbf delete mode 100644 tests/input/faults_clip.prj delete mode 100644 tests/input/faults_clip.shp delete mode 100644 tests/input/faults_clip.shx delete mode 100644 tests/input/folds_clip.cpg delete mode 100644 tests/input/folds_clip.dbf delete mode 100644 tests/input/folds_clip.prj delete mode 100644 tests/input/folds_clip.shp delete mode 100644 tests/input/folds_clip.shx delete mode 100644 tests/input/geol_clip_no_gaps.cpg delete mode 100644 tests/input/geol_clip_no_gaps.dbf delete mode 100644 tests/input/geol_clip_no_gaps.prj delete mode 100644 tests/input/geol_clip_no_gaps.shp delete mode 100644 tests/input/geol_clip_no_gaps.shx delete mode 100644 tests/input/structure_clip.cpg delete mode 100644 tests/input/structure_clip.dbf delete mode 100644 tests/input/structure_clip.prj delete mode 100644 tests/input/structure_clip.shp delete mode 100644 tests/input/structure_clip.shx diff --git a/tests/input/faults_clip.cpg b/tests/input/faults_clip.cpg deleted file mode 100644 index cd89cb9..0000000 --- a/tests/input/faults_clip.cpg +++ /dev/null @@ -1 +0,0 @@ -ISO-8859-1 \ No newline at end of file diff --git a/tests/input/faults_clip.dbf b/tests/input/faults_clip.dbf deleted file mode 100644 index 35f7f0eb882f291f8a96c7470670505b2ea251e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49957 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

sPgE;L>n#AP!K$JA8#-%^CHcexe0 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

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h 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 S3*P&=bk^1Ae|?$kzyAT%W_Sny diff --git a/tests/input/geol_clip_no_gaps.shx b/tests/input/geol_clip_no_gaps.shx deleted file mode 100644 index 78d33f6fad506c30cb2a02c956aeb3c09e2ed983..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ diff --git a/tests/input/structure_clip.cpg b/tests/input/structure_clip.cpg deleted file mode 100644 index cd89cb9..0000000 --- a/tests/input/structure_clip.cpg +++ /dev/null @@ -1 +0,0 @@ -ISO-8859-1 \ No newline at end of file diff --git a/tests/input/structure_clip.dbf b/tests/input/structure_clip.dbf deleted file mode 100644 index 7f15994662bc5ff545f2de71f06d3f62ed447adb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45216 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 YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- diff --git a/tests/input/structure_clip.shx b/tests/input/structure_clip.shx deleted file mode 100644 index cb743f810babb48dc0aae0199ad824ac44b006f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu From e23f02355c2d81bf14da37f6b9955f3e4ba3e2ca Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 17:34:51 +0800 Subject: [PATCH 046/116] fix sorter --- m2l/main/vectorLayerWrapper.py | 15 ++-- m2l/processing/algorithms/__init__.py | 2 + .../algorithms/extract_basal_contacts.py | 76 +++++++++++++++---- m2l/processing/algorithms/sampler.py | 3 +- m2l/processing/algorithms/sorter.py | 14 ++-- .../algorithms/thickness_calculator.py | 6 +- m2l/processing/provider.py | 10 ++- 7 files changed, 94 insertions(+), 32 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index d68ed3d..72ff281 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -9,12 +9,15 @@ QgsWkbTypes, QgsCoordinateReferenceSystem, QgsFeatureSink, - QgsProcessingException + QgsProcessingException, + QgsPoint, + QgsPointXY, ) -from qgis.PyQt.QtCore import QVariant, QDateTime +from qgis.PyQt.QtCore import QVariant, QDateTime, QVariant from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon +from shapely.wkb import loads as wkb_loads import pandas as pd import geopandas as gpd import numpy as np @@ -35,10 +38,12 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: continue data['geometry'].append(geom) for f in fields: - data[f.name()].append(feature[f.name()]) + if f.type() == QVariant.String: + data[f.name()].append(str(feature[f.name()])) + else: + data[f.name()].append(feature[f.name()]) return gpd.GeoDataFrame(data, crs=layer.crs().authid()) - def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: """Convert a vector layer to a pandas DataFrame samples the geometry using either points or the vertices of the lines @@ -392,7 +397,7 @@ def dataframeToQgsLayer( attr_columns = [f.name() for f in fields] # Iterate rows and write features - for i, (idx, row) in enumerate(df.iterrows(), start=1): + for i, (_idx, row) in enumerate(df.iterrows(), start=1): if feedback.isCanceled(): break diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index f3dd275..f0aaedb 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1,2 +1,4 @@ from .extract_basal_contacts import BasalContactsAlgorithm +from .sorter import StratigraphySorterAlgorithm +from .thickness_calculator import ThicknessCalculatorAlgorithm from .sampler import SamplerAlgorithm diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index f329229..d7beb34 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,9 +22,13 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterString, + QgsProcessingParameterField, + QgsProcessingParameterMatrix, + QgsSettings ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer from map2loop.contact_extractor import ContactExtractor @@ -35,6 +39,7 @@ class BasalContactsAlgorithm(QgsProcessingAlgorithm): INPUT_GEOLOGY = 'GEOLOGY' INPUT_FAULTS = 'FAULTS' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' + INPUT_IGNORE_UNITS = 'IGNORE_UNITS' OUTPUT = "BASAL_CONTACTS" def name(self) -> str: @@ -51,7 +56,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -63,6 +68,16 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + self.addParameter( + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='unitname' + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_FAULTS, @@ -71,12 +86,26 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "STRATIGRAPHIC_COLUMN", - [QgsProcessing.TypeVectorLine], + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + ignore_settings = QgsSettings() + last_ignore_units = ignore_settings.value("m2l/ignore_units", "") + self.addParameter( + QgsProcessingParameterMatrix( + self.INPUT_IGNORE_UNITS, + "Unit(s) to ignore", + headers=["Unit"], + defaultValue=last_ignore_units, + optional=True ) ) @@ -94,22 +123,41 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - faults = self.parameterAsSource(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + feedback.pushInfo("Loading data...") + geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) + strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + # if strati_column and strati_column.strip(): + # strati_column = [unit.strip() for unit in strati_column.split(',')] + # Save stratigraphic column settings + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', strati_column) + + ignore_settings = QgsSettings() + ignore_settings.setValue("m2l/ignore_units", ignore_units) + + unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - faults = qgsLayerToGeoDataFrame(faults) if faults else None + mask = ~geology['Formation'].astype(str).str.strip().isin(ignore_units) + geology = geology[mask].reset_index(drop=True) + faults = qgsLayerToGeoDataFrame(faults) if faults else None + if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: + geology = geology.rename(columns={unit_name_field: 'UNITNAME'}) + feedback.pushInfo("Extracting Basal Contacts...") - contact_extractor = ContactExtractor(geology, faults, feedback) - contact_extractor.extract_basal_contacts(strati_column) - + contact_extractor = ContactExtractor(geology, faults) + basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + + feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( self, contact_extractor.basal_contacts, parameters=parameters, context=context, + output_key=self.OUTPUT, feedback=feedback, ) return {self.OUTPUT: basal_contacts} diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 3a6a1c8..f8b38ee 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -67,7 +67,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -86,6 +86,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterRasterLayer( self.INPUT_DTM, "DTM", + [QgsProcessing.TypeRaster], ) ) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 5c4340a..849a18a 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -22,7 +22,7 @@ # ──────────────────────────────────────────────── # map2loop sorters # ──────────────────────────────────────────────── -from map2loop.map2loop.sorter import ( +from map2loop.sorter import ( SorterAlpha, SorterAgeBased, SorterMaximiseContacts, @@ -58,13 +58,13 @@ def name(self) -> str: return "loop_sorter" def displayName(self) -> str: - return "loop: Stratigraphic sorter" + return "Loop3d: Stratigraphic sorter" def group(self) -> str: return "Loop3d" def groupId(self) -> str: - return "loop3d" + return "Loop3d" # ---------------------------------------------------------- # Parameters @@ -74,7 +74,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT, - self.tr("Geology polygons"), + "Geology polygons", [QgsProcessing.TypeVectorPolygon], ) ) @@ -83,7 +83,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterEnum( self.ALGO, - self.tr("Sorting strategy"), + "Sorting strategy", options=list(SORTER_LIST.keys()), defaultValue=0, # Age-based is safest default ) @@ -92,7 +92,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - self.tr("Stratigraphic column"), + "Stratigraphic column", ) ) @@ -177,7 +177,7 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: (units_df, relationships_df, contacts_df, map_data) """ import pandas as pd - from m2l.map2loop.mapdata import MapData # adjust import path if needed + from map2loop.map2loop.mapdata import MapData # adjust import path if needed # Example: convert the geology layer to a very small units_df units_records = [] diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index f56b09c..71f72eb 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -29,7 +29,7 @@ ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer -from map2loop.map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint +from map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): @@ -62,7 +62,7 @@ def group(self) -> str: def groupId(self) -> str: """Return the algorithm group ID.""" - return "loop3d" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -79,7 +79,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterFeatureSource( self.INPUT_DTM, "DTM", - [QgsProcessing.TypeVectorRaster], + [QgsProcessing.TypeRaster], ) ) self.addParameter( diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index b6a5a97..318e944 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -16,7 +16,12 @@ __version__, ) -from .algorithms import BasalContactsAlgorithm, SamplerAlgorithm +from .algorithms import ( + BasalContactsAlgorithm, + StratigraphySorterAlgorithm, + ThicknessCalculatorAlgorithm, + SamplerAlgorithm +) # ############################################################################ # ########## Classes ############### @@ -29,8 +34,9 @@ class Map2LoopProvider(QgsProcessingProvider): def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) + self.addAlgorithm(StratigraphySorterAlgorithm()) + self.addAlgorithm(ThicknessCalculatorAlgorithm()) self.addAlgorithm(SamplerAlgorithm()) - pass def id(self) -> str: """Unique provider id, used for identifying it. This string should be unique, \ From ccd1c93b5286aef86f70b95f104517cb10ca93ae Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 17:59:42 +0800 Subject: [PATCH 047/116] fix data type of spacing and decimator in sampler --- m2l/processing/algorithms/sampler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index f8b38ee..c018e72 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -112,6 +112,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_DECIMATION, "DECIMATION", + QgsProcessingParameterNumber.Integer, defaultValue=1, optional=True, ) @@ -121,6 +122,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_SPACING, "SPACING", + QgsProcessingParameterNumber.Double, defaultValue=200.0, optional=True, ) @@ -129,7 +131,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - "Sampled Contacts", + "Sampled Points", ) ) @@ -143,7 +145,7 @@ def processAlgorithm( dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) spatial_data = self.parameterAsVectorLayer(parameters, self.INPUT_SPATIAL_DATA, context) - decimation = self.parameterAsDouble(parameters, self.INPUT_DECIMATION, context) + decimation = self.parameterAsInt(parameters, self.INPUT_DECIMATION, context) spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) sampler_type = ["Decimator", "Spacing"][sampler_type_index] From 26233f0afd0179a088a6c50c3a5b26b9e9af2ff0 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 4 Sep 2025 18:09:28 +0800 Subject: [PATCH 048/116] rename unused loop idx in sampler --- m2l/processing/algorithms/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index c018e72..9e4ec6e 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -186,7 +186,7 @@ def processAlgorithm( ) if samples is not None and not samples.empty: - for index, row in samples.iterrows(): + for _index, row in samples.iterrows(): feature = QgsFeature(fields) # decimator has z values From 1709581bd85f624d7b4abad012b79bfec4e5e68a Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Sat, 6 Sep 2025 13:14:23 +0800 Subject: [PATCH 049/116] add dtm to input --- tests/input/dtm_rp.tif | Bin 0 -> 161316 bytes tests/input/dtm_rp.tif.aux.xml | 11 +++++++++++ tests/input/faults_clip.cpg | 1 + tests/input/faults_clip.dbf | Bin 0 -> 49957 bytes tests/input/faults_clip.prj | 1 + tests/input/faults_clip.shp | Bin 0 -> 9308 bytes tests/input/faults_clip.shx | Bin 0 -> 380 bytes tests/input/folds_clip.cpg | 1 + tests/input/folds_clip.dbf | Bin 0 -> 9713 bytes tests/input/folds_clip.prj | 1 + tests/input/folds_clip.shp | Bin 0 -> 1804 bytes tests/input/folds_clip.shx | Bin 0 -> 156 bytes tests/input/geol_clip_no_gaps.cpg | 1 + tests/input/geol_clip_no_gaps.dbf | Bin 0 -> 305246 bytes tests/input/geol_clip_no_gaps.prj | 1 + tests/input/geol_clip_no_gaps.shp | Bin 0 -> 103704 bytes tests/input/geol_clip_no_gaps.shx | Bin 0 -> 588 bytes tests/input/structure_clip.cpg | 1 + tests/input/structure_clip.dbf | Bin 0 -> 45216 bytes tests/input/structure_clip.prj | 1 + tests/input/structure_clip.shp | Bin 0 -> 3404 bytes tests/input/structure_clip.shx | Bin 0 -> 1044 bytes 22 files changed, 19 insertions(+) create mode 100644 tests/input/dtm_rp.tif create mode 100644 tests/input/dtm_rp.tif.aux.xml create mode 100644 tests/input/faults_clip.cpg create mode 100644 tests/input/faults_clip.dbf create mode 100644 tests/input/faults_clip.prj create mode 100644 tests/input/faults_clip.shp create mode 100644 tests/input/faults_clip.shx create mode 100644 tests/input/folds_clip.cpg create mode 100644 tests/input/folds_clip.dbf create mode 100644 tests/input/folds_clip.prj create mode 100644 tests/input/folds_clip.shp create mode 100644 tests/input/folds_clip.shx create mode 100644 tests/input/geol_clip_no_gaps.cpg create mode 100644 tests/input/geol_clip_no_gaps.dbf create mode 100644 tests/input/geol_clip_no_gaps.prj create mode 100644 tests/input/geol_clip_no_gaps.shp create mode 100644 tests/input/geol_clip_no_gaps.shx create mode 100644 tests/input/structure_clip.cpg create mode 100644 tests/input/structure_clip.dbf create mode 100644 tests/input/structure_clip.prj create mode 100644 tests/input/structure_clip.shp create mode 100644 tests/input/structure_clip.shx diff --git a/tests/input/dtm_rp.tif b/tests/input/dtm_rp.tif new file mode 100644 index 0000000000000000000000000000000000000000..a9ba843ebb8ff37ef232f9f26cbed3d5e2c1cdd4 GIT binary patch literal 161316 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ℜ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 u`9Q?|`E=UPCFX@l`u8i5mu_PcJXSd3UBZ!R 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

J@~-neKlvhm@zs1gyjwY}`PBS(6$ABM$z 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≪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 + + + 1175 + 561.72162965205 + 297 + 107.52060579962 + 99.73 + + + diff --git a/tests/input/faults_clip.cpg b/tests/input/faults_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/input/faults_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/input/faults_clip.dbf b/tests/input/faults_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..35f7f0eb882f291f8a96c7470670505b2ea251e9 GIT binary patch literal 49957 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

sPgE;L>n#AP!K$JA8#-%^CHcexe0 z5g=fjra@{1+jZ;>uFciuV*>)Wr^{zpXl4iadSjX^xVFg?pLoqBr1qMpjWzosWeq0m zz~i-j^qzwhg0hwDkRa1m5d?59KLW27L11T}dubL-=BXDZP;~j6@~0qyXl;x?25AIG z+4Px3!OG`8#C!(gZthK|DHk!ZAh27I7J=Q$^a@kKV6rSomynPl6h^LD9OPci)8&Fg ze61Jp7F^pU>Dpcd;|-p~j@MRkpRGCFI9%JlTm~6DF49H_ycGn9YeV`z51nhnp0K2AlS8yOU7HTVHPh8y z6`V*nb#1C<*@_8lo1t&`klK!okU0aapmJk|s-OxxKYxR8`QIn^5&`WuM*L6Clew3s z9{aL*=8+(jq&9eZiGf@~+X{Oa2~yWBYI>PMe`lUw^jPde-+?uQhycD12_cFU5qpy* zoWhmK!cdI%KPOLqh?94=i3Pl7R#(fVOm2jV+ri3vxgH0}IVg)d)h7gS5V$n1ZD)Ov zgmK57m6dcz=#oO))ySEZH!QC2iF>7AE?deL*Qa_BYcL;fgEyg zLuv^AZw~K@k{h-?$l-wMqlxL>C!i{FOh{tZu8zv9fZ>bPwSjKU&>TgzA(7`M*9KxY z%Q00=a~!=LuT3>IM?U4Jup+OHYo-KoZ4DM(_K0h{A+D{<#GyKbz^fxbT$|hwH1EPV zzYP%ACO00WYm>u|Y7Yy6W_=;9Em^0I9M%X?YlemZac#u4b()jJwRM86uGEvRO}_$P zPFIJdy9Nl{Hn=v;Fg05@O-m7h+}b{*ri+k5R0S)Tgf4Jx@&sTFH)Dm;iBJ5nbxdaW zJ?GIp2xcDpJ%v2Z($v<_b#1?bTWIYa%I@J)1n!_-943&;dd5NkVRA3Dd8L*vUd^(c zCm^%}|I&QDD?CsAMUdvbOWcO8YXcwHFb3yXFMVb_2S@}iap;4x5&UVVA+J9kH%A5h ztOfsZwWt&!FiZr9Ym*z7=3N+vYs0?V$@Q-P+N5jiA7i@0coAsU7vkE8Ya8!8K4;5` zYa1@thAQ_jLGGNcvJem(KwGod4h{Fa18$@5@aT#MROhh!c5Lh`sFZ@m;V42EFf_vL! ziLO}@CTj*?G>Gi>-iyFZ*JFQM$8rsn)UZ^;GELjq(Y48^rVzR|Od*=4 zVdqBp?Is3U8&)P#$eaW$(5)9wag=S?Tw7reF<#pTp+{qEZzMLx8X245&uS?f=+_FW z&G*tN7XrB61#RoE{|yA^`#5=e$dQDM&F{Et4m0B4W#YB9xwawd+SbF2y(*JELf|L_ zs{7VagzDV|I$DZVRg?+b^>|XEHiEHZ}V!FVH5op$zOJ&*^+vZM2Je<_@ zGazuWx;E@w2H5t7o_jzuU4(%8%?!uZ-C=WM*gCnMxHjcVH5~`&G+^5bvTcoUk%v?> z4Fis=ny#u_KBV&ApMq<{97N#SP$!sRUuKgBG-2YOVV(yG=drx|XfZ}Z=i0t5!UViV zr@>5!&MMtnkqKe_0fJs;@S6;%+P;VRV$HHmBhP;*9Y?{Pv^?G7>so$~1xeTFD|v%V15RwwobAc2{sn zf-EYJ-aSW;kdpHxX&Fo~)J&u&UHph)+v`JG1YbKSb`yY9)X%vDZR?(liEG2~A+42! zzcdQNBL;P}MQ+0DmT6IS1M>;@SqwwHfL_=S43gky<>Dp|^Mi8Wd70fiGjC@GtzW&%;8~Ko?Srj~1vH2u@vWCv> zkPoTcw~uPeAyD17cXA^rwyWud7ipVoGc1fiYdBUKP@x<0j;)Qk zG7l+)%Et=W_CDqQLkuA-ang{MBDkp4V98xmTSY#Wq@rD;@A(G%OW3A zxvyz&T$^IquA?g8Dk@w^t9KURL#mk|YI8K$ahxq(8-n*I=-LphG@yd64fP99D4qVA zJo({-VZCV_Ri;V%tg|Kzr1W2MqwWMkd(TSxz!RkR-{o# zR&kK>RVL!N1!&BpcgMJ>yj<1GouQ#qu)m+U1p2~)}Y@1x0?Lx)`*HK*1y|uYE+g9XLBQUUn z>hw&ywwKoJAD_eE|Jqj}T^s4z+VkOc_p!OQp~ugAU9``C^+7*d034&^+9cyklCDke zJ0Q3_cC*Uz)Qdt0Jn0->Hc+G?)4ukQ|^(jZ5t8#ObCnzf$F|B)E1A9Ym?NqVc+eBI<8f_NY_>&L4OH>T?B}0Bd%?C zR?v}`5V%;b4O@qXZBH`{1OlWPdQ@Xz7Q1~-OL6vo&v?$4;17hfjfQJ;4Db{M|54o$ z?xNK@OTo3NK(^_crDzIh66XPrzClR0hbyR26_Wr9tf2aQYMP3wle?Mx`U%H-v;+yb zu{_B9RZwN7l)z3@g$c&f*hBLo#n!d`t#bODJwF7Dr-k#UWS{%1UX1qBbdn)m0(-Q{ zXAx;&$jqBW0O5wzD=yev8}|CrYv(V(IB{(kFv!%!&ImN?)#b)(>kJ-MyhsH8K8sh$ zOsJYarFHH1>DtsQ-82-%RZYu<3FYnxxEIF0uF@cYr7Q>@P}nwD^AMRfhSH`6J@A@Z z%`$L%fe==$5NsJ_9bF&wc`}bUL}LS)8x!H!cnsm#K)@CzK+M4{CJy~v1ZZPX7=uTR zFeP%9io~YuUUWVWd2(IG$`Tn*r_b!Y7tO^DqvrXF;BM_!9ooNv;l6!80k#c?ZO4AM zZu=}kr$RsUBQJwscZWJe#|eQ8Lg3Q4Htf5}1x=M+x=LJIFR+r)H-bQWy}DGkZJQW* zBSTBw=m~-C`69TsE<{>S&FjxBwhj&3-q3Ypz}KKsA=o30&5dE(>xpYqt_;Y50OXlr z>xR9-wJC5I*l%0cEePe-(zU4uq)d>;Us%BuqG_5b->6ovZYICo#Na|YZ5@-@z4cOH zfo{EcisMC!&9xQw1brpb$yVJDiy+Mzn*u-fqRXh@&uTWVqS|~fopRx9+Pm}Uy2WC& zKTe(=av1SeCpzyoJ`*^3{n}g`&jDHnzO}$cg02ym$!W;zkH-Kwt_4 zmf?sq#I-eZuhSlJZIfD!)=oT1T-!0ziMY07P@*!Iia>SW8fyQsb*|03>zd1#?D&p7 z;@VC^l(@E&;G)78hCs8v5Z6{Y{9V`-pzfZC0C8AkM6I|PD@&GwB zakf9hJP#7iV}|3cIdmV=uZu8&%%js_CVWV%cx*+;we<%GHkLuqPKe0%JvR-(&d2#OeZztkfmVU4O10l!k2sJ7A!S{71<)vOvKNNgkgkY)uZ7JFG1J_ZoIE%PD~ z(f=OKW!KGT$a~}ap;r{H3qA74_Yy(Lfum``4At@61Q&PBy!d%O^CBL?KWkg1WyXDf zl|tSGwpuPBxY^Q6^NjKPhN{~N1SdxO&Vbwq8P1Rc6S|poHVgi>3KBnH-B!WPJft-+ zHQJ}{lo+2{>u~vHHQI-Z&>2FYHv}$?Yuj00B$3^*C%Il>-|Zy3*L!truI;0j&I4Ah z`pc9*hH&%9%Acx4V(;v`m&R}h6&GeVt1M5wNMxG(0J(yW=bPdA3TPc(Y_WYlvF&NLiW(Xo z8&qf?w%aFa6S285Y+SK z2;TnkG9YL|Al`m%hi|LXB(UPWB3VJEQt?s%@~a>fX-SLJqFYRv1dQvd#c2Pt80~v` zYm7C&Olt)mw1v-2)dKCV9Fl$mFh9sHb;V0~cQJ z(zv#r^+j_0ckD^7SJ-zu$?o-D9h+-Al0xgfUvvRN00V($eZk?{Fxqr(8{2s$&wYKm z-nbCBSX~=--cW3Nnqi_+4cOBX+BfLuVQg*;+g^WMo2r{OsMkzLh9GhvlpEwmP;?iP zBDj{KskYVT+Ehz*t8(3^iLfHCj%%)O7*;pQP-9|a?`D!sK7aYg#xa=>U%3Ae1(1U@ zi9%3X&C?)A3awVw%_>YV!Pud(Z8$Nuo4(uyvoH>l+)JNX3Vmm6mCVA&5Ijn+ft>K8 z)eL^8=TSK4kWW)^ZUwov3}Rx&N$U31O|Wracu(_gdUr)y@qYwqffr_t(VNb zl!Hq7Q_AN-^32|;mZPx=6YtLK*8BOIZO*_#hHbAWu1&czK+t9y zx})d@xQbSDNN{bM3tvb@QBBv};@T`#-d$6*EL5c@GIruH=2XssB(&2^GAZ41OJ#f$i0}S%LNCj>Es5SRAJ~` z+oq1Hg6pI%Qei^i%_30Ux87_Aw>q}<954OL&6aukcp?JCwVepqR`Kok)UDI>kq{U? z0^7XE=sQFm*g#-=4cod$ZQKxwfJiu5G9-A5zs;RCz_3u!1Q>Bd+c66VwXSKM!}na9gF{}B|30BGpEM78sD{FsOXd4jsNL~DgW&Uc002Z zQx}Wl7-Q$Qs7j2-&V$vfXMwfHe>h%r|L3#pYt>H($n?YsPMx(#Mk}8Ddu%wD9ZR{Fqz+(=H*RUF>6V?@tMA_h8W6X;)0H;M~dfmcx?% z47$_%g2^SYkH@08jvxko!a`0X0?fYlc7yj)i@|zFcv@70F`gQd(W2n^O{)wX(Vwpt zF#nFDEz!o;1a&d~?6&O^UaN8p<7@8i>&A=o+4p5QaOn=;znI-VG@A$i{QiBPC2AUZ zIq{}+<@^p77P}qnYqNQ$y|Tp|T>Hf$LxoVZdk0fUYA*PsmjN>h_jS!c;6L2K z`+P?~)`aT<&+*MTesg7{Nf^f9L3v-}Nw7$hmVv>wf7-f^G3resG(tE;0Xizp&Q?d(N}x5^H@5{I_4&_r;!97QG?qf5umoxQ{@kZ^y6WT-!9o<9^r&yt74KS z^Y?e8pUGj+-TvO5Y4~h{er4&$w+ycdhtvG-zPq#a9rsLgI;h` zVnP>e(v#hm`IJFh2CRDK3f8Jo{q-du&yzn|GWWpGH|qplf6kzjMeS}Df@?$W86GNN z&>0%?9?jsy1>5%P65Kfs1H6Yuz#a+mGz=u=Fh*6250j$7HYcQNsK@7_c>30j#B|k|2Wp>D9UW z4VqxS-*SS{n2(#6v^BbdLlyhnbTD74-kur1238fgBy}8n-*Wf;`yYb~p3F(Eh+)v% zB;)N~g1MA^U9006wC(BC=FebV-lVUelNogOSy2xGwC%`x;k(qS z2>mps80oPdT$ZU9`3d7T{g=Rto#1cDOu;&gv(ap>7xv()5Ke`v@_+7?eebom{5W>t z_)Lqk)0(xoAM+dQHi35@vyq>HIo2IC{RIQuJUPg$L_aTIJC|xi`|c<@>+oY-YM1c0 zz6HyExK~$@$DmU^c50P?vz4lSjWNz`J|?XN;F48i9G@RCXd4xS=c!=-wcb1V((oJ# zY`f(KRy?p(Y!dyEcydB>1^D39Jcj)Z20h#KsQV&tu}=J>VXW`69j_W^flq9nawGK$ zo)7z8H%?$I6@FDYYQS#T@qznnCD{1+orLMw7d5K2Odo*R)8v zXxA6}(+z20{jZPH20R&bUP@ZSP4I$N4jUIdrz#yHZkz+N+r|EFHnaB$avvf08FC*Y z_o@H2j}iMExet>2}1yqYxC6vFxoqdP5sVV-tZoN%84X7?3)Kd_lC_EqvB zgU)*WOS%&CD45C6vrlEvS}Gh9N#Jnz)6GBe%qZvh?Bfkyu|U1&D)!xx_@q2(Fi*P6 znQrXkQ!Fwxx>@~~R?%07anF)_qQRPXWdCDWU~6;!$Nn2Sz!#UoFZ;i2>R2DcurmMu zS?>$~JC4-rq;FF5|2cQg(t!Xqu%uL~xH0VUt+~>St_8BReAw&kTCi|($d(6S>04sq zTIXR`onO244$i+}@x&+z?6x?k;36eU`>nNJDe7ZNd+G_^I8wEuH5`iODS2p->YOrC3w!Q zcFj0^{`Y9}ij5ac>2$q&o~-NLI8`_D87#S@x^dSWS^9L)g{-X?P3gSIV`I}KW$FD> zjm2()Kef+^NuMc87pDiDIOJza|6M;f>g6<9`s~8SVR7`$ixZO;T0*k)s2T6oFfh9> z*?p(adq?9l>-^cD9vb2JdGQ{fC{bD3d|Tsi6|VP?rzPh!zE3|qcvl2ieD!5sWwifk zsQpz%uw3Js`jhC7>nlcnRHHrK?m;8c=&$^UIU{ahd8bSh6Ioe$eXRLU9&qXL#gkfc zvb1Pvs>=@CpZE*6#Afiv<5?@dp+8ogGXKx`D4$+q6bYWHe~Q;|p)B1zr_o3o%w^!_ zDkm>X?{V(P?nHlIms_M+2VI9DFVF^W^GMw5N1+H_NWmD&?uzQbo}VZC_dM2kUNM zd4W+&(I1!IEN0mg%ha2TI6hMJU$dvh*TCGrW8(B1DSGo>`R;tMA=mVhnih&~xx9pb z1iZY<-go`B)Ix|bnfAA6uoLazik0{<{Y!u%ixM-!%X=UQ@W$& zZ=xO8VELe*Gx$oNQlB_je9(ef1}++Gjs1x0MeW;@I4{+do_|hH_X^l2mB;@mxbVyA zrSoup>+LbYWog(qM86j|fq$Ai+`kImqobi11NM=3O`VZ$N+0GlQcS?-|E|d0q6bbp zk))7`_E+uCv3CGJ%g_m*+D*|5c#`^3!H(Y_jX1PZwDjT*`#$h3g}0t+Z4{lEd9Z&G z;t&Su%406z*T$Wx7T|5A6`pB0|I|#y%U8fIhxZ0VcTjZE&NRi_;N(q@B3aiz%c1q{ z9+={E==e^9~Pe`8m?f7$(=qL)oGT2>D35Zd>j;~|?7lr)CrEcuiSSA{Kxjpw|iifw-+|=0mtqUDqM@} zr;jR^4q!fxYjdTW<9-{S+)26u{(R{tk1N`@`lcDb9@uF1A;XzqFFz(z930K{&ioPX z*P*)4uN3p$P=9?=<`6}Hv+GNo4^B?AKj{Z9sXUNai}iEl&;v~qoUd|6Twe!#^znv= z3&7E{|M+o(s>v3S7Ic1^U7E+dF%(N77jM1U|qfGvj^+3Ke->(699icz2{`sdx}o(yT$h% z`%zg^c)WQvMdymeSpNdgemW%9SwYd)4#r*_0$WcW+hbcs(Z6Pu={A8+J@a^Zzl5T< z>Ri8f53FW0v*{T2zX-L1C9}Y_+f(e$Vt;I%wNtMN`=_3JT;bst6umvXKRFYu{-m-a z_8CQg%3Eb)1rC2^m+zKG(f4P)?H<8?uW6uR>GzbP6YWhC%fY_a-Sy^y*E(%r7J}C_ zj_8_$g>`k95#T_HF~xrD--)w06nw!mZl_uLV!vlUi^%5<9z()#p-NcKtu;A0J_lGf z`A<0w^$1fsMR1|@S*&IDT4g=ABv}80Kk_m+-{`ji-#ceVyU*Ywica_` zm$V!#>>|M{nN88N&vo!~fxnF|yB+wDqBYB+lR6Tx531JITRouY5IqzAB5;9Tqe)i= zMZ2CFvk3#ANdGB0nNHC^cHhv~0zd8=sv8Ao_cbS$$77$HFq>Ties*hSk3X328kg}E z@P?&X`a8e_o44x*rcw092N|}{;!Np5&JfexV1>q;J;%W7V_PS#rea@T?W(^7d||db zV|NP1mG=!(0=)0d9J|BG6y2Te{iZV(?GdMLNhD#OD!sD34&Hd`m&?FCiq;WIwOs=) zKWVRe^e#nceAMeLw6dfR7)m@G_)*Agg zo}5Q1x})cC;zJgvY}Yh9LeW8YcKcli=RHb~cxp}28V2k6{lG`InT=Liz%E!R+jRu| zEHHmi&kWD;p9y?Bz!&6}iMi~f=$A#)x;BD+xATuF?uH$6qgq}QywY`>TaO{e`v+@ig$h1fDS8h7>AQB|!+O@1e%r8*B<|{Do$s}#k;iQlMR)N#c5Vjql-bAV>QZ#* zqL;7Dz;g!Dz3kS*Zn{vpF%Ue@W4c$g21Oq)N_)*}|Kb4gJ=N;?yjbsoIB<(3m)G%? zm@i!7|6xnjt?!hVW1p=(_2n*(S5My`oTW_BwNvCf9)r(pavO49M$uodQkL(*uC8Yz zHz-mx|DTJmTEMauYA02fQgr>`+5hm)c{YoR7gO}m{EoOP9Czw}q4H=E?5t;t|8uu7DEi#VlzCUcy9^myZ_mQ`<@Xf$ zfM0$%WZFIx>!Bu&>lj!)`037{Z zzk*gTgEqMuDdP$D`*W0Lwm>#Z6X?Ve)=U3?ctwf$<=lhU-5swky zl*=^D1pj>WE=T}zugnI$yj5Utsb9x7;CuzYvcF;2TkHCsTK>lIJ*yY|Jqk8p=EjC0 zp7J%d)pQ$}>(i$)JzS5y7w0eX-nbx(LFf1{ioS;9!7c%g+3>SfYJ5#v!R8A)&i#g8 zEpBpV&<5Yf-pl)!_qck3Gi$P9LlK|KuJax%0RM3k;KD{l|dI4JI4%w2d+I$^}&2HU!XDh9lTa0(_rfloFBY_ zA&mWLs6u+j+24puEEImB2-eo9QZeJi{`7XdW&%kgWH$_|T z5Ad=F2fq4SpfrV|r`^(*JqgzA`pDzTkMkexJLd{+Qxx-66{cw2WCIIFFm-cJM5qY% z>!sg0kAa28gtpqw!G7v0t9=5T86-}1U_It+7X9oDR$D5)W&1q%T}KudI)G>TuUB}6 z^?NFKULmVJ?}IjmJ1(GTS>yWotmDH`j#r+{r*OY>C6mF)1;-3yWhpv5c2N5}m}Bba z_1-vtb#j6BF!-3=*7@(SU&I($y&41`JpDs%0rsEj$dC=6!9VD`sovPHRIe|7-3k^G zDRkvqP0__m@)Lf6+hqeC9%28JD(dSP13UA#yFJGK_%`Wz$0+!~3T>+y8*sn->Ujn5 zJYoJYYS<5Ue`tSqDmZY4?!iquxPQB2Q!;V>Ow(GsG3>VnPdM(q0M~2FwN_(4HWcK! z*aKEvclyZrE%0l-$8~t{`8yH^i+^B05C6$iECQ}=>e-!PK+$VnMC!7*VsvuJ>75kq zxMHG{58RYfS7*PMq7_RXUu?qlT0(^9EZU%@|l9M$#BDZ1xlvix7LmTmQ| zm_x9m1N3cH;`4U;UwjT&V*k6(rEnH(a8Q=xJy_lEy?5 zDjnd{cSMrpz(J`F6_!>M%@A&5>VpGLm(DB0^KR8>V2>S`uiQ8BtSv>`TVK}q10U82 znKQ+nqAkZeZ6d(}Ll0AL<9W+Dk)UuNJdaO0Sn3q^=fMX(@4%ZyI2oVtybj+I@P-HW zfnVlKuRZP*{oW(6XE%65_6nwv7e!muDkNVA_pDsr_S>7HMIL`=z66IdRBxGHfL)&A z+cOpRN7C=-lg0tKK6keNUU0?V<6HHD;fDsE&@Tj+kEp2fg;4Y(ZcqPj;2p1@cZ7xE z`I~XS#}xL?HuWzWmUj3EdPP(8w!j+#N5G-$4pwx+J{p!UOiBSOe7xNy5BsTh({+VO z@UZQjEpNeMSr-*n!2Wxhb>Um~9gOEzPTN56*3?v$SFse`wkWNq87#o*B9;;dyZ>fN z(h}I04u9ep@8c;tWnCA)57?gTinYaET<`9f-aGKov*yfQi4-lb^^1Qt?Ai58!d@D{ zUKCrIEMNz2jJ5T8luFTpRShNK;5R4FCY^*m+WyR2fw2-L%;2L7Dc;k=J3x1Z=3qv^#$y2yNiGH`oOD>b!o1Hy)Kn&o;V+V zkW$>k$vyBBteaA z{u;2FNTb2LVu}vlzb}ywepyOZ^!nRxD4NSvPfrtU`0TdANICo>ll0>4;IE@)IT>#$ z+W%I6rvdoos=3v??wT~!g_vrs@o78q)}I()r`1=%QL>a;5SXFd&=Pl?r(Co=>R`% zPR?1|hWYSp7ynfFrRwh7owlFhAL!fiuK^E;2Y4;`g1D0PF26HiK7WhJL*T|^PcHrh z2k0!dTa5G1_kQ}h=eyZkE(N5G|Sopoo>-Y3#K zdhdcA&b~L@@d5Vv3B%rnEWbWQh;sq%_gByFWOH!3cIMSZ?=fHcn+0RQI!h+$@EYvz z+@F$5z){=om{e53K67sE9Rs%{C&`RfV0_-*y2Og(+`jDVV}bF=`E4t78m!vkZo!Jf z^`Dv{914Ek(x`U&B}Gr2qh5X={EX{jr#}2rU+x9=tT;=;3y%hs=ZN#v7xcA)b51Ym zrZN6(U(i(Ral0n^gUw4e?;JhTPSKxdSZ)7-^Dk#yd@zpwc%}4k+W^@5;9u89-2a1t z`IRj8_*88fhVhJ(kT}*0K6B{TY(eyg+s@v=MsVkG{q_&FugtgobW^%ssWa;@9Mio0BuT@!N{*E8jEyp#q073w4Ls)3@p zryLOq0duOhY>vSA?Z!9%_-iuPo@>{|hT#;>g&2JYcvrYr*YPUkCs4c_OazC{k4pLIRwD|qK- z`7Ki515VINOQE zT>h;2rv8{Cej|>nOdjbj!F){hU$|(vm!hc$Jxtboo#Lro>2lb@uf3+nH=C#u?6|f_l^ly>tF7u3SqyP)tlQ@2A-?!6CaE? zONVRBy%cb~^Qd$H=I@G&rh2yE-Q3Z$=VHFzTr$$F1D=yHo^w8rqVL_^+Re)UtBG$@ z-GTkilp1Fq!v6QugtNTx5&SEI0seX5)s%g)JdP*xIoXV0KlK>gCKrX{Q}Yh$6@ya} zK6|~!{;4H-_9-h){^>z@zzOh$pJv_3V71uE-P^F=O4+vfd4ON0MSU&DejL_vxI`PQ z8)WZ$4V-eL*{=)x|LcSqTdso5i@6ojz+^lHAr?ruc)b3duQW*upQ40w|4!FGQ6los z+GB-eg7A;^N9O0^h}1 zYe7EnjfQMGm`mx8jA}cB_IP_pwiX;|Q);OMzwE<)t8=XQT1NYaoHt$2w;cM$DT{GC zdd^Q=5`J81+JJ=(_?z^psBZXq`o8HeSkIlE=C?Mo{J!ru?yOscdGt>GzFq-%=THR4 z5pWle<%mu{{EBR^b3FOq}x+^A3LWo8dCXu0=+x6gsAx8#4_F$}-Z z_JVmm__k+%eIL=6|9ph`bmeS;72@JulGC)eDITTWIfJ5HP_bD4(pPqq4e|a zUkrNvw?3^TaEEDm;rvlt|I}@cE-+v4lVjh<81y}(S#5Gy*X?4Zl<7Ewej#CFVGs6p z+CKRUd|&NtK{z<^X=LRXSn_qlof2@%`@JbEC*g;kS{Jtg`-oS*SFZ+mo2cTU>tL4y zU1yRe7_>$1tGEx~WBr2d?f87nvdF`d*e8vJc&#JB;e{>Sn&58biz?5+VgrZg90qGP zpQ+l4>xJ+KA9ez3d|qMd4!`Hu$)FAP;FT*}CZBL2&eor?fOY=dM4!Pw+!Sr$e$2`p z9Jgv^$5z%Pr+$poW>fw6fMM+S^N!b_<6%(DKPuFLC)pr+S3l6$J}7@CsA`=O{3_8KSP8vz_$grsxO&}ePPC@BN5=pN4Mh+31D9o zJ7{AIp4<0bMvD)2-nEe8wct&ci>>xdp=du^zwQ#gJIyK zt|c2;@%hrK1?DpNe8HjQ1RpRp^}^3>@au+dpU`mvYnDl!Y(pHR?%?6OTftv)!+$ml zQ*@+Iez7{(LUHN*^Jw3(N=e;WU_*av^B}O)){9f>FrK=C8pgvoUvi~WQ5M+$TilN2 zxL%Wx@J3(oncfhqbGV;-`4Mt7nA3~jd=J{Q*n4AP5ayFZ|2@w&Xm7A#Yk@5I(wp3T zU-a*UQ7z{y%ukJ@o7eYXJo=_yIA{U>D&{nQ1mmU0Ur-5JExfj4 z@k`7Ew1R$#{lKPjl=%?rH|W<*E;n$K_RDk`>W=it*qy`3kz^uih6gfY;!&GhLo+t7g#D`4DqIuu^aI^^1u6eEZH=$Fj>sk^&-!5x1CX-Em+ym~-ms zg6+`7?6vw4BM*MK!LM)#aS)%Gub;_+d!KI3anE4TGrvx{Ph#&5nVS*x6*?fXBY&HI zfIZ&76aN{*pacGT=eK~ZC{=6{)i#by%cMi_~#(6FM(a z2ezEhiIF-oQfEf$&~6htHTHP2bxv$%>x<+F{gED_Pa^e8T!g-f)Zge4`W$XTzeDPK zt`hnmQXfR>he-PcCJiLSTolU=Vr!Au+M`af5M9nP#{d5;Vc9`6{;~eZ>##q|)-jPf zC$pATk zMDSLw^58A$47xyIpL7*CLdn>0F4jHA>=vmpFmGQNuN(OI+)GyCtbMY_z-tnpZ{p#~ zR{~$tU3N4H@!T@GcY-W`_}r3@=NIDqZSg6+j^NXmRo~4;yp>ZuSmP2{ufJNM1YG{G z=0+a)l-sZFbI@Tae=@oB13WcNEWZbA{iDTk5BBLn&OISK_`ZS?t8u?u_k4-j0&dOTDRmQkw_+l}6P&#C=&!lBpHP`D|8HQ0$B|;FRo_!9PHz4Hk&;Zu{xhY7k~bv)`s&1K5FskK7$@nJowjDaQs=xatp+> z)BGFj_kq{S&N+7ttZKn6D-B-1KmM;7cw5YMzxQa*%lq<|4!lBtul+MO47|v5Q+-@1 z`ZwBOpE8&|mwkCZJSoEcxi;>KKZfIUT^J(_obtQm+8=!W2uJC1X>fEzA@eSDvI5s% z-FuJzRQz>)_{(z!y-H*8#B%VD)=Mh2Xz#9u868a+ub73qgy-Xa(zf2@a{?zkFg)Od ze1mxQo1xj@$Zx(2b#ec}5g|%pm>=8w*4EeK_~kttHSEAG0?f2W;QWN!XZgVgo^;lh zg72#>|NDY9|D5JNZU?`8XWf(vCUv`{uD6cR{gS$1Qa4QMib>rusY@o;KemMFY~3Kc zZEOv-;H3`BJFwx|W6#=0*t$W|zhQrtt*L7m0dpG+qtfVqq6o z4nDs;E?Nuo!04!21@#g32UVvL3+{~F^wJsjjF6_t>N_&X-t{xBTD$@;cX(DV9I7A?7=O+#vL{t;sL$=EZOJKXH93NKVe4Pl+b!uRB<=Zd4zA@-S zoCjh);(l+B>VJHI_W0&>N}mNE_*u%#?1ug4B4D-FB?j%Up(+ zU_TZaewbjbi?vfl(xY*|b_Wv7{L%mIUD{W~nz0X5pJ}PZ_*v&AOrAu4ivH2pG6MI_ z*(QCtjzQlTtMq8YzMs8-ArM#to%F`P2abcu`Glz^f`2xw=i6#p`H{tP_)hv~(=VfM z`scfw*sS}gyb>he;s3=yM47$a1%b8@5tY_5XV>NNmj9RQ7-Ufr0hdRg{(D>)OVsm~pl&(U2%eQYk>wI%h!FYLf=+=hE-{ykl@BgtXT#0zm z@!xN=!R)rO$DM2sn#w9!SjW1r>T4;})-9!I{olsRhVfaB+J$SKRR7=mVx9M&KjPT6 zmE3zwkQZlvwpjVT>r&*w7l)P%{KU1`*Jihayf&^m6DGty*7e!85nIE_)_Af%%PK4@ zVRbV8F5$m96+djc5hG)tA8<0nqzkNTrDdda9$K9lVJ1&#tlfVNlwl%N8TZ5aO`@u6z*BQizJ|hQ;8X zH`W!J9}!~@|C=cn0AJ)i=d)16Xdm_LRsIN;u)gBF{vBebbE?`#z$!=2|Jhj$tzu-D z2z3e1n8iFv5{PL>Ec~%d3H<$MHcwC`a%R%w4(q@bMvqNymmx-Xvt_yiScIo1qV^SH zgi@gyZ^1&LFB`fGkfYQ2v1|-%c4LN~{R_mzpXh8C4MhLMXM0K^rq@!pAw3zaUuWqg zm4W_$*_{3dtlQ))E|fyiu}%*Z=LMP4d)-@IXC@-2L)}+g1+L)pN>YtS>>!LUZ7+D! zMNRR~G4Q3QS8NFaU#?aO-w=)d=zneh68tWjv6Yq6azDIOnDa7XL2jig16Pp)OkmVW-KT?Bq6Y}b^q5wVf5?6?^r z$mxF;9okJJwv(lJ-WjaA@?m0%xMZvUKJ2rgZA{QW^y=zua2G8HIhY)cu4ckDX@rQh=sT`#xLmd zy0hTgnOu42=0MA;5Kgg<8-A{f3z9%i=k&ANYH06en<15PXma%We=ru}ergUYhdUv5 zc6X=$?GHFUJK=3L$26>;qYG}QgEOL?U!E6)#_ZfzPfIZWZz=5<#FDZ{9+}C2-+vu% z&_`^q{rLXLd-y&-x9arLNyJY2(mhS`Usf>8_jYg&Nt!m{W@23++S7U)>NJ8(Mr_1k8SZwrB)0xcr{A({&BFmf3Fl z8ttETP;K9JaQyLLgBIMcn62UJVXzkG@Qdb4u$8-wKP`pEDaouy$Nw^e)~l9y@*T&= z9`5zgxyqo=o^E~;3vQa0EcOune^R_IY8lw1KGrnuHiPcH*zVeb_EeoeqiP=qTRb%X zQ#Y%<)(fK;7|)Dsr-o@b-?5-eQ{yg!Zv5-gumybgmbu_Cc`HP@sjo78OoFHJY0`6e~tq-LDdl#`lsQj<<<)=5n}sd*z&XKisWbGYUn@85}k+pqf?H?JBVYBmU&R@mQY!`(x#SECT^a8&P z>CeH{oqG+X8L~8w^P|flU{&6PVmH)O7nkNMo&vA>IN`W&7HS38n{Q{;aCpvAxurZE zHOn_&R82s${^Q-n@Rg{6zI5b!dOg^xDJuLuY7K82e@=f2-uY#R$q^1&`na2@h&8zH z*OcjcW6;ED-4+>zJvYH2c}d_8{N3Hxl~|hkq&2#Rn!liRI6m?19Qe@Guziyedplvkk25ZoG&H@$HH{_)w5=Y7Cd1+PT45O2D;T%Uz@Z+)0=L>IdiV26Mb`D5QT7!JO(QKtrEj3Alw2X~2e#^eX3z5)H9DnJ$}Pb& zBO;g+&nbEb_sPwg;7?_b47u{*kMHtctN^}jJiVm(3C1I*T~HEy+Ox@WEEigh{Dwv1 z-~-1FT5U$WCr_|Q@DJ?uC0da(-49U9DBRxr7;N}jK>8IlxJY(wasr=~w(#OZ{AXHk zkpEI}ap%fqQTL!p&butY4_;reFHSKIpHB%B5Ci{^j$F@;_)oK%w}K+LW#?5di5nFC zLFK!R5?CT!%Xc5*Kea;FljealH$Jxfjrh;Vy)Px4;6%Q0?)RaHzkHbC-voaoXxZ@k zWmgd2S;%dB4b0s!+f@HD;thk%dON_E&7}`Y1YrKG`(Bb5Zc4lR^Z2?VUi3(Sw)p|( zS!`x~%!i_v|2Fpf2v(92X>9jK{Hsi~=P~%pn3-S);z>VNOcjU*t2~cAxy=Ldytzm8 zJiw=?Y#&+d22EXRW1>0uPyAML!*xZ#a~e`7XV_tW^TzT0LVTjt<6ac+5yY=X0*h#@U_pLPQB z3&FXEkB#Yqr5z6yF~FhQH7rICLDQ%j)Abkei)QgRt<~Vj^ZB~@;NL47eMeZw(>tfI z;xjecmg|))p{bv@kWT^3*D9L-3g6eRDWLlW@rWkZC6OP{KBK?Vd`aMw-)HV;-QU)f z`MTC%o;z`|Gtpn|g<-`q;Lx&yd0Ob-s^{u{KM}83C%JNtJH}(W`!Abh@ZNmg04`7T zXJ@Ld7g)Ogd*XJCe}VB$0Uhwk_i|efV16)vIhW1_cZH0n4h2#4)3dZ|FDf-1dZjlpUt)i!%K9y)s z-S4-fh|hlNFv;JB{UnDu^GhDMTc#&32m6tIh|$nVFhg-)=lL&)uR4u~cq9J%z+po5 zB=*Oji2@HC5pOP=F<21y8}ZolMIjT2XD6OGHoAV4HUC#PH!O~#cD>}3!+Q4QpX)W3l9mZ#WygG*#|Tt+Znhhje^Z%x7TCiZ4o z4#unFv(=_VaG2_;R8frAscU6NhQNp7FD>%}H!V;-BA<%**s6m`OIhQ0K5k1gSXXB#jZCt# zmrKyT0Qb?LUL1cn{5@bJ?#H%CFpc%@hoL)dUN6CN`LBgp?}q4&DhObm@9S@{iS_+8 zTmv%Pn4hZGFZlC=U;Q#nvPS>;7EYGrqy2?N@`B$nzs0K>n08=KPmzE$@S)!?Y%cATF#;{<&mQGFH9NpIr?2{-04Mp}cbozH=hCBafjQt$ z=ZcQp$M}5_yry4(@i}(2KY;b_fT>fn1VX_dYqfZ<525BXecXmsQ&KDb+_wkziiBuO z_ge6d9mWTFF<(rs>?m0P&h}~HX+{2Ocm|jL46vV$RB>uI?15Sxe%8Cq;tTJN`oo^- z+wi^Q19+ibRNi;kGd<;pna9D>>cX1(&Dc*DIVY-u69UFAM>aq!dhyOhVesYY_oq46 z<9TOhV)G31Id^i4oet)Ie&=#O8?cPv@-Mt#ciF=J1K|0lsza>xbK;V)|5vP!jqMWU zS>@Odw2YbC!G8i0CB3o!q$KJSd$8VA-^`yqqXhT+JwaeUI9jjvKio3+)|;+)tex&uQ%oQ|CPIB=Nj3$3|88*k(Ic_ADKrX^DJZ@hRoBDc^op& zL*{|VJQ0~kBJ)gS9*WFUk$EgK&qe0J$UGUDMeZmdw+Vd0aBjOXh*eJTaL^CiBc>9-7QklX+}1 z&rRmR$viokM`OKF9fM+=oIOpbNNpZhN&1SXJIQ zvm2b>vC**O6W(1AwbseR7j~5WmH1XXr_DUWs|a?*uF8s2a0)({xJN2yC!=yWaC^yo*C_Q^*^zW$}XY zM(lgFH93v~ez4oC{mcDOt0S46lW`CneQjEaD)zBk@k8me{E^3&7|&~mAGctmRu@CM(z z$eRvHRi=SY>(`lP$KqWH7N=5wgZnhQ+f;7Bj#k;ebVDGtgX{UnVk6<#7RK101HW-> z`+D>$_W9S6!Y{$|-UC7yTI?2P8eFdQ}p2eKm0CW?A9S7Qb^w9y?&8 z%^UErdHjQw4)D`Uv-w8BPA~1$y^dghm_~FlFT?K8Sf}P=!K(dqzH1KlQR)6VYK)qN zPz~Q(;2GNM%I_Jm{7AV4Przwya~x+G;yK7|?b8Lml8~Yzxe2wa(k-85VCSd&*qD1> z7dn>mzWHk4WoI%pXKg^ih5wgTVsFj!@{4!V*yr9um^S_0!C z|60t4;Q(((u=lB83s-fDKCB%&kEJ7+Y!$d*wF2X7R$q_@-d1<9aDxio1;f#2@fqx) zK1GIa8SekbFefkkmZ1CrBd^8qTSBxrSAwf9ten1bA!?^yX`G7zpPHC*_1ApVYStN_ z%LH2%Mx{NP2fH-JobwmByMOi9^$hrhvyEgg!>{_V%XVnjT)a!<&Xd+-;9qHzgWlq( zH4J)Wz6PA<)(}k1#P~lL(h>vf+t@I^OoNW2`0#wbxqCw;NDjMT(Kz>tzF<0;|{K}GE{iLjrHSrzu6U>{<-^WHU~bhlf2d(YUN9@enj~tM=Wj1?z-WjCevb7k%zz{&TR~ajOds(5M+Fy5#$T zT~zf1vyn%ialbWM6xW+S(%MTQkDh+G(DVzAOD+5$UklCP0$r7}Uf{nJZ+le*YA76} zG-5FR8@`Ih-hj4LOE2~Y4bGTCFIrxLcbSH7i{=3DUDjh@jXZn6ufVYnn17Ef+pcFp zdpc9U-Zc*#OC1os`GP?|i*;@A0P8P4#KC~JbkVx1)my+jG*$-h!n-R1Hn0CA1>VW& z)<2MgcjKCvtmX#~R7%M>fcMV&^JD_+>FH)EpBZ?!#czG7sKq$GPHIdz724#+%-BC2 zIKE0k*fQikw5Y*n_PT&C%$pUr4|&*zqhYhTS;vKZK2ad0lX!k|~y{gjXZD?>7LItaQ0ivto5u%DS8 z9~WJZymNuW#cO-OMNWSn&BHsmYuCPTX~llH?Vez6NDT|AA|Thb!2$w&P_K?r+3tikAnTCuz;YAaw<#?ts)Kkh%p@*FfqXNL>V}n;>-+r0#;$ zWstfJQrAK1K1f{%sT(16C8X|z)TNNR6;jti>Rw1)45^zTbv2~!hScSdx*by2L+XA= zT@a}oB6UTi?ugVSk-8;P*F@@`NL>`En<8~pr0$B;Ws$nA|E239bYG+{jMR;hx-wFC zM(WZ?-5RNDBmFhFaANTLBw2nRdoN+HWw!0eezvjvz8Cn%`XisKQmfg0wqGyM(k`NV|r#dq}&8w3|q~inO~(yNtBkNV|@-`$)Txv>QpglC(QX zyOgwBNxPP`dr7;Pw3|u0nzXw~yPUM!NxPo3`$@lm^czUOg7iB`zl8K#NWX^kdq}^C z^qbgggY8$5ei!MNO80Y6B&>eshuJjdr9)rn`tTZ2tSlzj2rH$Hwk<_h9NZxh7^ z--6>y>Xf4{&%u$6 zcM1k69jk<$6297{fgADm(k~(dXJOYwk4{83#vy+g;2ks*cF%`e-S{t2$WKkY4GaaB zSPXb*fo-xxg(JZ?rGDgC!_OI7r{9+VE>cJ+xDG#uH%YbcJ^1kGwuamO@N@L03ax=% z6(28@Tjm8nNNZ`SFPO*0I%$C`Y6@N-e^UUS`6S586MoA*fpEV6=%<4@)}KXw=XmP$ zi!!j|vQ1*^Par=sn@d*y4ES_WQ9*$j-qA3UurU=J)m1u}whMN0qjUTOICepXUBnjX zt@97?=)ew@3_QELNDuMl#lBw-v-t7+riB|RddklahupzlrH5u)z%MZ=G(N=AN7tOR z%yrSE=&Tj`+_~VtTtBz4Y8q0Pxg6{UTk_e*g{;Kqiw4d~!!F+7E2NnOyiluK|xVC1bT@3Fa;axMi3_Lg6&rTbD4Oh4M^Ec>^gAK7q zI(d*U36Szw2Hwb@Taf_0@1Kfq<3BMT#nTo^cl|(ZP+nLQ%P&1Bo8TsbTAk$lwzG;j z|AWtfWeaL+S~+X}4&%5>FOTYI7yN>cHv7}Sp2}xGxT2P5v%8W8H~6RA@*PW2J9DXF zb6zv%Pr$Qf)RHE=D}Qvp#uM<1Bf0zbH{u=t>rXaZ0l!WERjviT^5)E%yz5{;B z{1rvB9$`I8eG1o;fZlYU^^JOMaL~-Y*pNF6`rPEm?x)xfRA?I;P3ToQiYIq#fwksu zj(ik@-ws$BdFmtfkJUCC>VJcKq68w1!Dr{alVITU%PjBCF2erfX;nAe6pMFF#DqOv z39cSJl=BeZS7CGYMilldt_No))$}yoP~_TkZ~F^jzh+I$T$!gCnDoWWSohNLy>VRGLA*YxyU#e z87Cv{eYgPPhG@;Bqi-;^VNvrUrtO*is4 z-y9G19~XnJ-*V(sgDG@Y8S0eGMbt$!3;x|V2;C8>n1*JZGoJ-!%v3A|e>g5_Td z1`=Goz^WIk7MFDKu&B|6ny}W%w8e4 z0KZ`}&3UzI99Ztk8o5H~^*H^aj%R>1Pp)ek6_cfd$^&Xx%)Q;tYiI`a5&iKhm%ygs zZS~$$W$D&~LOV8qBMV=zr|>%)&WAgyCBTc^>wN^F=le1y^L+&Rsp|PW7E$=^fMwQA zGg`s6SN$DNAs-)7sgs$_;{1e%Q;?6ptM_pz7`#_!`J_Jb=Z5@BA?v{-b_+~YknfHS zHy>hv`2EGN#^RKg znbRFV!vFXkmsSJzdG?_E_dCS#>sr#v!0(59*93vXo?R_uar=w+TMofLTA0S6SPu46 z&B^&xi5lUc*~i|1t=?9tHJ3r3Xy)2~7o2{p!bPtH`W+9Ssm|a{GtXvO!M|y0=?&Zn z7RroK%zc44|M84+IdGWA6}4-~1JS&OTV{gm65qT$@dSB@!^@V;0RMcmH@Fpfpu_=> z`%A#jCpXI7%R>LkUN18Q=Z1&}gk{2i87SX$1pNC0SH5r>^dS26rN_aOD*W27lcDcw zm`FYb4!T~=ymk+I3M;W*1F+VOUR~h?_(yv`1t^0z4nDUCjlnxFy_Ax>pg%J>c7Vqr z8vf5yrP7CBrbOERN7jFU_4tPW|9GLDvT0CRm6R40tygGiOIAf88d@q!R1z(P(4f-N zpfpgC(9n=nC?k?eLsUk{==Z$e|4+x~^FO~G2gmU^?$@|q_kG>hb)D;Z1{hpK-0g+w zKpgn;->NBbu&1Qj6vF+%Zr8WyMaRKji@WD|9=v<4#{%E;$RE_u9XJDyi%@i#bq;YV zuPEsNu)?7@Hp>{izvThXR`9E<%^JUu52$@scVHQ~>fh_u0dVmr4X1y|PkXPC&S3+d z8Zk5H5jgixUi{iK$mh{j;!Ou#Ma)SqXdZ`tD$97jWjOFLq8R5l2ahk+uRm1wBX#34#40n%l1cRIZX z_eG&e{~|bCM=wDM=i@H&v-TeN;ZONwew?qa?v?Bo@XnL-D$-9d`A~^GL*QHH=eMhe zqTX}fVaLh1zEO#NX_ol?oH9eT{NN+c17DBg`ULd4h6#eLHP4>g8wq^uLc9gw1=Tn2uEhQO>z!QJ1b)YREz1P=ceiCn?PsuwzV!Q-7#~`*&N^~p ze2xSb>TSgMTEU}}GY_mB^Lx7}#-qr?RpD~rO-s8HZ(zKd@LJ@Uf*bs~0*sSzy_Mw* zkAbh=PTiw;8ToQXN9(e|e4FKS&!=KMd+Irrfz_-mc(bp-o=&u^e+ZUwNn86j8{_e$ z8ZR>+Xuk^1o(}(mMgFJyYVgvMQ~~~c%opFQa*DvCVa-DpMR;C4U)86B&qz9bKXV7Y zF9sYR#e-#k-cNW8|Az7vwdDn1|G{swx-lPQuDCsT2Yj?i->4e%h4QLLkxc$7@19Pc z_8R2V-0WCB1M_QnxU7>r0rg+$y!?oWa8K zpR~_lJ}bMvU)~Np{rryNh0k$3bBWwm@Mpe-KOA2m-&yh3Co8aI#6jb4ov6=s3cS7@ zyl}Z!TgV64~AbpH1rMgUu3-d0dRQV&yr7AKYqoh44(ymY={&5^%MSw zqZPxI;O{oul}`V`?~_a$o{#k`*k#mh-#@HR^DYeQf?1nS1@tia>CM&(yTDcTgFAVU zFF0wF$H)=z>F(u4V_5H|mW_`jf!A?GpPPyG@PNpAg%4nd>)Eom!HT(jUpTN{Z}+eD zt_9zyQZQcu4*X)(X^UPw38#Ne7XyD=ll58*>uYM^5``_`r2(v+df?9mo16W?PYdRK zUjhEor7}_o&Z`wpeu4Lo@3I+r1Ln3a++2?JyJ?=ac_Y~4?u*iKtoI3H0%!BVLRwcA zH=&1^iO{2hNU%!Z+68OT^X%U3x{(BM+9b~GesF5?!|A2q{ROMiCW%R~eog8xcmY1= zHoVvb`-@L8h6SI&1&{ru?=F&H1-tzyn1Q_v-^-$%cIfGrHuCSxe6alN*nuQT~U)G9FhCJp94`~?~g_Y9!-ShIe- zJX2@z+BQI71n1EmpWMXsEq=+t<#HasBk|jxC(pr44W%FVqK?5Hx(AJ5XvYdJnj^vb zwo|kz2YilYA3uMZ1WQ&wJl_eNCGw#&k6(g?#wtw)U>*0}$qu;gzr}*hRKU^aPgmq{ zq8FQor@S2a_stb|pJH5`3r?K21Z$1D& zWpgX`Mae6|&)9c-Aa1f47Z-0C^!1eRz_MlFq>tjU| z(_4XC-Baf*de3c+Q}=iVUQ+quV*$p^%OMBz=ioDCk#;|Ey>!kL=J$fPS3Tg^hU+;Le?IH2Q%jw3qG z=s2X~l#XLM&gpYNpA-5V(dUdlhx9q6&oO<@={!K^2|AC^d4|qIbe^K~7@go{HK z={|t&6X-sI?lb5cj*j0~TD##gwyVO=&PsYA3UgEwSql0l(y>jO*?9&3HUER#U`+1KDZNk3mhkSZa z7X1Fp=bieGSI+(s%rNYGKSk`rO83c}W^{8DoJ~ zcY|L)GU!ElF!oI=b`2@)f?s}StdL1BzjeY7A9=Py^&$A8OO5wIu-ubvtt;{RqVW3Ja^ROLb| zp0~p<|7XUp#AftXjq=;ODiqvlKl$)uAqm#L(>>31!5@9iDvPn+x#_y!%7x!Oe`M{VzT39U1sbttq>h1RjqIu}|8 zL+fN{9SyCsp>;U4PKVa<&^jMl2Sn?HXdMx)Gop1!v`&fEG0{3FS_ehzq-Y%#t+S$a zShP-y)^X7~FIopi>%?ds8Lcy;b!fCsjn=WzIyYJeN9*Kh9o@uUdZNyb*5T1QJzB>{ z>-=aPAgvRmb%eCekk%p6Iz?K?Nb4MF9VD%jq;-_E&XU$)(mG9A$4TovX&or76Qy;e zw9b^)q0%~4TE|N3TxlIFt&^p7w6xBa*5T4RU0TOW>wIY)Fs&1&b;PvJnARcFI%QhN zOzWI!9WiTh$4%?JX&rb*NKOgU8{|RC5kV2yLm#5j1TOYt{pR&v z@e}r$k93mMDy+ALn|+>ifxTnt-1$wfl&LFB?T~e~0w)#TDM`Zq z;E4M}A%@qTkuo-b-)8&N-TOCzZ&b-DB{ahCXzXWf1nyB*Z!w3TdCzI@sVl)EoyUf` z;0LNmzkN*{th0M+cuzHYd;FGhVF&*doDou4fjpM02c3uE=UBaxv%uh!&fow=Z!H<@3N;(U?>d6LOBkafK#Qx;=2Xn@H>+XYJ zP%7kWDC76iEq_6KCNyYMx69E zuz*z6csBBoss(Nt90mUo8SUAch&nr~!2US!g|z8nci@+-eB-jTb#XtbrpVp=LPk?V3AiH&8_fDme)!0GJd~Xf5sYq;QlPx zt6aYa+;FHo_hk{rmv@TuKJdkA`6;!CE2zHg$zl9%hx^-$VoKqc7dZB@9BlhNFxMO7 z<<9$+gUtPIu-?6==mC0@?vz;W3ick6DXfK`(oXeOB;#kxag%c~twnE5@ybXauwl}Y zkur?eM=KvRc!EWWO9h>tpjV&pA~}Y4q{`REVf>qTWiIvw7iXl!eZ%uo6P7J-416F| zSw#naNcGQWoQKJaSpwhY5T=uR|Dvtq{o zkG!`RqKBYkDEC!#499)`+8IX=!;kzdf^RvF?;I9ev~~n_o0qMmcY}w<%m%-L?|Q3G za|J)RG87hw*Qcxv8!7?c9n<_6_XTmNN?!M`V5(z5buOq52Gz-+IvP}GgX(Zloerww zL3KW;4hYo=p*kWH{+|io-2|IVjTGsCKZyG8;L@9i_~{nL4>)0qz22r8o(J1%;=L0- zL&gOdihs=C#IcPDzO#66UyMpWPWc1BV8Sl%|G2@nty32MNRNSg8-HRPhOax+-UL6E z)Srh+?cg8FuAFm#op8)*fzo~Ob(39fnuw1!Dlhtv=e-&d8h~GrOFr@JZ5%gN<~+O) z@zLGYv4z=SnMSS!Rq*q51~!??fkm~GV*KIf+orBxzz_CO3rq`wAFo-p-JE&O&y6p( zxiW_5W|{N!a_}A(p32kTQFkP8%AL`VSomQ@J3GdWv`We82(aH~(H7N@s9#<+=oSce ztX44!?}h(c8IvB7UR(b~zJ&oYQM-l8WbktTX2$G6tuBf5=Oj zJ7S)&IX-RpFXA_;+KY^jm*f0Z+Xwr=4OQj*lffI!O3wC!d*AaLFGRf4P+~?T6Yr@J zY-20Hyd>t4tnveF*`?F5_Xg^jpQOB-gwOx9{bs5r)*-LukrBM$YYo%h_ha3;k^eB9 z;kTR?Y2Mcm|2y+J=NI1Z7m{Plj`-&#{We2Jet5fV~83bTv{C|15f*!|0=`2{(IWCnNsdt7&KtZg^hDyAt#Ef$R#WmEd=OjU1L^ z9lL#NXZ>Pu(xC$Z{g~$qbIo&1!JAK-C2vK1bX&Y~&P=fQ9=qRdSl0&nLL4eb;c_wB-;nPzI<6*>KTm3xw($c;A<9T3*KYh%J;D}GzHr( z7VFK#x|ZfEQf~m>TRyJ3F#_}Urj$BSut#vc&|nyJro$F3`wc%xWUADx5cok(#bii# zfFHK8b8a|^eMDWfBctyzy5XPt$O**bM>f}A1c&y7C{}@UJ3^)Xz&D@1jcLZ~4;z|< z3xWfhKk012=cQ!8tbw;=e(?XFeL2dAFM{=km&JIS*ucm?={ zUaj9rKg8`tGmfc)HE&;Wc!A&h<7vWW1u&1!(#{*+$O}28VK)t|yX3`U=L688zqLcF z1O8a$gnK2+Jy93pTxMGaUK3k-_pm$m4Y6gSF<{q>TeJ4Kq8?fI^`)cWB@f$#bX>3w zhPC}+>OJ4~dRa9)L03y^)ujk<%h#7P2OVL5vng920m~bOl&FEvcZYAc2ZveR7#-Y) zynyzBF>Ub84V~?u_9AZ}@SGYa*yB=m(;WxcfBQ6E)ZyM9^7LulwFmchZ(ea6_*Qp+ zbI&gPUe~l(rk*r0KGiG8{(r~91^om86+85{RT`ah2FKU_;{3RNC-OkrM_1K=Bh|_q zW`YA{ZnZJrci;Tw(0;IVrPx*nFn3nrWqUAx@yylC^^uf{wn*Or-I_eZu{pSY(zpJ} zzO=>ln& ztSH6xztfm|SqH2UB^W5^hCCG!>rPd$d7~ZgSKPnE8BIYe!JpTtd*rslA%2NxXQHk^U$KQsQVrc;eaKcYD~z56ls^B3BEquhM>A&cr+P zKigCeTf;syoGQ-5N3vEZD&N8NHMu$OvLK%QS4jp&Z*l)7=7Qz_be_(}czSy9BWnz= z-?(f0?*$ltw-w$7F?}M2?e#;pIzzYV>Z{7j;8*nQ*{WdvXL0?9!LBz>I|YOHU%bh)8(h6*tw$jIM0qAx>$Jg% zzT)G|{(XJ!YyE}biSHhcVs0Zth#)-esIZ zGT=!!|At-zA86d%u8Ys_eeCBL16JN#_HoWF*!j0lWQT%9=}43;g$|i%{it_@#8N@XP|=&^$KF7JiXYG+}&t)tYs?HQme!_Q4UEP@V*^ z^33cK0r30A}k_~-9-lYTl;6Ky%TRqLhyssh~ zE&>kL?^o~2z__*zGY|$J+#fpaWg2?gstPUR0qg9o&z_QmxM2%=%;%s7&EjRXRTp8u zh5jDU1ncQ+H1SF&~fttEvyZdIMhQ zbTaH-7UuE!f7LU=Kc5!x*nrR2ZW$Sez46mJAWRDUrFZ^s>sXx6+K9Rt;K%8)nvY;_ z*u+K}tOdL637h3`8uf4utLlTmdkRh-t__FZE=$X>7yR(;F4?8}pu}KIbHGQPHv~yR@(m5IwMTDwuEZ+{bkquotyW9X5gQa`4_AUMp6NUfASz}QxIPOGeyxV-Nb21H;7s37P zwiVWR{rTR$$)?~10cR^_;P^VR>P1FigV$b_|E6Qz`<$br2^Lw+xAzhc{HRwl9?F7u z+G;NPz=rFkbn>z&*dgMQ`o*8fz`v)D!NyF=OXyu+wr$Tf0 z(xH(z(X(x6XvF$O_M8qU*qqysu1hg04upu$nb99(GFzRaE1+$)z=a zT)?MvJldY2-$m(@%0H*Um-HgU*F(4E-kyfCVsONKo{JlCKX|M*9%%y4G+$I~U4i;U z(@f27uuN*soRiRwum5l-2k#{^BAw+c|-+sJ6TLb78%2w30XrR`M<^ zdN~96&dWpI9|y~twkGAyfo@f1-lPcdSN7Dvm9w#4AJdG#Bca~ZSf2j)pNqg{!JY zO$+vkKZ-1!?t$gD>l(kr{QJIht>IHJ&(X~0J1$sXB;PIL#(KcF;EBT)%;%;z^q1L! zHCgtXuOGzzqE3tF%xJIgzR+7Xy(Y5^>**=iAuA!QKPS3RevAPpus=1^@WK4*zGko=+);P` zA#?uY>my|ty;Hs~XTLK#TsOv=2ZO*{&n(|RhV|&+BQd*Dur}+Q@m28oIn^$A!C#vS z1Q@%+;Z(ovT44FB-pig)_3Oq+ODr{{D{vVuTbZ!V8QM@E+kL| zZXHZ0&;i#5SGkvghhO?TuxlWW_-l^IVQ}*sb{%Vsui~fQA8UcT`NTD(S3v)^%e)Zw##aK_4e;>%&1K#$S&3^>r|C{U`N#=SE8oFyp;rWq& zw2!k7*ZcfcePI&;tlw(WGvdIWAD+I;;>7(weQfbj@GqyOcINDOean-s!(dUNExGM~ z(a$HyIM@wrQN7#$Xb*IgbRPtXgX4v7>zj0;mtyTFmN59+!Ypg^PBGTCEt^?$!5RZI zuwzF5C5^5icJS;;2bFm-f8@Jd(`LqtUyAOnr>IB%=_e4_f$`M)gX4@K<|8q#>zen$ zai5gj1^Qa8r)#C&ZU5@^}-f4Ejfy{s6ok8rE@ zID`3rFHhTleEdiAH?tGyE!k38)`H`k=AJq}4Z2V+oF(ml!B<~L^ESnxKK%Z+H%j<@ zjf+-}HztX()|qlIH~>zK-Yq&A>zB3*cakMo%B!wI7X7X?pKRZu3;wz9yWiK_Vywz# z*2!DJuIBI2B^a+?Y`S{~_()h6rxfbpAJyguhk!$cMz;5%p5FaHOke_7ZN$8l?-kZh zb&m{2Pk8y=+3ary#8~_%PapdTF6>gO9D+Uo-=!R#OSsr#a)>nymYC&!KNAJ)_T8!C^1drXoyYJv|JbWc73uH8QDG#&H#i}9gF=fE2c z6g$^q{N(OgRapdXS7~g|o&;UPe{GfBVEqi_K~9Fwo#mm$pTQ!J|AYr{K!3tWZv8i~ zT+RN$MOY6)5=1h%G2Z1wxks$H@w|QSY4C@*gw8!Q(~2hnMn+v$lF< zmD~ec7KuML1Q$-{UeEN37w2ovbejrYyqoTvU%{@gBE8k7VZT@BT<6Sx{wkiwg898U4&Z_nB9&fPZx)=9;Xew7Jyytz^(Om_%<`MyYbP9e z>#$!GFU%eM3r;`q(tMXJULU^WlR50E-#JMw>EQJ*Tx5g6#!^OY4&VzxzOs4XLjw2P ze3s+-)JinAfls$6wsGO}lttD&=7Rn9a_*64!)4;EKgLrdRlwrkpDb1apTBd>#T?wP zeON~W%y#hU@~z-t|6lH#!CFFlWOsr&w!hAc17~lFmfZ(lYJ0(31)qQUP3JYH=k>QI zJJf5L^SL}vAOO7a(wl%ySU->45otUQ_8K-?{2B9CM#=Q3i{N`-SdXLSaXnKnMWurI z3nz`_DTuR-l#*Su!M~jn?lxjS-MY2@`c?4#e=BunVg23Gqw|F6S)AOZzf&K)pvZox z0Bo7dUbzc=;n$k$CE(@U;r4sM+I7mN55VF#cE)W5Z~JW#T@U^=)9<(_xFuRA`W1MW z)50WQe4p$d^A#iDw!9?KczTg@2&%K({tGUjK1JqU&~W_m+s{E%-d+=u3^4z|Yt$ zwtIp5MN*^Mz^8j_$L{0xOMY9J^1|P7=D_DH1!d?YhH^}k2QT0j-8_30_BVVL`6^)N z@(dLpRjfybi-xCyCHF5?ex(M#lX<*5^M2W_uQwlny~D@OnLiI466r3N1#UP#8odFW z^JkZl4S3R#$@1>t==j)=nOM&ngvy62!Nc-rrNUuf8TD|wKLy7rx{unx{>qyxYSs>B z^NQn0H9#JQ=?AyhV81ktnlD&y*DYZ;YXdLbCBEkh*5lA53sdIxa{F?V%HaoNC2owa z0+(O0Pj|M5A9ZN+ihJNgWAm){?tz~#H%qPwtWkDcN*L>X)QS5h{CTh+(zI$>;7M$o zhUCGlkqn)Fu;q!HuKU2p?tiT~-~{`6XqIa{SYzqU);as(=d&w}x(Rm8`tdIY`wfB1 z9iN|pwL0#UsCZ(3IAc=dU+`$WM}jH*WTz{p<;h%!za>O7&f6RHvJUKW8^QT9rRnWp zSFYKgJ;8iS-vlo65ogU<-|{&T+)}gMYCX7F^@Yhju#~s*6kTwx^9{KcaJu-J?RT+X zGt&7Xy9mBBOX;i+p4dOFe#0SW2$qyw93+bU)~V5%p98_%9g`z?1OM08w!-XO`p;u{ z{SLiTry1XycE!R}#82BNAC+a^pQkUQdO8g8>4PD;eBg_Tzs)_PkazNGjQ=^lf2C~p z_HAbok8XWoR1FS!|Lf?z3)ru#FNwSh#{D|a_G21EWtW5{^Cu%7d}3pyHh7M6 ziJV$0_U}pE{L)~nzicmBGof!jcEDK(d?4Waw>8)Be)Uk9F2K_f!X; z>f}=$eX6rhb@-`HKh^Q4I{&l}0PPb%`v}lJ1GEpp#8{i?Q$YI|&^`yW4+8CzK>H}r zJ`1!D1MSm5`#8`(53~;i?Gr)!NYFkLv=0UCQ$hP!&^{Nm4+ia%LHlUXJ{$D;#Um}s z#x_}>iC0u-OgB1L1Rt(qFSix;q_Q)=)Vvw`|L=wFz)S-8ll$nWlJOb!%fq%Ke=%Q* zbGPJv|A_r$=IYTar znUBq`EByBEguislhg>^wlJt`R6>vd;{ikJM_b<1;yW#coWqMo&F<+MF>z-#mf60{W z!CY{jSGIJ)AokZb#V&5(-Rzs>nDhIyvcyOqyjfRWE*Zb&xTIh1bnwJ)8{O%4mFZtm z*?mZMA&$Sh{F|5QU$H1iU+*;De>`_e*nZ5P4~O5Sz62+1+nmM3Q%*<956;HxXAj#+ zJ;MH@r{hnSGuTXdIP?H`hOq6sDDdMIU!^3$&9RqCc7xS)xE&v0zmwwA_uU_#m(npU z+Xp;&MZv=g=YQqIpp+2!_)_<=@P4d!Z+_NRVt>{zcun~J2dua8D*J-K%DpZjrd?RS z`L=T_gUu%i*0ywD{f}DC{T=(^$v>0R4BDZ8u~o9~E_j9N+lOo~aXoDB)La1TnXWn^ z_8j>{vWYc*V6FNokByp;H|ton*9Lr{plf_}J?!!N)qT?7PdDGspH~BalZIj+vtM^! zb79r}D)?(IXiG8ihBFGXV@oQrUe@lC@&c=GwNFrbfV^Ol2YXk6eFR?&v)_Z>M#@(G zN#OA3b8k$(i|b=$-}egkiOh@o!j{{JPxR|&G4Y3UvMOuS!E7siwF<$aE|=3bmg0Q! z&GpZMZ5O}!P+9^V_Q$PKUf|{%|7QAu4_U9&cL6_f+8NEPf96kyYE8jBuO7{pV%~pz zZXM%K5O!_JS+&5))%uEpo8q>fveARZ>_*b#aZ{HpMLxEI#z6Qj0$IpAv(|hs$H%ckt+T_J&Fv|7iA^8?FYH%4`!Q z3mmr=|0}f%ES&$hIJgY)fh)#+GjPAGCSMW~!SzmD{=H8ZoPJsH`v;9dJ(@{xu$jw#{1?5=guaK_qTtoq#VHqQy<)YI{^D@M~i+Tcy~Qt=T6Ko zvNHyx*zi1E<6XMfAFO7gBee?58L+m-9lZP|OWz!v8@6;AQ&%8f*e~S^RxM>qUysjM z9Czxg1%Kd;oK=VU!%JpT*a1Axc~VPe>EgJYp5`((Jpb&Qp2Zvo&)mTIXb;%)t6~@z zIBc>1hDTu3y%&GP`ET4HX(WpIWI&Lsp&Gm`P}OKLxSV^fY7Dr4lGL?j;E{m`MO@$j zv5LW!;BD8N6#wFS_;s7|>w;}84XoL_VSil?cisvv`&u?Q1@qmbf+d5y!R$&M8{*z# zzKzt23A)LC0}I1@bV2C$9!@AUDF**zk#i_ zO5bbX_f0RjyZaw_=9vPC>zMy0n64*SV&+3<_z$x#8S|~z>P4((qDJsul+*BubkuD0 z=PzJ-GEBy6Ci=@T>(h1o`;uQ2@D2W$3zt5(Cx>Fa(Q@_qGzsyx-11AUVDu$p`@{kJ zBga|e4|wOAd3wuvVZWc;V51g>@fvb)P+}&2kErwA3*bFGyB-F~{O@;mEM3u}2R?Xl zTgPP_7Yegf(N;y?jHz+Q4RGE@)$rTvV6VDPyWa<%!+%RP$PD&*W`OoEIC=VN_ph+$ zY?9fJrGwcn&v~=M5&jIDW8FKz@4~fD)VgAR5WUhP558=n%gf~PJP|&m>jO@o;=9NT z_7ST&Me-(i>70%jU-*~5&UW2b1AcvI@@P#Ep4aNNIR)U|dbeZpVed3OEO%o1UbHCQ zY}*@-JQ+{j0V(j{7YnN!r!n8kb49$x^_YD&yxrm)p8qNFSFeG0dmWp#7xqrXKetB* z!EP!2iqGPZmvP?ekq+2sastoEOV~Tep1nG767izWIlN6N@V5u`3>bmeg~}8)r{aG9 ztK<~`hgwRFd`-jiH=}ES(WN)+&$hgG1mJs+~qF)vk**Mxq0w=I&Y5)v2ZafP<>k+ppq!w>VvO+zUSIxQK(%!_D|s zP!|riSr{QF3VXDFTd;H*xbo`7fSI^|OK->4m4eSbty$!G3jW21DgBM$&8O?!Cr7}3 zmGbI;0d61FklPr>jK}f5H(;+CgNDy1QRgz6U)KgM&Ah4<3jSBY7WM%Aog>gX%Tslk59<}cH5RsP@4=Hx zR&hKI#(c%cvavXizA?Le)}05xon>p+58kO>UFpmmPvkkxjQ68WAJTgfw9MU zK8za{Wy}KC-qf7MjISl0i!&H~(#FshodXzuuZvzTQ3v0Ae#7iBSX_5W^%n581EFH< z$#~vDhnMZn%ACb}WuEPj(#hBw!3I1vM$rf@poCAc?do}@QeFk5A$7UTax zgSUeQSi(K)+;Ti0b#GrS;Q~LI*3`Qe95!B^QGX2m$zngXvcU3NuPnI$K2~t6WE}55 za*%(?CUD;Rmc>uO6LYe4X|0^-l>g;M)xj(-%(o%_BU>u*dXHVL;R~D-JxnsP{m^tUnTCj;q`|W4oi8Ui>@Z@0&j7PI;d95ipzS=Qr&3*7O zH}pTn_g(ya^|dVKBfmVuQ#)`z4+8eQX6oz<7vzf8kBhM!1C+`pVSc0Q3C-)Id7m^d zl;(}nyi%HXO7l|xpS)F)*GltVX*9xyh)l@N%JmgUM9`kq#nQZ4npaEnZfRaF&D*7Uy)^Ha<^|KdVVYM=^Nwj=GR<42dCfHM zndU{)ylI+OP4lj4UN+6!rg_~o@0;d@)4Xvy|JWZ$J3F=&>xpvA4@=CC7q~424{t-f zCG<|Q7g(UUPqqvEHE_M^4X}_7w{$r;GS}iz3pk0#I4=|LPafV|@*TX~t$o44E!Z!P zURIle`E-l)O81b>@c(yZZDsh|xrog%R`6eUzB=>^ubED*!xDob^f}!7Su+~udcQ+SDH z=4>(>uwP$)RJjYhbipT~mH7Vkm)vGO0>5ERwNt|H6_2r0eh5z7`0)E~rt~TGf_Xju#c1O9&)DR7V*$RuaQ#X5y&(^b!l zz-x>GVlr_5&dv&6^$|RFIneJ2#*6BD<5fKPy=Gap-mVy5+=<+6i@zt-I{XY;?-P3s4yoA{AC@AwgwL4$QPlr0k9sR!$NUw4c=WIvc>xozl}y_5Eu&eC zrMtX++ZLR6p1tMm70AO_c~<&%6xbuGd|*?(7_0i`3gI{4fUM?=tE$CV1IS=89uJY1pmD8xHM1{$wmLyQ_cih3_MOF@1yT$Ur7(RGJ#)mb}8}} zv_=oD#XjWtEFbp<S-z*nn?YI`FHV^xf6j9&zYwjbD?%9Q{$HAWa zW=|4?uKly8Cw=?C_KVX^AA)m)75wC2=lyiGdY*?oMfN#sUWI}WKQRmOL!R63_obzE z;Ikj{H`d~Mw9lM;kqfpp``XKc^0;0_*Siv&!4+YBY5$s#N1^&t_!fAP_$o2OH^|F- zCh4CQfjFe?26Y4E!E|a#SdV}ucdmUx*^5{Y?SRtf&sXSbgJs~D?Fe3Q**@C<`PPf|yb=f`{7U+@(C2K|eqo5zsXCv;7H z8CZz@Q2qz(+ZLAlB(4Hmd~vV}XNO%Y9IjypHhkvb{d_X^_1-Ma^WdJ^S#7^y_n-EZ zT5ty}n`=~)&5b$9K&O@!c+0i>0)g}KTveabwgZQKSrBC<4nOIgf8$KwaskJY`P>U(Hx$pjp8|%S zhKB8u{~h~W{xEZ;}g$Dm$JUZ74Uehw08(zf5cun#S_fA&TZ5bpQk^g zSB&Ya;q-24r3$`JE$!aUU-*483w_l- z{DwP!?zjvt{hq3nKLdGL=~iNAz{P93huH;TS3KQt&jk#T;Dm>oSJLF7Aa3# zmvSOb?kQ%w5PYwqtYrXp!tyOXN7%tb+`?aQEBUr&oSP-j(69KU2Wme=^xr`Z4~ahSjfO{CP$c`%Xb#X5)jE z{`%l+1`9))-iony&I$Sb1mk!6hwX8}$QzPb?|0Y}T(mGkdjaMTos8r=kMTS>US9aF z_%ZT=zq3}mgU7-SzGuht(RF0QHpXwH*!^PZY|IyDY>S0h;8hl@c_Q$&kN71 z?4@yCH9ViUkBI)73l=OAY5j-i*J9O0-$Xp`r$f0vz5#!X((_~btd9y@i08uVZ)%?w zdV%@E&1Rn8Uwq!o(d?H;!D}wA(_4h^GuBzLZ3cMhiNVr$cz(4$t-qas`AAACt8@X* zH#dIQD@E{r_2PHSF#kEoDGR^G{N*NcTk`TVG1h_*fo&JSpV!nDAAE`agex|VG5TCW zYtJ=?B5y4(U4BduyzfmBw{nLVYhRY?SO?}uE}x2bQ<3+!SM*!S8SphezNWW$z9;5e zI{#uyhLr!LKxQ7F(Egav`k2sa;P!tRd>q;@Oq|&m|CqnGP16pi4ny~XgQv+H$A%PA zqY_6D&tR>*&H_*S`litI3!cNnv!|^EYy8;qhW9J{PmZ_b8NJkr&%W+EJAVb3>O@c- z392(ebtrs@P6gGmpgI>+2P2c{WKbOqsV!}o5vnsnbx5dA z3Dq&7Iww>Ih3ceG9TlpxLUmZEP7Bp>p*k;A2ZrjzP#qbnGedP~s7}qqSf0?ap*lBI z2Z!q9P#qnrvqN=ws7?>n@u50DR0oLa1W_F!sxw4&h^S5x)iI(vM^p!i>LgJeC91PT zb(pA56V-8|I!{ywit0pB9Vx0aMRlmCP8HR$dKX?M*!~&&+5ef#==_B#O`MI5`@j9H z*8lWQnwT>udc95b?qcRaA?82)(Y{->FBk3GMf-ZuzF)L280{NI`-;)NW3(?B?OR6s zn$f;zv@aU%n@0Pp(Y|Z6FWVu~w~h97(<6P~XkR$mH;(p|qkZRSUpm^ij`p>qeeY;r zJlZ#p_SK_(_h?@}+P9DP^`m|NXkS3uH<0!fqpw|g1p9-sBPP~Y8^l@m$6ie>$9`fZ>w63fJi?ZqXN~HN3uJX-AX>_A{^b zlRsM5A@8C5zQ8beuL8euc`f`FW_E5`*dJ}P4R~;~26=^Nt^ebzqc%Kq!KlVPWNA!mxu3blnCSvtVEulK;UC$f7Bpxx>XY#cBbBi zu}k}=tSjOH7vyZ2%ESrA{+hQ8R*17MUR`g*_}%}7^9xPJaaT{pi4H{0hytveg_d3 zcsGsbljTnFwq)=(S9`~P};UFruOb($@90GzTfp|=3s*s&{V6mgBm zbJy=11GX|hi1BaR3pxGBIXR= z2G|=ZYvOvW5w}wLbWWxSJb0_wxa$n!C|{d%M!-{^d*nStoG(@I+(#Ddmoqbi^e!Q; zXCIv;a}exj*y^<*1a+$18x2#y0g2ki;z97MNoYD%f=id1+L<1~{o{BqH3{~SCRcGt znlJX<(_Tm|248ZVvSRE2{CFvSS5|>L6i?(9IKuB!c&}O$e7;joW9Ay^!7=wA- zt_iN)i9S0wswaDc=M+x)9SpzXspk)ZZ-aMn)U?cjAIQwGv8x6gl;5`VAN)@7!m&Cp z!4BsRdBwmFrgdCm@^|nW->`I@^@!t_oUmUA`>uZ1bB%c{#Iej{>@C4fHQW_DG!Pfe zi_|#`_SAZjuB3+Nt-6uL_!nB71UMw+VW0lVWitos+RcsFxeRsm(xvy=zz6o2)ohYN zUE3GI`yKeccW+PgdBC2Q)85!o3iiFK7#O)2*SB@%{W!2z*{zCi3*qO2y?-3s!@WAZ zL;~~I92XEN(pWkfi&;b`4pXe!qKfua3_pK;6@yeBtuCSLku$pXZas4^F zl-|sRz5MB3t!5DTt=hS@!u*IkALX&t0FNxbz9<^@v)o*7o2R(n(MhK&1YtirPTD=+ z7d(F0C4D90ZnMYq?k)h+JO>)LqH!%6_o8tz8aJbHH5zxLaXA{dqj5bN_oHz^8aJeI zMH+Xc_9(SisXa^WU1|?gdzsqP)ZV7{IJMWQJx}d@>JOm)0_sno{s!ugp#BQ#&!GMe z>JOp*66#N({ub(wq5c}`&!PSv>JOs+BI-|~{wC^=qW&uC&!YY=>JOv-GU`vG{x<55 zqy9ST&!hf6>JOy;Lh4VX{zmGLr2b0k&!qlN>JO#JO&= zV(L$({$}crrv7T`&!+xv>JO*>a_Uc~{&wn*r~Z2C&!_%=8V{iH0vb=C@dg@?pz#VC z&!F)R8V{lI5*kmT{yy4Qm-gMIeR*l$UfS39|LOZn`U2Cw!L+Y1?K@2S64SoL6Snz8 zUt`+$nD#}ceUoWlW!iU{_GPAhn`vKX+V`3Eg{FO@X*9A2w! zX}R+~_U)&60*!Zpbpl?0NQ#CISDy1HF|hGTS&qUB(2>6EF!ej~d0hfO2Jpj<;1T3- zX#)FqwZ4l?h5tKo(clSij{J1LTbYQ{Y?VVD8RGLk_QR}P?91oui_ieinJ!htQ;2#) z`O^{nV8t7Xc~@^D|8I%$z^y#kZByKDTrWj@T|~{0sTWLK6)(H*4)W2tlj`2(!cI`! zJ94TF`&##C=`&!lzNb#Q<WqTq>Zb133l=+kS^@t&?E z?SkVUgJXHZ!F_L@6_?}l#FeKR>;R8nTl3*|3HHG^_#KtOGOq``G;d*_``~212>6qi zg2v7w#LZ^d40KEsNHt zW|%VdI&&{)$aeH(>`Z{E}%<0iGi9TyhQA@PqBPo8WvE z3q`d!)Ncj|@dkj^jZa3s$9PaqNX=dYw*ET1<w(^7Q+N6NT|DS;=;k2q_`V0`G^4EvLgdIj;%9~qtXl336F zE8sQvpS!DLJa(5W<|u+wBVq&jz0@z;1V`oAH@;h&b zX2+!>Kj2AZO(({;Rce;wVsLt&uatBs@<%PVhSx%`Bela&>P8^yO+9|}@q)!THizFi zjC@dm^U~L$50ddTQ0Ju=zE3}nhXpP@$YH?gfw-$rqVycF=bIlD2F|Dth%c{efd0tg zvo)oedtvv=o|Mi5j~gkxVeP{AKNjZ60{5E9G$-#s{_C}~j<>;g7WHlqu|a<7rn&ze zPnBwVd~rMKLHOCTcjLHwUx7d-@@3wgZQy3~Q$*OblJ9Q8^(?cJ%7wm)ZFp5%%tq*$ zSv}&?0tY?*q2Yx5iA#l9Tms;KJ;6ModdSyI6_k7f{g~GeJ{phcAYXNSeoZ}i$O)ceD5@_-^{1#l71ghz`c_o`it1xg{Vb}lMfJC+J{Q&RqWWG`|BLE_QT;HgFJ?mY z$EZFT)i0y^X8(`=8PP|h`e{^Ojq0yaeKxA!M)lpO{u|YYqxx}FUyka}QGGh9Uq|)r zsQw+*$D{grR9}zk?@@g|s^3TT{iyyQ)d!^dfmC0R>JL(VLaJX#^$n^1A=O8u`iWFu zk?Jo}eMYL^NcA15{v*|gr23ImUy|xiQhiFQUrF^Xss1I^$E5n1R9}35h{j~m{>H|>y0IDxQ^#`av0o5;{`UX`0fa)Vq{RFD7K=l`>J_FTnp!yC} z|AFd5Q2hw1FG2Mus6GYNub}!CRR4nNV^IAJs;@!yH>f@b)$gGC9#sE>>Vr`I5UMXi z^+%{a3Dqy5`X*HWgzBSE{S>ONLiJauJ`2@vq53XV|Ap$qQ2iLHFGKZbs6GwVuc7)j zRR4zR<52w^s;@)!cc?xO)$gJDK2-mQ>H|^zAgV7!^@pfF5!ElE`bJd$i0UIz{UoZd zMD>@bJ`>e%qWVr$|B32DQT-^YFGcmIs6G|ducG=^RR4Vr}JFsd&`^~b0_8PzYN`es!BjOwFN{WPktM)lXIJ{#3a9|z;pZ7I_Q}$nH<;vKE{9M!TdHcmKkdmrgGSS66|H;}FT9e-8#S80U{?+JhS3b;?d<3e6VZ~!*vo?UdwIWbpuY0!rYVfxfOfjS znFr!(HbKTe89g=L%mrhiywI0?KioYX_PWc-k>R_@1FpDowOR-K#Na?f$V~VXbfvih z!JbcaIhP~PS5)_~K_&Q0tV+fSapXDJit_e>XFTty2}c}J=1}5QPWT6&rUnW!_SlWY z;}I%gcHYz^32EdZ$CMc{`qFlrA3OAb3+!$zXX@|gKfj;N99Kw_iByHZAz$@v+A!jl zFW2+&GyVwM`%5jdVeftNww_uFKC{6p{iH1PlyiilCc)p)RPB{)1s;!75eNhGnOvB; zUk3gPXM@MH;ZGSI2z8%>_rGsUA3OpcbJa}_#`iraZMpUmEVOU0&MO?}Fmv9&68@YE zD>V}O@p*dXYfNInJ+X_O0~N(t`<_m9Zv>|g4+!w9!d@-NHX8@Ou5O-U2K!aAY>FA< zPqH|X`dEJ*>f5#X|Klw2_7BT8K>s3%y-6R(J!8%Ggjpgl`Lj~AI=Cn1w)0MF_`l41 zqxXXq`Shiv?4Xy>p}V3I?7oMi!Q26Q0rJmH6yVQ#$C+`r!V&kc_p*Q$IPS*EL8blB zD_c_R>IzofscF>d20iArg;5#cxnC{z4tt{B!ES{s7yNDe%8n-|dc(gkzoe1ThgvkJ z@qrfN;>|y2<{buSj4o{G^oKvpoGt1KnELnN`di4x#{OR)u8czS9UiC=crNugbK}064r;cwfulCBS9b=-4}~n%!*diJcSb%2JaI0wZ`O?n;l3s< zwd7xfdWJmXL@p2Tw|5bW&ykO8`FeWK8?fw;*stC9#8_(TGXIouA1}pKN?k!c1W#;L z<}>iMpS2f$p`O5rtzLOP?t@>uWx6Z!qgkynVlm(kv(D(TL$@)6;BPbDthum5kVd_q%KU5{Uup*rfllMgFr%a%|``u%?7?&;_uP-DHXRxX$VR^BU~H zd138s+rT_8wWjFde80|){=xWBZG=YRR^#{7bp2B41P4rdD_M-+KXI+;wU0e`qcb1% z3Vp2t--M4&nQcMNdwvNB;B8z{57|;CaQ-Qg-0kJUiz%F~2X> zu;Kvn#}#znS_^m+J!CN)I1Nqzy z53MxVzGAvbW>@`;e>v8$+6$#A>ioVscL_5J`>l@ylb6| z=>NHv6W5k+QbElSUZ1A6D*hSrI`;Zc(Ypf{$vQs#YqB`Y?ue_y5%8*&-s86?VLtqn zzhpO9y=$fQ5v*@Nd`_)%0c*VVZ89E5T>^LXZVzyu!b!z))XB{Hyjz%g{T4osz474l zl^v5cz;2&--@ga9tcc&uoWJ*uUf=A0Vl2CoslcW*D1BQUdKF4$A5sYsyumc9{he*y|^_P@drB>Fn9EA*Dt@t zSecvVb}k0@ST|HILS4_|gO}NQaep{o*LPX{Kpn==pjI~6{?(TFHr$VcB8~yh;L`@# zyG3#RZ|msayMgK<#pjzo)8K^&bpE-+UyF%?Z45XVUs1@Ry4z z+RMO0{~ud#9!_P{_WhG7Lx!R<6EY@4N`=~_5QRde6hcIyNGOENL@87hrOYZtC9_CW zrY4jjGF8SV%I|yiKKs4z-+Erh;g8R8?S1XN*R{@Zo$FkpCsGE%hM@%&EttBIiK)W|oPKF9*$$3x zv-b=7hiWx8g89#|)NqEO*P^=+^{7^lvJ zb&_({!)K{|teIWQyjEOBJ=c6Tr(*EiiSQ@OsdbZ!(rmJ!R3E4_zMUA058GVi_JQeR zlhw)=(Fo>9<2CQW_01x`=YIf`eL2X!9b{h*vhN4k7lf==BFw zdhIN|_LX{${P?H-$k&o}r(|6!S+`2owUTwOWL+#-H%r#ll6ALaT`pO-OV;(0b-!d? zFj+TD9~*jIF@a>~=tQ~GmX;+Iu@7vz}CGComG`(!j z`+PVXk{b6Q{8N8kH{Ha~EtkZ4Bdf*_oa4BhwNvh{f+X!Zdu4kknEu|jpS}M-4%AjP zbHnvJx;h-pczyWD6M=(Z#&xV)QspFROj+*}_k&v!7@Y!e&g$l(u7AwPwc2(+KF{5< z(aRp!%N515eoITzG`4nlxq>$;osrwVOp(5Fvccq_ zgd{ClSJ5j9yvmG0P9NvCX6A$zMS{cUxr%j(O46b>skMiK#TS-!_=rf-425l0odR3E z*Qgd1mZW*Eww`hacQ4=`@mwrPJGwt&)jIHo3BR;jKAh8?QkzZ>Vej<~#Q zWdJWwx_j&Cd`VjN@@Kd`l$1N?nB%o#yPeD zTsJ#iS=fy}c}@bn%HZU#sFMCJ)Svgtm~wzGUp*n%`W}5%H%xq+#P?my5_iI<6@7+7 z#Et90Ii6YnIHY81JHd(!JW4D1j)1tnc)?%G8aPbjQaDUI8!@f1x zPsuLwq8;aWx-R-#d>q$zec^p^8NBeOUveAn$D&sLVmmndj;{T$-vGkoRF_y%S?S->mRx}P5Z0R25s{i-t#e#O;UM}-lbv*f42rvR=r zTU2*t41JV#yjE?Gh2Jp3+O*-f1ns1$UyC1j&q@cO0L+J`^U*D?;H7>!BeH+dhn)GQ znhiMZxZZf}JW1M=sg)P7usjam1MIVEYrd_(GAA>01yH_pGn z{8TFTUey5>U>??VUkG`6et2ggIQGFo@0W`p&+9G}WrAO}ZM&x-AW6%05Y@O2wwpDP zAHw{9qNr$*27bYoET$$XNlW7W(MDZ2+W%CI3-V($e&>D`SaxlSc`fAe;~Q>^a`30w zXC2})lC<|H6Ym|9O>{|Ha~OUM|}S{>IM1`y29q>fYeR&ER7eg<3&klK)p% zB@Qd{lq{8`*&hDQzY*8vmIyvhf;@_yytrU5SbZ+^*BF;1jnVhZjw9g4@muUZ7$27w z%N;e~Y##l%12foP?2?sIdTIVrR`5o)VZ*p3w{lbI6^&Iyn zBxuai7Z+{^qi^QObL@LMRoBXwK=H#(^4suyjhWVu=YrW+w6FvZqEDXtzJJg6$fwm( zERerkp2ey>c>ewe6taVQk!Q%47+(+Oj+|S@19>~o(qE7cc6fJLijucgH(19Sz`818 zcJnabCU&zFeg(6MJ*7pqVVzraY`Zk(+o{}TqmG!Lr*=Nq(gvrS7b@`OX%C%tMS&282ke4jZim&6~rMtFzk5r?N_LBu) zS3y2f)>xVqRH84&!$(K_!H2VV$GKIYUcKcW_hT^KmKbMyF8B-PoLeZg9M^F!*$V~8 ztITKCE%D$+X{CSiyzKVdS5CNJ(VE$R^6nS@`l>Dpx7JTs# zET;V7^}c7Q1KC`{-3$K8`+AKO=o{d@pv3t z!6*PuG7VTX1NQ1aQLqFo)1@bL=`H%S-Lj372Y>#}Y`quq+raZ_r6t&=rRMLwJ)cmQ^I_^}7`SF-qo&?x^f8=QQ5Xw8XwawD3jOQ*C!042%$S)qXEhH0 zOvmuf5%9a?;ab!=-M8L88{Y#C>YO;=4f)Qg3w~w>Uj5xi$`a>(mkJ$HGX~3lT|Ldi zB1zl6E68&zm_P38)Jk?qnvnC*qy<<~IXdJ4`*f98##q@Bs4V`TjURCv5+Z zh~cLWkYBnlK-zz}1hViW{>97mGc_~?u5XJ+jfI@)U%@}e(1Y}`)C=m27RryfL^%=o zqrcXEzSaV7OfPHuiql5jwq3CTPsJbBeh?sz^BR>)4}&i*UzcnXhPd84WM2?i zH@j)-aSYy-QTmcH9%Af@dWj15z1VU60n?4G!5F{9GtX`pA)lGe(q%M<=O^utTH+R4w_67mED1UKAMV=>%p_-6V^%?FR{BP z4Es^P)x2w_y>AP~Z)$Y@1+Y-9S(7&MyPS?PK1~trPYOqW8jJFiQhHf?nDk|6~v}Jbre6YvD3*8zk(cdomP^KKXWnF}r zGxF0H*iH!nZ+skyAS3VfwuSLJd+=+$3M)qY^k z4PAmg{HP~%5$tmSC#~vJlUa!I_?*y7afSzHrycVDiHXX=)Omczt91{5VnbgM!Ru!z z<{1_IuFinCQ_hek4W1vhawHb{9=mlJ43nt;^08Jlf88NYJF?9_jXIC3e=N62v=RO9 zYHDomfPZOMs#u{<%+>PXTu}zW{Qd7woWWCWHs`pmArHIngs49Fn0@L*sUANKM>}%9@ zHhlh0E*-Ta&|e46g!)nEpIzB?kz>ml^l>rqUq6HQXBe$Dbq@#2$j$x%8@MokRF8xH z&{}$B5nk_^y;PhZzh}E?n}{MfMMe^|ScCTno4RQ|F(l3dcCu z;r(CCnie;K|McB&GEWkxu_&r{j)ChYUhZ|vKp!6_lT(6t9+!_?>e0$9;0m#Ow8Z$O_7N2j7P7x2d2d5iYo`DnUT*qjG%`rB%H7 z!QEfY=06=k-;r2932J_^t&tZmh5j?hR%Dz5zm)yWvXT}1!M)#I*T8&y9+%&8ZvpzE zaHpnI%sx8z-Ua&Y;O%>T)?ltC_0DMMv-WLs2k(LJw=b!-kVU`8Q2(bdz)UgBz8|6A z=INc<+y>Usk+dDfeBW3p%{C3bZI>uVt>@T(Sr{e;`T7{fX2J}4c{O=6%mu96!2Z^A z73xoUZfwZ~@0h5JlUaxQ!XJi%@4#DbspUCC{`8{`$_;^c&)KVSLqAT%S^KT}?JLuqzXy!-!VcuDeO zS<`lH$NCr}jl&80=E^QJ@c)*PdJE8xg4Hfdi zi%i~Yhg(2@?R9(J0bV;#iqqc``}i)!Ki?1icgCoWxf1=!t|qhPUI9N@ACp1ptGk2F zQEgx=-kE?nYxs?^{)*IjRT-a_gfKwA$=kW)*g>DJKGWSUg7u^eW$Mvs;HNqleSLA= zwsOPhG`Qk>&6@Kzc%Jh(r>CYE zzm+PZYw-FKB>{gA6ZnxWy8o^p$#d@bY=pRO>hr(*v6`8`@!pL3@VNTMF-pG={nGBD zpe2nKi*`Q&>2DVO>;1H9Cec4o)^#Q+cI2z(-scxoTs+#WL=GN{v_Rh?sIMC;Tvt2N_Gj<8ksP&hxM@^GQm2GsZU| za5#M(`Y!dxc<7dZH?U}xXkvX;``tiLBzR54llAZ559A%usHFTA+a9Lpm$0soJ3UrT z*^`EOLDP?laUR%%Rr{&=kiM)X!#q=*cH{7~zg^(&st$E-=(C%dKSgW7ScJ+AO~AU# zszaxO!HU7>zb}s#r=4pH|8oEw?KE`qSt$C@lvZeM1@}gtmt#ML^T2uqN;SZAeYEX! zk&fbjG2Op{%Y`L2rUO*IhyGf+E}=hr`m>_^G-v!%fArVV?@N9z`Mnf~?@j)$kHqgL z$Aug>a$L!AC!Y)X+{oujK6i3nkn@I|SLD1S=OsCB$$3rAdr~e)xgq81f5{yom!#a% zpDkUkNx3KW0;xAhy+Z08QZJEui_~kR-XrxQsW(ZzO6pxwFOzzk)a#_)C+z}hH%Ple z+8xp^k#>u;Yoy&H?ILM6NxMqgUD7U-cAK>8q}{(l_ywfjK>8J=-?54COZsafM0F6S z9f&Xyq5QzZA^Y}jIFEJ6iAQ@WKcjF7l1LUmZ~Vtt=;rt2}dOZF3o z64tAgEGu=|!Q!D@{65v6>>&L6`RFLt<;EWzu!J8_ zP_6H+0l#$7g@K+taN%0RC;sqbgR(}etHI}U#n$eWz&YXvRca+*=dT*oZ21d6+2`*5 z%y6*Qs%O)4E0Aw3$o#w*di2wo;>Hu|=qnl@bjTjO-0G-%!&<~`dhhFBfdzLgdc&*- zzk0-?Apm+mN`0u=4u1aImxC^q;H=PT_5pC_yEiV2D7&Qp)P(Ab&nNei!vg$n$K^HQ zc>nsbKMjH4(Qv(@5Gegyn{;S1|zE`wEB zf5ZfFQRgN!*&hV!3G|E3XFz`U>mFS>aJ1rfQz5M9%S*&|Q-01R>tn->cd)M4mQ{8P z%$8x$(|j9#eTd^6b*}87b^EH>RB_rRV+I>laOFcUhr<`)hbty@EdcMn**a_rKlbKi zpB5D-RbLDm*>)O!sY=(CQZU`#gvAmCg(jV7o^;faz)A=IX^LF73lR-g_;s#VTfhFtQztS zKrcVd8uye+M)a7`+f;cAXNgnuoL)c0AidpeB1fFYF{CMSHU;`%dGVI4TZm&HJh@^G z{^h~Rkb}5zV~|j%DELlGQF;R8_5SNKGnKfvR&jC2e&o&QpGE)d4;-_5#&F%h*~eNE zadu?n_4O=x|E26AZSFLjgUcq>=>s-A93DFVCi=#I3lS~`znuvcFhkyLwchf*!{Bwi zIq80d;$J9jAMGXC zGMf!R}bbzM*Em6{EnMgPk`AML^8O`_)hKVjdNb5$sVb=2;dnoJ=N7`I8B z3;gZk=(Es;Pjr9R@B{`xcy-g9DLkqD!d7G9PZAdAHd-W+FIW+ zKElFkHWgsDXKw6j7{8N8kDZDJKRNVHE94RCe$)fIEWv5d+d`Hh57XT6WbbnD^K);S zOrh`7Z6{~H;Q8@37T!#Sz3$GI6nO+D=NqOXR4yX}rJvpHzj*Q^=R%)5^szVKeIPc1 zv7qKH4!QaFw|ec>q-xZ4-SMmz>%_H4EyH1^*T^UQ&udSG?y5HI5~o$|aF~n5b^5*N z&yjr2-%6YlY3QfAg3AgHkbYdul*9;Ck(l^kxhxf&(p%yMFUvs-qFpDn?>X%hb61YS5&KKc#w zhAnxSSPsPlCM8roa^OaevS(oO`Qf1|VICw=^FYIO{;Kj2^o0JO-y#-}P_+;~NlW}L z@g(HJsCl6BZx%2xSPYs~`y$Rg^ChX{6Mh%{y#qT&AB}+>Ox@f!MvBwCt)_(d@V$>T zPn@hzLS1x>?!Vu8b9LmLRXXNB&-Tt2xE`lN3%v{d_hwP)Y#X>h>;pd|es@EGZ`TO; zTa2Vu7xa_iY1z`{_#GQwde5DJ{)uJu7u^BQuW6Q5y@$Fh_qy2(uz9RSjW+a=w_R}8 zC$QMBfs73JtINNQcg=ylo^zWyVjbc7iPay3@Y^nz?yM4RM}4HjUORm-o879jOHlU| zCu_Ce5$vKQYh&~Ub!Y!G7M?kNaedGi_C*U1tiyH3s4H5Rp-&1O`3|5r7wv$Jj?@7B zGa2=;Ya(Fbb(~IDu#TXadGqNCuwlpX>c4#0=leBVR|iZV>rLXdQpVs#&#p^FFGb$G zO}g(dzV8b4#JV}W{;t>GpkALB=TX{!y0qnJ$bWk0nUsi%f5zH*`Z8X54y<)j zaNJej@7zkQzKTL+Fk*>NF%2p>&asCw$gkE0`o{?;@_X=taVg?Ptr#Mcqnzh}rq)5y<}?pR?V7I)dLe zEEmp$XLW^Bs<wWj1;Fonh0+QT+SL_A9=8ui+>v}_h8ydQ`xfltzZ!;hWb@|E zokEf{pO~{nCg1{tTLH|dySaSg^>p7^tdFGjYG;6d{{En$58fgzHQ<0cf-iyf8ZScO zXN0NC)nT2oQNFyL>I1#v=o;x+aK|G3`@-PoH`4s5`^{rAeNY>M^&Z2R9vap$eFGl} zID`EZiz~xW2NbU3w0aTPAXvxbx)eU&_K`(aF!FEN@y2&CwxX&0iBunG`dG`Juf0$8 z{})>EHF6DJzf>eofa;?!QJeKm1WfO1sIJF#(F>nX=L6=F@@K)%miRn)hu0U5FPM3Q z-(&mP?g4fGk&~v?#rXb5H@iQ`1NYc5`8A=A;?TMN_RaWyvkQM+ZN~V;`Z}!+2ftf0 zx7limB+a9{w7nCo{BqB04Xo?li);{3#_x|#sM36jI*rS&bzUdHJVQcqf54&h-c9v@ z^OCHeMB(*!I`4VOV|Z=N&E+d;9@-K(N;<5_K2Kdp@={f=i$AOIPto(wsUY zcTR#M-S)KH#C+A|kh?^03EX{epL8b~m^+$TM|i zIMxek8zUW1_du6jx~xWN)zVCGUEIpMuLjqToE}Y!jzFKsJ(`jE;KWV4S>MDWPIokD zpz4%9Xt<_UB%wdYHn**LxWCNoTJG1^&FBPE-by`UGl58SDIGls$)S$}oyX?~gx|=4qH)^co&psSlIR7ypl5y8J(S ziGGd#+GTQzx`U|yE4lhy<160lZG1-PELbpj&ui*Dh_Am)IiAZ)(j@&^tn$EEFAA6k zHh7vTK+QwF!zO_{P%rqqrBq`p=9Nuk#uh=;3o_JIdgX#22&)UUK#qs1N)tJtheqFo z2zp3M(lQ)7rmewq75cwrA-4}EuT2MoS1j2(!-D#T#K5=ruY$YdtJLgKA9$>C)%1O^ zxH|WY6Xe*a@onNq@X1)?)U%LVgF3$ZJebD^XDm~bAh#Uc>i4&Rc{f+9=|ir9KM5v= zfZOf`2Wmo22IUgB&&LuJ>XXL%&Fe;pQQ*et&$w->3vF|LIxt6_Asy zeiCLKuoJlq6=vpOFYcXHtewi$f`(`BJ&&Gtx5*yPDmk)6@FzRiCvM<6(XpQX$R z_fz}6!l(v(e{?RDAr9;QMFq~&;O!g2ip$QS{^*XO0x#-sHgF2w@K3x-DjDGY9t>|{9)z4m3%UzcZiJ#$;*NJJXWyZF%S@&#=3cQ1<%^o+lN zngUh-Gn%eYzcd}^po(rbQUPa2PsXX-K;6b6)kq8K{_#q-wOJUiw_?Nj;7;wv*oS$j zdr*Fy-3U%PBNDgz4*C`?2;po42bW)W(JDfpou?wX)O8m(p^;O?Scm0X+4uxpZ?c@l zrwnxsvpbt8)-Uj2cZDANTh^&m3vQAx9{F8^{=>KLMR$PLUsG#d2)!Qp*xhXy%$6`C z*YylOQ`zAKOA?_6_dKyOc!B(=(*8U>aL0wyV<({Z9QB1_&A~MS{a+)X_wM&q@tc89 zcIufozrnec>_=8^1#e_r(HZvveScnUE#LvGdOj`@Lw(=<$g(jRaF}h}>xgf7-V)8{ z#K5(m8|J1aC1^Ce6!#Ti*6MTG2Y$lNv}W<{0eia6lq{Xac-_`)J^}ubRCAyicJM*f z*E?syOu8e@5lpDBv^h4B1HR(w==G3Il4i$rQmq+mdaRIPhC`AT;L+dw2OM&Ccd`QP ztWRg1rvh|uRz_gzP1xazwFc@2;IV26>HY8n&hFPgrUm9ar!1%szv!=C>G}O&%dLWC zTTwqv*WDLxMU_^g-cr%0XUA?_r|a*YeasTu|BC~DT;+!SigsP_~VFecz@nrvvf4Wn%mGOkloGJ~fQ%eu@1S zN7}ht!R1>zJKmvwxYftfEgyX5f>&~MIo6pUS;Pc_8-M1-ij-oX-6t&`q4kMIF`ejnVHQ|AODT zncZ{H&)Ysk@dB7P==a=FFb9tqmkxO7^QG8&@Y5U1T+5+v9Qda1M&kXSnnrRrf={w8 zCC=&m(@7Nj?`?$+qBhC-~{K*E- zf5)l30B_}UbuH+^KH$JJW4FPJZ~Qz+)obpqlaD_I-tjt1ekJCI<I+Tft#-JB2l zA&(`iRH^fejGmQd{DFNFI;EjH2zw^;BC?kW^1VgOd7P?O{*u%6I^rw7uk0uvRo^_g z$8byR1nl9N_U2UZZ(7v3MLR&(}0J}LdZJ5o;avtRl@ex4WM=h5{9 zsYggXL+T+?Pmy|z)N`aBB=sbzM@c

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h zq#Y#fBxy%UJ4@PO(oU0hoV4?#A3*vEq#r^08KfUV`YEIzL;5+SA4K{|q#s54S)?CE z`e~#eNBViBA4vL%q#sH8nWP^|`l)1m7|{GhcNgNv>pDA(2BGKgw|+R^90t8>v@Mbc zc7iUec^|{&eZk+i{?!_X?7kbfYPbx3`i7BpD_&pqEY6wQhZCJH#~+IO#hkw>{}JpF z+#om_0lOL@r7#0_Dx1^X5rw*Wi|fy*eMC0W;-7CJKFv5O+(6m6T=P<~Uih(v-aC!9 zgZ0Z4-lWIF&MwJwJ`9eMDWwI&k3Aj~n&l0ajqc^}xPbbsI7g!-u!Mu{%p3T%!-K-Z z55ZPprfIw3*X~JPl}*{{RG(u3Ln+8h=9y)02Y3H5c2v58c=y4925E3(`!f#eJn~~% zRW2IfwL;AvS@2`cKI<6SfsgalZPvaHKk{CCwh#CT`*Gd`_^s=24j83?ldkl3N8iFe zpW~Yp>cGCC*(QwHST{F#W7G%!z-pf1McLW-ui3xAxsANNLx{JF3ZF(&e$5x_`{_Ec z^T(B)rFfbv$nv?!d1V_efjw1MmOb%Vo0c5&Bt5E{y&PZmcQe zVuIb>VOo+f1Ww;5WfB6`*X$e}1piQxGjpzl9}&Vk_6L0Po6W=h@PiYhZjLhJ{)<1` zY)glo-ZA}mR0w>RD`Cp%DdKmn4@yd4abwmIJJ{to<0COUz&;J00t;Y=&6D;RdxIa$ z6y}M+FSf{$JrW2$_UB%*AnY=$;IF2OU`d_01<|m>p)PB>(!jj_;a6|BHJ$@ z$Lh9#)DFZYnQq@g!Nt42pG=0ItbfYur7yVsRloHL*x_aQ)Bmm?FMapqdM}=z_@THc zT>tF9`L!iZ9s2YT#%01ims;2Aiy6^*e+({9x^+yIJ6+@4pES^Ged#RIVmG0vBwvf1`ta z6)#pvsaJr1C}du6gk3&kDfW*eH~DnFg5N7Q`&7LX*Bfo#^sGQ$!eh96$4{{F!x83L z8~a2Kj|Nk{sZY>kp2hh zpOF3w>EDq459uF~{uAk6k^UFypOO9>>EDt5AL$>G{v+vMlKv;@pOXG7>EDw6FXEDz7KN%m8@dFuOknsn7F4E%@GJYZB8#4YO<0CSDBI7GE{vzWu zGJYfDJ2L(w<3lojB;!jm{v_j5GJYlFTQdG7<6|;@rt1NEd`)Nierj^RHMt*)+^uS)J`CHK3M`(erbvgCeRa=$IPAD7&(OYY|-_xqCjfyw>CLmWf+w@#|Zm_~PajXf_UrW!uQ+8g1u=DiSN;j7379!3U z)@&1y!Fw;>VmL94fr1+Ze0Rbz?Y;Iw5#EtuqYp2y$b7z^GyAG zrNLIm`tJ{b7Z{#kF+(2kgHWf19@c5ro;dJt?OD}ly+txupu+F=2dt~S^DNT%25#_F zoZA3i)R5<8j&&MYk2}`ZSXa?aKk%Iy>oR9f#Y|IqiYG}z(|%y-&h>A+k%zpbF+J4` z7FZX{qKdr6;{?|W%2;Q)$NaI_2kW1PiE%rVz`+7TAt}g<9N5-5`3h{va$T#BT7MRN zf6oHzD&%@Kxt>j~ca!Vk^s%R}my_!ppbK-5V=l7 zt|O7_OyoKgxlTo{W0C7zwM%oAh}LRt|OA`jC5I| z=eg)immi3g8rC=e^`W8nl%V%Gpud*hLxhsU3;!dBWPg(X)xU)3Z$kDzq2HU{ABE0p zmri}FMI8QQ>m)}qgGctd^v@tJN%LTB$C;6|cAjf9mXNzO1$v3p^)H?bQOhBBGzqC`Dj$8| z>^EIQ#5Mk%rbSeLj`z8jA}>Sk-fZ>ZzXUeF%B|yz^@We80xv{^Gr~{Yu7DhhD^=Y) z3(j$M*8YO^iKf!Q)-Z5<-fA6H!W;5%4U4vdPydhD6qEWzJKp$=}zmk0}uIYj!WQrU;cZ`{a}>_ z?K{fgqE)HWkzih#U3YB3+&}JSc!1YzRk?E<{K`S@t_HYggHCfOIDAFG?=Gw#9hH}D zrt<%%zKXWEQ~b*|{yI40q0@yaRp$}+Mow5a3X#b6XN616GIFYTq9=!eaH8nk~ z7sQ&TzK{i%a2oQsKgWIzruE!Q!1+IWP6~tVbn;`lz~09%ro3-J{h4CUHU{w2VjinI z;F8o;+a{0~JLY4qnF@AT!2Y!tJjuRlJPpjE(D1bdJkiW#bsxM(rkuMPOiSFL(*^E` zF8q2AyqY`GcQHOMP=fVHCb$R38Xp8-yk5R71?<*v_hA)y>vZ9^bKu_a4{NUD`-g3K zt4ysYFU!B+E%Z`?Rvh&3t3BA=C-}E3#v@2@c0d$7#_IUc40)7++r}&BgM)<455!sP#fWy!&`&YCqUjZR(yj>Lni6)M{)4d&qyhPy&6VQ@Ve(G1xqIkufLq z6}!IfcQY_u-!Y}`ym=#4!lOg-fko{=Lel=u28?xUG*$;>8mqYf`A^Yu+ z{dmZJJ!C&0vfmHc4~Xm+MD`OR`wfx(h{%3LWIrRa-x1jliR_m|_ERGJEzxZ!y&n_V zuZisEMD}|k`$3WYqR4(yWWOo09~IfJitJ}a_PZkcVUhi^$bMR6zb&#K7um0i?B}II z^!p}N*yJ0ts{k^R!herjaDHL@QY*{_ZCm*7UJ)AtL6 zos{P1hi*ycHPa{=`SGot!^U8w9 z4+m7SVI6cQtAPhQ*t({YWe@xyQL*bPzp;-czBJnQC+fI#^gja0pQ0Z#o%8{3XLEiV1LrbXZjj+W~Cp(ni@ zZkqalt%T?6jG}Hs&~&G&7I^>3N!@tVInaL_{kL*`+Na8m>%HsEAAZAcH{Eru@DTW5 z|GVbb@SD8eJY`O+~TX>)+AK!oeyv$dI;0rXnWKrb1R~SSV zx`3Y)UE+~K-Nw`bkyqK^=^vB+ zGwENG{x|8Llm0vD-;@4786S}G0~ueC@dp{7knsx{-;nVS86T1H6B%ET@fR7Nk?|WD z-;wbj86T4IBN<%osdWEbo7AP-QsbN&MGv#va=M%;g2 z*RezWIjFbSdR@5zpYQKg#+C_IQA{e9MjWSHbz;yA%v&Emrh@P5{8M{A)qi_Yr})^V z_oypPay~SajqA=1%N_B1TVM7!Qhn^J)(SVXAdWow{kiNR@W}7y#f6A7BT~N%n1ap9 zuE|d$4o$KysM`+CExMw64eS5&d<_h=!E1OsO_$+$#O>R2n+?o-b2Nh!&+Ecz=lN9s z?u!?d155DyUtTwoO9EHyezCORG3qB4Z!|axR>`MFgizrC!Mbwzf8mZio)bI!1C{|eU=5c8(os8 z`qL$Bv}+o;in>zS>a1MwivAFez$>UDj=s$90{-gyyLfjBPPWaM0pMgWkV7A84 z+y2fHG*_os!*Fn#FrTppVCO-YjYG zfp#<2H(*9?2RR0?shuQ48(1^EExZu>AP*aU3fT|-kuy);AH2c2IW^55d8kOkL#x3! z|5wxS0G{W&Y}r}p&X4h2LaX*+el6WDN9|qiDt`UibvNX>{$W@bxas$tlFts*(Q5o1 zV8i>{yM`OqW8Wt9dA;YFxd89quc7D2 z2VS}74GTN?*!*6x$Mhz^lPT`!9!Wu7WrUYzgWF>C6W#H8$z5-xj)E6P=*pK#OVH@HY0U~z zH?f&JLJsP7n-jr4u2N1?)jRVNf315wH!zc#ksVL1QlIa}c<+LBwwn z1Vs4EwVe6TZToZKzccUJ$JUy19*ej=-ka^SzL96l5Vd-tBL z=vWbWul3yrl)c2MJoSTMrZ-LD&%hra*yo$W-t~Vm2p5OF&7a@j@(cD*F!6;!1ejC5 zql2Rm>k1P$BZ|RMbxe61!0hv9*K*o z1xM!!VE@nL8Vt6A+sf`7`1?qLc4Nu=`uXrL3~G4YN6N7OILdTT9(+Z_iurOS^lQYQ z+mt_|a^`7dKn?2sObx<4!9SEQsBMJ5Vqa!q@EDxU?71|g8U0i*xE$&PH<<=;d$gmz zulsVC82qKhtiRt&!2b(EIU;qQiF3uk)krXR)nm41aAdXU4T`mf?y$+iKP$7~^D76N zZ;Jfv54Jn|d5aD5?r9Y_Zq0n>ZtKjgNO>eB9LO$0Xe`X;z-rpD_O{5867Px|w4@mHgsu%3zw=)G|s z1_wQUh`IR1ZSO{H*e?2eskp!!|I{D-wbuEGtSXp?mAjwbT#LS_w%^z7-3i|HIOT;p z`ld>4`s3#g&iED<>yDluZf83E1HsE4L}(YG|I?oVv#U43`8k)Gve38UtETIpa`5T4 zR)O#69}_uy<_UFfd%SRA%*YL#)8jJRH3c4iytVig`o%09nL9NHzPBm7dK~>pLKfDR zNn)NeF7T<`j{Xa~9jDS{!Ms_K$&=`JvcRc-za>~@_{ycQMseD=3Gt1T-{`;4>(Ms! z8IcH%KM)T7D#tyZ{T_WW_`4!&{~yeko4q{?eQWpIe6(%A^@~RFZzj=)MAB-3ZWs9B z%Wlyled4sUVOEl?kQ=3jFrMJZ3K(?$d9nk zq94wuym^#d$y_v7tokcX6UiFtxdB!lVw2)$gq{*hnMcJD96R#5&6yBqFlC>Sf?S9! z5ifcKUY44;%o=>cwAxY?Ts_#!mu)+MwN-@ZVmfKd{ zA!h8KTyWOT7991gKO3J z;dsX&cJ$qRk|a%o97KG}Fki(9yU)p`9}3>Wp|xugc6{k-kEcaovk0dHF$>_gn1r#u z2HPsHkO~GLd9ifM63E4;lEm*H@cLxVCv}=&?dKxiJ-FX%yMvh~V2K51zi)#dFn;a$ zW)E<~E13gg_&$~P**8PL1vX)0=ka~reCzeIz`D*eW_I9HCVFSLeD7V7z?P3vglf34e)&C~ zvk+`oe0<+49_Xpin5afDj~=&OFfa7Rg_y&i!LvMu$Lqm#S!}+T%e`^Mf4ELH$9IJ? z#xtS*>vz2V;%X)6co#)S& z_wo7z0US4EHL*W)^3Tf>aC-4n2i4!sH*|D+9(Yy4QL+8-8=VaO3u3`lcapc>*1`I- zzxm5J@Dum>JThi84pnl@p)-qU!>z`MKys!nLb4=`tQe+AyMTxiSX z_3%5q-Bz;WexpmCchyiX&@VgR%P~n%eni!2v2y6QE#tj?ZqSDjOXp>#K|emezDcSR`u2?c z35|kp=ok5XNl^t@P$#uHA1t9E^p8tt8YFztm->3~|5Y-d}}&SVkh(O29*J=lIv4FSgTStF)`&wDRJrgXnLVsViyY z3D)vU<`}&K`!#q#dkdJhcW!P+7|zktQSGGq7WY3~SMVHt!0EP?Za>u*v7au-bvo1S zDNHPNgrFv6mo-wu9^Q_DtlYOZ`HE-%^&&^P`j!944rP~_@lXAc*UXb|ygr@qpZEUX zd);|*@bCZQUbuxO0|VROfBOUU*o5x;&}EkT+~3rP;*Y#9`MKoxBK;E5Zz25}((fVt zBGPXn{VLM$BK!iWL!taePmon z#*JiLNyeRITuR2RWL!(ey<}WW#?540O~&11Tu#RAWL!_i{bXK%%o~t-1v2kI<|W9y z1)0|%^B!bggv^_ec@;A6l61Bw`KTz?2^1TvFC#B;mUATwb?&s`>;B*z@U+mqC2`2J zL{?mjqxi;VzQg^mu=X0 zc=t9NH}WDjku`75??7I}@;TcEurR}hv?sez6H#g@mj`wqyu*D9d6@2m#`!$RYoyD) zYPxKVdN3}=tb^dkQlG6uk(Uv%Si*iA{Ml5oIc-1ANytrbegvK~R_&tMV?TV8(7)?` z33U%|Il=!~5hkCH>z-XdstgaqKTIxr)(C!hg2j;419?#ItMZGHC%M@gsQV6i7EXgC zb{bfm#VYc#AM&=`iSh;D^p8ezMS=J|9zO;-kVm;Ocv4P29C@sryCdttcV9-=^+e$F z({DRVBd=no>v13@3VoCwg=Zy!%L}h}7QqH8wwq+DBCnDjbt#ZD4xcA@*|`S1zM|b- z3;d}}En5wFmiH20M`YrWm%rpWlnG{189%)7JnA{O+>e+AuZY;7>3Bf`eT>~B)M0;1 z`~!af1gB(&4&{Mw4F2IzNJRgLxNLS4*z>)U0xe&_Z*vWXsd!Y&BrLY~BKjw!q-U*% z|4_KbTy1R<)>Q%oIRe1UlI^nVQxNA5*zCCvR#STwdmp@P;+A0*II8sne;RD~``OTl zIq-7d9W6CiaSp`>#o?3iZ|oJ9zoytY-bel`SSD5~z&H(gw{p$lJMf3bhaD9>uc6n{ zC-GrH_)lwlTBP)mCo)Xh#&HHb@p?SGG85+pSv-E03*KQB=uw-Eb(7WU^5tN&@!sN$ zJotNzJ%fGVKWabJVhRwyyw@8Rf`67+w|qLN5P8!jAZqx#g5_QIu8&pRE7-+!{3|BT|e-s1bN3IF6UL?!LGLAvQq3rUBzp- zAH1DUUEZ}E=NSfHVvhtX=`R{sS%Ex#)KqvY*qkw#`zo#nd3_iZq4Fk!tuC99*Wgbd zo4*sRqvkXogZo*!$H~WoudYsBaR_;i?D?tBy1~fP7Zp5&zbw4gh&oqYtw=3D=RV?D z8I$1$;Kh7$_t=rg+N5$WiVObl5^uXP8pZ?X@J4BavsL(fAKXH`wp-ct5ZJ%(b$4_+ z#`mh!=vlC9;79-$o*z38iz^i`n1rohxr_NwcFl1C6%XhJbPS|oKCa9^{6!po*2bMX z5A`FE_I)2}3_`_A{&o zzcf777>anSXR|z8AlNl*eC!GG1yz|jwYp%ATVlV%Ij~OEJNAhWyzz#=sR!~A`+`h< zmP4QL*g0`bApg=bX~FyeTx7h^#2WdZV^J*`l)mzno#2rDg>#KW#q8^#&pajd7o9}E z=qkY7t$vN!EAI6dy5 z2ow6JZgicWx)c0DN@MN?`bTftK40t@c%%4l_vgvzS1kTV^fZ{aYlEpN@_*l!_?-#{ zLw>E#YVa$@r0sFUk0mj8Dnl{yj9hA?OI_`{n=S;7jZv@U+;r2rqeUlT!K1Y0m9qzurL*y#%pV{>HW&k+Z zd#T(yu!+^DG-L2lmFdG`=)3&-d6v~;@Dg3sugu_2#sa2&umg72?^i8H|7x4&Ht8^M zC|CLdJLn-R!4m7e;EQ@}a`(~q-ePPM0}uGkE~~iPxW4q-=jf-f3px9Z_c!7Fp)o$v zRNg9SyY|dD?(eYlWXEo>K=npvN%Wz2_s{a<0*^6T#Z#YO`Ih5K4eZ8(eEugx_&rWj zAKk6MC;Wa427y)ITSra7P8fV?maoTnHQsBvTnhGyRc{prGit_ZMS;&Ky$fEB-@ENb z?yx)f;+B zN0AxW6_a_5amM(*mF#;&Ux5wP6V4sP`<3K8PTdCE1SyF1;rHid?+`r>entE3`y1oq zUCHu6m%9G3a*qvo1yAI@#o%oxmKpL<<2Scgn-e_T{L0Z0d9RMl<6R5Ew3WL=nJ!^n z=Ioj)o50hbObvsf*AL&W6d>ZSM0^Tsr=UM|04zr zoD&JttB=B-|3(9B*w2s8DxU5-27f|7T5&Bng|XFC+Z%N#?(0W~U_WDye6tkv!G5Su zVI$|ke{aPdpFW9oyzRM)3SfbOnI~$9kF2-6bjgLiYCA3eg!>fs6WzPlU=MD2_H8Wi zH2e|EUJfm={rD2!2z)mwYgre5F#WqqUF~h4&JpD=)N%E}`^O^I4BtmQ6eb%XsNjpf zJbRZ$N`rN1c2)(bvk|S=ihO~1ic{>RBlZ2a7a1r}=d-Y%zSaEL74e*Gx59j|vh`+@ zcZdgjd-RN25uaH;uC8m@3xDI@PM30UkXJyWorMH#raR-=CGfMr4V|w|QP*)Y&)FGV z)uO1r*ckoIj(!hc1dcp^qPhC=b5uX#l72v|DdrB?q2mA zZdnRGv@1GadOg-}ZsmNgN4z;M8A@58S)-{Ve>mjX$|Vjls>kY`mROSD`)2QA@?kI~Td7 z@{1#0^zxFX&Y_f!D@YZGKb9!;is>ylk@t^*WG*X7NUQgQfM!AZe^xs=TUz)amaP!lWJv0oB_)Tj z%_Z~LptgC8LM2Tq(n&JAhGWPS8Ip=PnUe5~+uk``j z)mx4`V!co#`Aa{`8)>nS$=UrD@6~VcloA7f4v4y>(S-HD=9<27upVoWruN+#T<@8?#3@~hRw};Rg0*ku!oOxagi*fDeWS#FFz-ZR zK{n{%Zz$;%CB38bgqQRk;VmV-rlj|j^rDj9 zRMM+TdRIv=E9q?|y{@G9mGr`r-dNHrOL}KXFD>bt4n%!NiQ$y z?Ipdwr1zKf0+Zfg(ko1QheE$N9-K5u>^nOzaFF5H9Pb9qJ zq<37B@RGCVE4EjY&4yutrmeXDu0Qvs?|C469o8Gu-hrbBUEHeie9KH_q%Xz&TW;yQ z`ZPQrw^VYLYJ>6rh8^Fq|K-cUQY&Y0#;nZR>|0ph>?u-bf|HDmmDy_{*~0)aT@D++PLcY*??gw+LZRoAZf6Kn0Z0(4|sTj z=pnA)uTJu|M#wL0Z~be?AMF0VZQv&I5t6aNa;)e5`1}Ln2FPFB-Mu0w2|T8EhN_7B z<@WlMOFx2-HFBJt#Qk<=%QFR5d~~47|FRtReQur|z3BjcwJb932=X!R`MuH4!0!a4 zsT;N-pHf_XkQ?z=%NtHj3&aCOZL|ONj8Al*J;6yu{`0G-fj`PS#!?<=A|GEHQhDZ2 z@M8HMfpYN9P_gBeV3!Xfv#fZ;N~^Z%6WEy^S!jiLCHJtTfdXQ|k^I7z-}SJc~--PUxL^zXn6Es z1m1l=D9YcQg?IK zdt_Ukmh&iVN45uY%+qY9!quyQ`G3`vj zYQ9l7G*NE}#o>0c;1h8^45lvXAykHI7RL*^-)ZxcjyO&{QL`It&r{PXih4Sv*ECcE z7JbP5E#xTPH`)F&qy$WHy(75K26@;=Ra~Cn_`$7IVF&E{5EUGzgZq`j3m%-3rv3Wx zMU^@@?G*R61;nwlSs!zEg4KD8!mcCE{#&R^Q4aicv!Sga;^NOdZxxq-&!=n9rGv2V zKt|oEA{ygFuY;iT+Z0!4d!llyIXw`d3M3LPc-n@lNS0A z))ko3Ew?A5uzy=EY1jhm2D`@NHWh*+^UkXK;@X*IW(i!?9}L?FV3a(SD{co=c`zx}2;5-{AIL$MBM-(OR+;9)qtL zYx8e*$Nq6~A8jx2K(t19tqa;)B6oN@nB$u08*8-BPs`wU*5J$)zn_`5LS8bHGx!Er zblDTtAUr2%HUwu{qoydb&$vPPCeFt z4eUU%uMPrdkKA8th4wfQ<~=4OjP{f`{X7BXHfpPNHuGbjR)oIgZtzRjq(a$M3EJSlf?-DN)PvsvY7 z*#_?09k)*$`^pRgC{|P)f9a6&n^l5%PJ2vY*h;`qK6L`uEshA zy`WD5{ZH)t*7u&0=$~&6_iKWO`F@WsmO);ZD@E!Ic<-N2%4aa|W%LePhk||cl>#py zZ=BsBT$ThL=!+F_lgIgJnDjpdlj9pheh=HPHOmudY!iVsEg7HCEhj)U75MN9JR)q8Rm_%rAC7W-s;KXHVQ0uGydyTFTaIH>ufb~Ft6^GqS*L6mDt zEb#S#8T0;1HQ-xqyipC{mPf&Uz2H6rCq+we*Hq+ld3;|sL5^1eocl)Bb4x1bO+H@7 zqEHK(?eWXXTo|`6mYxo0fJ-%v0^QQ_{`ie$j=zD~bw40PiReaK++1eRu?B5%q)tDa z32vd@vS6`iv4M6N_>M=U6epPGRkhw7{J8zmp~eiXpJ}hJG6y$BwrYBSdkVYP?*Z37 zRu^f+@z%S`_82T^m*e0H-aqKZ(+qaK zmk@Rl9OHJhwFNw}82G*v+)-bUUBLRjz5R)4aF?`SrWx4b)|aJ=9u5-uI*-V28Dh% z?OKCNK(wM_?VsXhWH@R?zLNr0J9wU%gX_O460-9X+Wn!Hime#x zCnL79y%5ar_LHfO`Ty6$?vK`j4HVWW3m9S@IqReH2h=rRR%4Go;;O^}-&ZWZR{CKt zzZZxfGFAjI8o+O8zt}2+*=-}gc3eyac_ytiSCM*@f2%j**rbp9qgKF0;txy9 zglTXNx7$DwesA-dQyo&cUfZI1F((}VS=7BEI(RoPV=@Tmuh{qOi!b=YSiH~&T%S)G zwKxuZf8`$48C-w%w8!c?aLakc&K~fFc*Ejh@QsGx6fba*ulD=|Sm*ZVfP1*!HPofa zf@sg(9;r!PI3A@Tqgx5Qld7va4Hj+^UO2>RZyo8C960{gu!qTkV2V+?g*%QfP}=X8 S3*P&=bk^1Ae|?$kzyAT%W_Sny literal 0 HcmV?d00001 diff --git a/tests/input/geol_clip_no_gaps.shx b/tests/input/geol_clip_no_gaps.shx new file mode 100644 index 0000000000000000000000000000000000000000..78d33f6fad506c30cb2a02c956aeb3c09e2ed983 GIT binary patch literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ literal 0 HcmV?d00001 diff --git a/tests/input/structure_clip.cpg b/tests/input/structure_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/input/structure_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/input/structure_clip.dbf b/tests/input/structure_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..7f15994662bc5ff545f2de71f06d3f62ed447adb GIT binary patch literal 45216 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 YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- literal 0 HcmV?d00001 diff --git a/tests/input/structure_clip.shx b/tests/input/structure_clip.shx new file mode 100644 index 0000000000000000000000000000000000000000..cb743f810babb48dc0aae0199ad824ac44b006f8 GIT binary patch literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu literal 0 HcmV?d00001 From 2488fabe502b5f4a437f90a23d6c843074a96d6a Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Sat, 6 Sep 2025 14:05:46 +0800 Subject: [PATCH 050/116] add validation in sampler --- m2l/processing/algorithms/sampler.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 9e4ec6e..b98b1ad 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -87,6 +87,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.INPUT_DTM, "DTM", [QgsProcessing.TypeRaster], + optional=True, ) ) @@ -149,11 +150,20 @@ def processAlgorithm( spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) sampler_type = ["Decimator", "Spacing"][sampler_type_index] + + if spatial_data is None: + raise QgsProcessingException("Spatial data is required") + + if sampler_type is "Decimator": + if geology is None: + raise QgsProcessingException("Geology is required") + if dtm is None: + raise QgsProcessingException("DTM is required") # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) - dtm_gdal = gdal.Open(dtm.source()) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None if sampler_type == "Decimator": feedback.pushInfo("Sampling...") From 38a552a54e2cfa575bce5282ec07d3ecb8c638f0 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 12:37:06 +0800 Subject: [PATCH 051/116] spacing decimator test --- m2l/processing/algorithms/sampler.py | 12 +++- tests/sampler_decimator_test.py | 98 ++++++++++++++++++++++++++++ tests/sampler_spacing_test.py | 80 +++++++++++++++++++++++ 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 tests/sampler_decimator_test.py create mode 100644 tests/sampler_spacing_test.py diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 9e4ec6e..b98b1ad 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -87,6 +87,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.INPUT_DTM, "DTM", [QgsProcessing.TypeRaster], + optional=True, ) ) @@ -149,11 +150,20 @@ def processAlgorithm( spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) sampler_type = ["Decimator", "Spacing"][sampler_type_index] + + if spatial_data is None: + raise QgsProcessingException("Spatial data is required") + + if sampler_type is "Decimator": + if geology is None: + raise QgsProcessingException("Geology is required") + if dtm is None: + raise QgsProcessingException("DTM is required") # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) - dtm_gdal = gdal.Open(dtm.source()) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None if sampler_type == "Decimator": feedback.pushInfo("Sampling...") diff --git a/tests/sampler_decimator_test.py b/tests/sampler_decimator_test.py new file mode 100644 index 0000000..8106f88 --- /dev/null +++ b/tests/sampler_decimator_test.py @@ -0,0 +1,98 @@ +""" +qgis python console: + ``` + import sys + dir = your_directory_to_the_plugin_code + sys.path.append(dir) + import unittest + from tests.sampler_decimator_test import TestSamplerDecimator + suite = unittest.TestLoader().loadTestsFromTestCase(TestSamplerDecimator) + unittest.TextTestRunner(verbosity=2).run(suite) + ``` +""" + +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis +from m2l.processing.algorithms.sampler import SamplerAlgorithm + +class TestSamplerDecimator(unittest.TestCase): + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + self.structure_file = self.input_dir / "structure_clip.shp" + self.dtm_file = self.input_dir / "dtm_rp.tif" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + self.assertTrue(self.structure_file.exists(), f"structure not found: {self.structure_file}") + self.assertTrue(self.dtm_file.exists(), f"dtm not found: {self.dtm_file}") + + def test_decimator_1_with_structure(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + structure_layer = QgsVectorLayer(str(self.structure_file), "structure", "ogr") + dtm_layer = QgsRasterLayer(str(self.dtm_file), "dtm") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertTrue(structure_layer.isValid(), "structure layer should be valid") + self.assertTrue(dtm_layer.isValid(), "dtm layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + self.assertGreater(structure_layer.featureCount(), 0, "structure layer should have features") + + QgsMessageLog.logMessage(f"geology layer valid: {geology_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"structure layer valid: {structure_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm layer valid: {dtm_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm source: {dtm_layer.source()}", "TestDecimator", Qgis.Critical) + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"structure layer: {structure_layer.featureCount()} features", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"spatial data- structure layer", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"sampler type: Decimator", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"decimation: 1", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm: {self.dtm_file.name}", "TestDecimator", Qgis.Critical) + + algorithm = SamplerAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'DTM': dtm_layer, + 'GEOLOGY': geology_layer, + 'SPATIAL_DATA': structure_layer, + 'SAMPLER_TYPE': 0, + 'DECIMATION': 1, + 'SPACING': 200.0, + 'SAMPLED_CONTACTS': 'memory:decimated_points' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + + try: + QgsMessageLog.logMessage("Starting decimator sampler algorithm...", "TestDecimator", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestDecimator", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('SAMPLED_CONTACTS', result, "Result should contain SAMPLED_CONTACTS key") + + QgsMessageLog.logMessage("Decimator sampler test completed successfully!", "TestDecimator", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Decimator sampler test error: {str(e)}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestDecimator", Qgis.Critical) + + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestDecimator", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestDecimator", Qgis.Critical) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/sampler_spacing_test.py b/tests/sampler_spacing_test.py new file mode 100644 index 0000000..bd667fb --- /dev/null +++ b/tests/sampler_spacing_test.py @@ -0,0 +1,80 @@ +""" +qgis python console: + ``` + import sys + dir = your_directory_to_the_plugin_code + sys.path.append(dir) + import unittest + from tests.sampler_spacing_test import TestSamplerSpacing + suite = unittest.TestLoader().loadTestsFromTestCase(TestSamplerSpacing) + unittest.TextTestRunner(verbosity=2).run(suite) +``` +""" + +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis +from m2l.processing.algorithms.sampler import SamplerAlgorithm + +class TestSamplerSpacing(unittest.TestCase): + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + + def test_spacing_50_with_geology(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"spatial data- geology layer", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"sampler type: Spacing", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"spacing: 50", "TestSampler", Qgis.Critical) + + algorithm = SamplerAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'DTM': None, + 'GEOLOGY': None, + 'SPATIAL_DATA': geology_layer, + 'SAMPLER_TYPE': 1, + 'DECIMATION': 1, + 'SPACING': 50.0, + 'SAMPLED_CONTACTS': 'memory:sampled_points' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + try: + QgsMessageLog.logMessage("Starting spacing sampler algorithm...", "TestSampler", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestSampler", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('SAMPLED_CONTACTS', result, "Result should contain SAMPLED_CONTACTS key") + + QgsMessageLog.logMessage("Spacing sampler test completed successfully!", "TestSampler", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Spacing sampler test error: {str(e)}", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestSampler", Qgis.Critical) + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestSampler", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestSampler", Qgis.Critical) + +if __name__ == '__main__': + unittest.main() From 9341a8ecb5d24c56515a207c85fa4d75352056ae Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 13:26:05 +0800 Subject: [PATCH 052/116] refactor sampler tests for cicd compatibility --- .github/workflows/tester.yml | 110 +++++++++--------- tests/{ => qgis}/input/dtm_rp.tif | Bin tests/{ => qgis}/input/dtm_rp.tif.aux.xml | 0 tests/{ => qgis}/input/faults_clip.cpg | 0 tests/{ => qgis}/input/faults_clip.dbf | Bin tests/{ => qgis}/input/faults_clip.prj | 0 tests/{ => qgis}/input/faults_clip.shp | Bin tests/{ => qgis}/input/faults_clip.shx | Bin tests/{ => qgis}/input/folds_clip.cpg | 0 tests/{ => qgis}/input/folds_clip.dbf | Bin tests/{ => qgis}/input/folds_clip.prj | 0 tests/{ => qgis}/input/folds_clip.shp | Bin tests/{ => qgis}/input/folds_clip.shx | Bin tests/{ => qgis}/input/geol_clip_no_gaps.cpg | 0 tests/{ => qgis}/input/geol_clip_no_gaps.dbf | Bin tests/{ => qgis}/input/geol_clip_no_gaps.prj | 0 tests/{ => qgis}/input/geol_clip_no_gaps.shp | Bin tests/{ => qgis}/input/geol_clip_no_gaps.shx | Bin tests/{ => qgis}/input/structure_clip.cpg | 0 tests/{ => qgis}/input/structure_clip.dbf | Bin tests/{ => qgis}/input/structure_clip.prj | 0 tests/{ => qgis}/input/structure_clip.shp | Bin tests/{ => qgis}/input/structure_clip.shx | Bin .../test_sampler_decimator.py} | 28 ++--- .../test_sampler_spacing.py} | 28 ++--- 25 files changed, 86 insertions(+), 80 deletions(-) rename tests/{ => qgis}/input/dtm_rp.tif (100%) rename tests/{ => qgis}/input/dtm_rp.tif.aux.xml (100%) rename tests/{ => qgis}/input/faults_clip.cpg (100%) rename tests/{ => qgis}/input/faults_clip.dbf (100%) rename tests/{ => qgis}/input/faults_clip.prj (100%) rename tests/{ => qgis}/input/faults_clip.shp (100%) rename tests/{ => qgis}/input/faults_clip.shx (100%) rename tests/{ => qgis}/input/folds_clip.cpg (100%) rename tests/{ => qgis}/input/folds_clip.dbf (100%) rename tests/{ => qgis}/input/folds_clip.prj (100%) rename tests/{ => qgis}/input/folds_clip.shp (100%) rename tests/{ => qgis}/input/folds_clip.shx (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.cpg (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.dbf (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.prj (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.shp (100%) rename tests/{ => qgis}/input/geol_clip_no_gaps.shx (100%) rename tests/{ => qgis}/input/structure_clip.cpg (100%) rename tests/{ => qgis}/input/structure_clip.dbf (100%) rename tests/{ => qgis}/input/structure_clip.prj (100%) rename tests/{ => qgis}/input/structure_clip.shp (100%) rename tests/{ => qgis}/input/structure_clip.shx (100%) rename tests/{sampler_decimator_test.py => qgis/test_sampler_decimator.py} (90%) rename tests/{sampler_spacing_test.py => qgis/test_sampler_spacing.py} (86%) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 4543564..9ea3818 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -45,55 +45,61 @@ jobs: - name: Run Unit tests run: pytest -p no:qgis tests/unit/ - # test-qgis: - # runs-on: ubuntu-latest - - # container: - # image: qgis/qgis:3.4 - # env: - # CI: true - # DISPLAY: ":1" - # MUTE_LOGS: true - # NO_MODALS: 1 - # PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." - # QT_QPA_PLATFORM: "offscreen" - # WITH_PYTHON_PEP: false - # # be careful, things have changed since QGIS 3.40. So if you are using this setup - # # with a QGIS version older than 3.40, you may need to change the way you set up the container - # volumes: - # # Mount the X11 socket to allow GUI applications to run - # - /tmp/.X11-unix:/tmp/.X11-unix - # # Mount the workspace directory to the container - # - ${{ github.workspace }}:/home/root/ - - # steps: - # - name: Get source code - # uses: actions/checkout@v4 - - # - name: Print QGIS version - # run: qgis --version - - # # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 - # # - name: Setup plugin - # # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} - - # - name: Install Python requirements - # run: | - # apt update && apt install -y python3-pip python3-venv pipx - # # Create a virtual environment - # cd /home/root/ - # pipx run qgis-venv-creator --venv-name ".venv" - # # Activate the virtual environment - # . .venv/bin/activate - # # Install the requirements - # python3 -m pip install -U -r requirements/testing.txt - - # - name: Run Unit tests - # run: | - # cd /home/root/ - # # Activate the virtual environment - # . .venv/bin/activate - # # Run the tests - # # xvfb-run is used to run the tests in a virtual framebuffer - # # This is necessary because QGIS requires a display to run - # xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml + test-qgis: + runs-on: ubuntu-latest + + container: + image: qgis/qgis:3.4 + env: + CI: true + DISPLAY: ":1" + MUTE_LOGS: true + NO_MODALS: 1 + PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." + QT_QPA_PLATFORM: "offscreen" + WITH_PYTHON_PEP: false + # be careful, things have changed since QGIS 3.40. So if you are using this setup + # with a QGIS version older than 3.40, you may need to change the way you set up the container + volumes: + # Mount the X11 socket to allow GUI applications to run + - /tmp/.X11-unix:/tmp/.X11-unix + # Mount the workspace directory to the container + - ${{ github.workspace }}:/home/root/ + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Print QGIS version + run: qgis --version + + # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 + # - name: Setup plugin + # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} + + - name: Install Python requirements + run: | + apt update && apt install -y python3-pip python3-venv pipx + # Create a virtual environment + cd /home/root/ + pipx run qgis-venv-creator --venv-name ".venv" + # Activate the virtual environment + . .venv/bin/activate + # Install the requirements + python3 -m pip install -U -r requirements/testing.txt + + - name: verify input data + run: | + cd /home/root/ + . .venv/bin/activate + ls -la tests/qgis/input/ || echo "Input directory not found" + + - name: Run Unit tests + run: | + cd /home/root/ + # Activate the virtual environment + . .venv/bin/activate + # Run the tests + # xvfb-run is used to run the tests in a virtual framebuffer + # This is necessary because QGIS requires a display to run + xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml diff --git a/tests/input/dtm_rp.tif b/tests/qgis/input/dtm_rp.tif similarity index 100% rename from tests/input/dtm_rp.tif rename to tests/qgis/input/dtm_rp.tif diff --git a/tests/input/dtm_rp.tif.aux.xml b/tests/qgis/input/dtm_rp.tif.aux.xml similarity index 100% rename from tests/input/dtm_rp.tif.aux.xml rename to tests/qgis/input/dtm_rp.tif.aux.xml diff --git a/tests/input/faults_clip.cpg b/tests/qgis/input/faults_clip.cpg similarity index 100% rename from tests/input/faults_clip.cpg rename to tests/qgis/input/faults_clip.cpg diff --git a/tests/input/faults_clip.dbf b/tests/qgis/input/faults_clip.dbf similarity index 100% rename from tests/input/faults_clip.dbf rename to tests/qgis/input/faults_clip.dbf diff --git a/tests/input/faults_clip.prj b/tests/qgis/input/faults_clip.prj similarity index 100% rename from tests/input/faults_clip.prj rename to tests/qgis/input/faults_clip.prj diff --git a/tests/input/faults_clip.shp b/tests/qgis/input/faults_clip.shp similarity index 100% rename from tests/input/faults_clip.shp rename to tests/qgis/input/faults_clip.shp diff --git a/tests/input/faults_clip.shx b/tests/qgis/input/faults_clip.shx similarity index 100% rename from tests/input/faults_clip.shx rename to tests/qgis/input/faults_clip.shx diff --git a/tests/input/folds_clip.cpg b/tests/qgis/input/folds_clip.cpg similarity index 100% rename from tests/input/folds_clip.cpg rename to tests/qgis/input/folds_clip.cpg diff --git a/tests/input/folds_clip.dbf b/tests/qgis/input/folds_clip.dbf similarity index 100% rename from tests/input/folds_clip.dbf rename to tests/qgis/input/folds_clip.dbf diff --git a/tests/input/folds_clip.prj b/tests/qgis/input/folds_clip.prj similarity index 100% rename from tests/input/folds_clip.prj rename to tests/qgis/input/folds_clip.prj diff --git a/tests/input/folds_clip.shp b/tests/qgis/input/folds_clip.shp similarity index 100% rename from tests/input/folds_clip.shp rename to tests/qgis/input/folds_clip.shp diff --git a/tests/input/folds_clip.shx b/tests/qgis/input/folds_clip.shx similarity index 100% rename from tests/input/folds_clip.shx rename to tests/qgis/input/folds_clip.shx diff --git a/tests/input/geol_clip_no_gaps.cpg b/tests/qgis/input/geol_clip_no_gaps.cpg similarity index 100% rename from tests/input/geol_clip_no_gaps.cpg rename to tests/qgis/input/geol_clip_no_gaps.cpg diff --git a/tests/input/geol_clip_no_gaps.dbf b/tests/qgis/input/geol_clip_no_gaps.dbf similarity index 100% rename from tests/input/geol_clip_no_gaps.dbf rename to tests/qgis/input/geol_clip_no_gaps.dbf diff --git a/tests/input/geol_clip_no_gaps.prj b/tests/qgis/input/geol_clip_no_gaps.prj similarity index 100% rename from tests/input/geol_clip_no_gaps.prj rename to tests/qgis/input/geol_clip_no_gaps.prj diff --git a/tests/input/geol_clip_no_gaps.shp b/tests/qgis/input/geol_clip_no_gaps.shp similarity index 100% rename from tests/input/geol_clip_no_gaps.shp rename to tests/qgis/input/geol_clip_no_gaps.shp diff --git a/tests/input/geol_clip_no_gaps.shx b/tests/qgis/input/geol_clip_no_gaps.shx similarity index 100% rename from tests/input/geol_clip_no_gaps.shx rename to tests/qgis/input/geol_clip_no_gaps.shx diff --git a/tests/input/structure_clip.cpg b/tests/qgis/input/structure_clip.cpg similarity index 100% rename from tests/input/structure_clip.cpg rename to tests/qgis/input/structure_clip.cpg diff --git a/tests/input/structure_clip.dbf b/tests/qgis/input/structure_clip.dbf similarity index 100% rename from tests/input/structure_clip.dbf rename to tests/qgis/input/structure_clip.dbf diff --git a/tests/input/structure_clip.prj b/tests/qgis/input/structure_clip.prj similarity index 100% rename from tests/input/structure_clip.prj rename to tests/qgis/input/structure_clip.prj diff --git a/tests/input/structure_clip.shp b/tests/qgis/input/structure_clip.shp similarity index 100% rename from tests/input/structure_clip.shp rename to tests/qgis/input/structure_clip.shp diff --git a/tests/input/structure_clip.shx b/tests/qgis/input/structure_clip.shx similarity index 100% rename from tests/input/structure_clip.shx rename to tests/qgis/input/structure_clip.shx diff --git a/tests/sampler_decimator_test.py b/tests/qgis/test_sampler_decimator.py similarity index 90% rename from tests/sampler_decimator_test.py rename to tests/qgis/test_sampler_decimator.py index 8106f88..088fd29 100644 --- a/tests/sampler_decimator_test.py +++ b/tests/qgis/test_sampler_decimator.py @@ -1,23 +1,19 @@ -""" -qgis python console: - ``` - import sys - dir = your_directory_to_the_plugin_code - sys.path.append(dir) - import unittest - from tests.sampler_decimator_test import TestSamplerDecimator - suite = unittest.TestLoader().loadTestsFromTestCase(TestSamplerDecimator) - unittest.TextTestRunner(verbosity=2).run(suite) - ``` -""" - import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis +from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis,QgsApplication +from qgis.testing import start_app from m2l.processing.algorithms.sampler import SamplerAlgorithm +from m2l.processing.provider import Map2LoopProvider class TestSamplerDecimator(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + def setUp(self): self.test_dir = Path(__file__).parent self.input_dir = self.test_dir / "input" @@ -93,6 +89,10 @@ def test_decimator_1_with_structure(self): finally: QgsMessageLog.logMessage("=" * 50, "TestDecimator", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) if __name__ == '__main__': unittest.main() diff --git a/tests/sampler_spacing_test.py b/tests/qgis/test_sampler_spacing.py similarity index 86% rename from tests/sampler_spacing_test.py rename to tests/qgis/test_sampler_spacing.py index bd667fb..a542b85 100644 --- a/tests/sampler_spacing_test.py +++ b/tests/qgis/test_sampler_spacing.py @@ -1,23 +1,19 @@ -""" -qgis python console: - ``` - import sys - dir = your_directory_to_the_plugin_code - sys.path.append(dir) - import unittest - from tests.sampler_spacing_test import TestSamplerSpacing - suite = unittest.TestLoader().loadTestsFromTestCase(TestSamplerSpacing) - unittest.TextTestRunner(verbosity=2).run(suite) -``` -""" - import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.testing import start_app from m2l.processing.algorithms.sampler import SamplerAlgorithm +from m2l.processing.provider import Map2LoopProvider class TestSamplerSpacing(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + def setUp(self): self.test_dir = Path(__file__).parent self.input_dir = self.test_dir / "input" @@ -76,5 +72,9 @@ def test_spacing_50_with_geology(self): finally: QgsMessageLog.logMessage("=" * 50, "TestSampler", Qgis.Critical) + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + if __name__ == '__main__': unittest.main() From 096a13e95f566efc9ee6a3d00ec70ff8adc7ad47 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 13:34:47 +0800 Subject: [PATCH 053/116] update tester.yml workflow for all branches --- .github/workflows/tester.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 9ea3818..a169c27 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -2,16 +2,12 @@ name: "🎳 Tester" on: push: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml - requirements/testing.txt pull_request: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml From 5fa83adae59e42025c44ba4dbe99cd0bedc494f4 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 13:54:03 +0800 Subject: [PATCH 054/116] change image --- .github/workflows/tester.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index a169c27..8e3ff71 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest container: - image: qgis/qgis:3.4 + image: qgis/qgis:latest env: CI: true DISPLAY: ":1" From a6c14b2e5bedc09050d89b8084e171ab3b41819b Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 14:12:55 +0800 Subject: [PATCH 055/116] update testing.txt --- requirements/testing.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements/testing.txt b/requirements/testing.txt index 1940035..d238575 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -3,3 +3,5 @@ pytest-cov>=4 packaging>=23 +shapely +geopandas \ No newline at end of file From 378f9aef78c3c523fbcea0e6cce8a8863bbec12d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 14:40:44 +0800 Subject: [PATCH 056/116] install map2loop in tester.yml --- .github/workflows/tester.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 8e3ff71..cf13a1b 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -83,6 +83,7 @@ jobs: . .venv/bin/activate # Install the requirements python3 -m pip install -U -r requirements/testing.txt + python3 -m pip install git+https://github.com/Loop3D/map2loop.git@noelle/contact_extractor - name: verify input data run: | From 06fc7ec2afac1328e641da46f6e770e18b097f57 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:15:54 +0800 Subject: [PATCH 057/116] tester.yml --- .github/workflows/tester.yml | 115 ++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 4543564..cf13a1b 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -2,16 +2,12 @@ name: "🎳 Tester" on: push: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml - requirements/testing.txt pull_request: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml @@ -45,55 +41,62 @@ jobs: - name: Run Unit tests run: pytest -p no:qgis tests/unit/ - # test-qgis: - # runs-on: ubuntu-latest - - # container: - # image: qgis/qgis:3.4 - # env: - # CI: true - # DISPLAY: ":1" - # MUTE_LOGS: true - # NO_MODALS: 1 - # PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." - # QT_QPA_PLATFORM: "offscreen" - # WITH_PYTHON_PEP: false - # # be careful, things have changed since QGIS 3.40. So if you are using this setup - # # with a QGIS version older than 3.40, you may need to change the way you set up the container - # volumes: - # # Mount the X11 socket to allow GUI applications to run - # - /tmp/.X11-unix:/tmp/.X11-unix - # # Mount the workspace directory to the container - # - ${{ github.workspace }}:/home/root/ - - # steps: - # - name: Get source code - # uses: actions/checkout@v4 - - # - name: Print QGIS version - # run: qgis --version - - # # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 - # # - name: Setup plugin - # # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} - - # - name: Install Python requirements - # run: | - # apt update && apt install -y python3-pip python3-venv pipx - # # Create a virtual environment - # cd /home/root/ - # pipx run qgis-venv-creator --venv-name ".venv" - # # Activate the virtual environment - # . .venv/bin/activate - # # Install the requirements - # python3 -m pip install -U -r requirements/testing.txt - - # - name: Run Unit tests - # run: | - # cd /home/root/ - # # Activate the virtual environment - # . .venv/bin/activate - # # Run the tests - # # xvfb-run is used to run the tests in a virtual framebuffer - # # This is necessary because QGIS requires a display to run - # xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml + test-qgis: + runs-on: ubuntu-latest + + container: + image: qgis/qgis:latest + env: + CI: true + DISPLAY: ":1" + MUTE_LOGS: true + NO_MODALS: 1 + PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." + QT_QPA_PLATFORM: "offscreen" + WITH_PYTHON_PEP: false + # be careful, things have changed since QGIS 3.40. So if you are using this setup + # with a QGIS version older than 3.40, you may need to change the way you set up the container + volumes: + # Mount the X11 socket to allow GUI applications to run + - /tmp/.X11-unix:/tmp/.X11-unix + # Mount the workspace directory to the container + - ${{ github.workspace }}:/home/root/ + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Print QGIS version + run: qgis --version + + # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 + # - name: Setup plugin + # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} + + - name: Install Python requirements + run: | + apt update && apt install -y python3-pip python3-venv pipx + # Create a virtual environment + cd /home/root/ + pipx run qgis-venv-creator --venv-name ".venv" + # Activate the virtual environment + . .venv/bin/activate + # Install the requirements + python3 -m pip install -U -r requirements/testing.txt + python3 -m pip install git+https://github.com/Loop3D/map2loop.git@noelle/contact_extractor + + - name: verify input data + run: | + cd /home/root/ + . .venv/bin/activate + ls -la tests/qgis/input/ || echo "Input directory not found" + + - name: Run Unit tests + run: | + cd /home/root/ + # Activate the virtual environment + . .venv/bin/activate + # Run the tests + # xvfb-run is used to run the tests in a virtual framebuffer + # This is necessary because QGIS requires a display to run + xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml From a2b96f6af09684f86d132223fd94fa489f360111 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:19:38 +0800 Subject: [PATCH 058/116] input file --- tests/qgis/input/faults_clip.cpg | 1 + tests/qgis/input/faults_clip.dbf | Bin 0 -> 49957 bytes tests/qgis/input/faults_clip.prj | 1 + tests/qgis/input/faults_clip.shp | Bin 0 -> 9308 bytes tests/qgis/input/faults_clip.shx | Bin 0 -> 380 bytes tests/qgis/input/folds_clip.cpg | 1 + tests/qgis/input/folds_clip.dbf | Bin 0 -> 9713 bytes tests/qgis/input/folds_clip.prj | 1 + tests/qgis/input/folds_clip.shp | Bin 0 -> 1804 bytes tests/qgis/input/folds_clip.shx | Bin 0 -> 156 bytes tests/qgis/input/geol_clip_no_gaps.cpg | 1 + tests/qgis/input/geol_clip_no_gaps.dbf | Bin 0 -> 305246 bytes tests/qgis/input/geol_clip_no_gaps.prj | 1 + tests/qgis/input/geol_clip_no_gaps.shp | Bin 0 -> 103704 bytes tests/qgis/input/geol_clip_no_gaps.shx | Bin 0 -> 588 bytes tests/qgis/input/structure_clip.cpg | 1 + tests/qgis/input/structure_clip.dbf | Bin 0 -> 45216 bytes tests/qgis/input/structure_clip.prj | 1 + tests/qgis/input/structure_clip.shp | Bin 0 -> 3404 bytes tests/qgis/input/structure_clip.shx | Bin 0 -> 1044 bytes 20 files changed, 8 insertions(+) create mode 100644 tests/qgis/input/faults_clip.cpg create mode 100644 tests/qgis/input/faults_clip.dbf create mode 100644 tests/qgis/input/faults_clip.prj create mode 100644 tests/qgis/input/faults_clip.shp create mode 100644 tests/qgis/input/faults_clip.shx create mode 100644 tests/qgis/input/folds_clip.cpg create mode 100644 tests/qgis/input/folds_clip.dbf create mode 100644 tests/qgis/input/folds_clip.prj create mode 100644 tests/qgis/input/folds_clip.shp create mode 100644 tests/qgis/input/folds_clip.shx create mode 100644 tests/qgis/input/geol_clip_no_gaps.cpg create mode 100644 tests/qgis/input/geol_clip_no_gaps.dbf create mode 100644 tests/qgis/input/geol_clip_no_gaps.prj create mode 100644 tests/qgis/input/geol_clip_no_gaps.shp create mode 100644 tests/qgis/input/geol_clip_no_gaps.shx create mode 100644 tests/qgis/input/structure_clip.cpg create mode 100644 tests/qgis/input/structure_clip.dbf create mode 100644 tests/qgis/input/structure_clip.prj create mode 100644 tests/qgis/input/structure_clip.shp create mode 100644 tests/qgis/input/structure_clip.shx diff --git a/tests/qgis/input/faults_clip.cpg b/tests/qgis/input/faults_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/faults_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/faults_clip.dbf b/tests/qgis/input/faults_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..35f7f0eb882f291f8a96c7470670505b2ea251e9 GIT binary patch literal 49957 zcmeHQOK%*x5jGMmf*gWegPa=3HE!_zK#l=&T;v}Jv)0(wMlVS6!p?2~dA`V~ zzc@ZVoqfJVetGq9R(^l|>Gs1&{Kx_+AO9;U~Sc+Tm+celsCPdA6} z@y@GW$Ith7kH^bb=}Jo9;(?_1_wrsf+dj$LTI-e;L)_BZG;RFf-S**rgVxT^-+xT6 znTyWY^wy-eZqx6=$;_!HlS8(~;g2yR<~5$(&&5S!ohi!}zWo}fzhr%cqh1=XSEdBqiH zDdb>I33&C-6%g=h8}u9CyaVZ{zP_yl{PkE#pVSqwgo?g*4uSLYcV`5Q-~OYkDFnCh z?}!z!k1pgnms(wkCB^E!iHv~pBfy>m@@wDU!Ui2na3B?Tx0QgqA!7@?mz{{$4_6B=>!RMH2jIpp4FhbC) z_oTP5L47JXkczw8O2FNaF$E=H28VVU2nh3cp9E~H#nxEO(NsGo28`{uZ0EWpSu9)+ zvUMe&5O63BJey+Y*4vFoQ!o z4FrVwyH5hPNI=UbMB_sm6oOcedCVplhovx%OH_xvG)BN485j*bo8m2O&_M(TQgL@% z3Ah_Frl17O;LuJ30b%~`lYpId2+tx5Ef(y-jmQIb&g3#19QCll7Ml{r;(5Rz&($&D zEo@Mq3J#>=?zR$eH)Kpf37Em5odyEJ{M{!3+mLMV7Q15+bs0twFfxFPoe|?exH(|Y z&qgE!vlVc}nz(^yQ@n)@I*8yvD(-G80e3^j6qJA&9NK9hAk5!=60q?eXVgHywT3!` zfF~bHn7zx%6$Bun%Q%p6Lcr@+z&OsgdKFE5DmajeyW2{@-H^LM8N zoO5vn0RmghAOfC>Lmn_jK_&!_V)$Y+Pea2o;?;S;TiBpJ6&y&#-EAe{ZpfH|5-@{9 zI}HSc`MXmB#_=?dQ^*bn)dn5JK$V|7I}A~g34tRYCcBdm1eVARJe%UUfeku{;6N(w zZYu$IL&g-8fEgUxX&@lX-<=XLD&%b5CTT{7quWzaLs_` zov*pZbV|Uh4`LvOz1jx#so+2=?rtjqcSFV$lz3Sw%q}+K7u55Mnkns!d=J@H}?QlHn`hY}Q{lvxN=nQ^A2$+}&0J?uLvh zC;>A#w9`O9n7=zEVC3i4<(dlo9h3!;a!7MP`Y(bH1y?I!EzD2C;YN#jOE{1`W(AC67H$sF;vjM=IF8|PGJ}O6 zP%L6~2x1Ex)Te?2skpnX1l$c7Q&0kCaA>E2fG~e|O2ED@wRl{%cGLjaX+}EQN9OnGvuxI3?Erv?boc1|36iAQgAFm4Le;V+u;Z3=Zuy5D@0? zP6@bF7f{J&sT4VA6E1^vXOC)Y1!W;DA|B516|hA~wzWs$wy;5cDmajeyW2{@-H^LM8NjBA2WDjf@8>j$lXL#P3_A_vq#LkA#w9`O9n7{iZU>k6mk4H^*vt*$S2|-M`q#1DsoVfG200~vs z5>G>0$a8H$;4N%Wp9&77;_kK*a5rR3K?#__p`8W-!u;JQ0h@q}f@;PN9LklAA>a^G z!kutAR)irkHvF*G<}qLp@Y*Xe76M*tgZfl(AQgAFm4Le;V+u;Z3=Zuy5D@0?J_*=j zd5ThwzC>KuHiUpJE}e^W#GOY-b}Og}0S5ln3K;h!uX}-jSKFXZAUKeUyW2{@-H^LM8NjH@zHpw}Wh4E#GH1YskNIO9s9go~G#i-WKSSNNT9AkVZm5qJw5 z)Te?2skpnX1l$c7Q&0kCaA>E2fG~e|O2CLbpbU>Q&W(xzgW}Wtd>;`mp0gFZai~5? zYe67BoL8@aH?To{DmajeyW2{@-H^LL*F?EF#-*jW?Gpj(lxL89?; zBL>bpN4r!+3+9A?V_d%pw}B1nQ^A2$+}&0J?uLvhC;>A#w9`O9n7{iZU<4m>j1l({ zSu=x81RGvBFa&EgACv$+Kju2PB!6EMtPofFC1@PZG%36;6N(w zZYu$IL&g-8fEgUxX&@lX-+dA=P649~k4J0(p@=~Qj5{~x6j27)mr_GO^-KfVcq zM|GlH8Db?iG{eM`N4#FH&wVysj)-RzZ2pnXBmepO|NbXGaOQuFrCw3oY#Pjvx2D7O zL%n!}6XfK$hxXBVLq=^kcz2W`PWDBy%8;dc(RcBR0NM-geKTFh{qF==V(!uQW{EG4 z=r@lOskp$BxYy|uZ36zDM+$P>LuI#IEsU)4q)>AG=XrwbBUe60xQ+7X9P>SIpI$$C6Lx zOB=6)Qep{$ ziLi}l>w0igfS%7iN0#WyB(}{2$1XDzQQpmxYUu-p`@t87xK7@?pC#n43kq3a`EiXQ zu5K(D80%<%7hHQl@bg^{jDK4{B?G+KC@&=4hb1+43b*e7SE^+_-x|P@qq_~?D1tkC zmaV)V#1g)l!;Uv#J;t66s9Sc1C5l7NU%mp~yw_7=>sgkZ{jsau1-$FRYqh6gEHMjR zS-udwL@->~Hsh`Q#qsIp?y@=YwT<9@G)}4)~xPh1>Q9odO^Eoq#W@bUz2Ht@Az(WxW!6T zjzl(>NuCFPSU)WO%5XVy)!Q{s6+u4E%bu4+jG&krPnDxDpWBr6B zY)(Yzj6fdIj^P)Ts$_}A=O5;J;FCfA`qKAUGTAOKF&liQORjQJ3G7h4KJ7cWWPF}; zcrHsWj!1NLJq7(28XTLH%96a=PfP2-Q~NbXR>iVpMv%@GqaYq>(AW`Ogmu4awsY}S zaHzp*q33}tS!rkdr+F}sI3DSYIpNQewl(ttQo+ZyZ%)5jce3Pk*w3xG;H%?uG*`hdx7xNEy#jw|7Ao9s z2YtWD4jUiBBfJ&I4?1pU$$1B11$nU5oA~B_9!nYy|Q+8_ig<%QUR)5ID{? zLBZ6BB~cp7FBX6wlpCiQEMUn8-+n>kGdv>D(=v)^Vtq;T!i`|dGzTr`*(^y}I7Qz9 z?31^8Q-U%}JX{kdT>RWKBA!vhf zs@77Izx*;oQmrZ?&tYxcb>Aw=RWRg~!uSY2eCw}qiuO_2fz-8*NAED?kUJ|=fOqLw z^j*v`6wR3PckP@U@AVN3NenK&S$d8~X2*KnT~@%5FY2!kx}X1h9wEqa5ACC}{53V@ z#y1!;{__;=d0{+Kpyat|>UD+``zuAB1)tg%;FNWhA!?P!);57{<=Z^IL@{LSTGwuS zj7{gJb72Z@s(GAVG`7;%>wC0jeCBC}+`Yc7dDpSO_2L$bduSh(Z4@aL z@e|i6%uo2fOXI^V!wLkWI<^3jDC&_RTgH$!cQAgv(8lW3a#4 z+haD!zF>&_wjt|E!Mg*`!1IT{FMhU^4asMScdZm}AN*Bm;LfS9Sq#~e=k$Fp{PhN(y8MwV3>jG9e6tU{ z_IVsHK9(V$d|w`HJj)~XBU26-dfftX^R=a$VAq|=u(krm`)Hc)> zu-S051DwCi@yJ=01mEf{`?$s@iAT1cd|%`EnjwqoThD*I^0&W?fE@SGJ}Mg&lX^O} zogoRXquv(b-5ZaaWF1==;^VtYPAHj2+I+otThuY6G+^-sUvPrSqd%)3Go(`8-re95 zkIc^~u6Di6kS4_eoe?QKvfp{ajh8nW^7zTt`m136j$yW|au_mGMlXNpWgeN8F<-L@ z`=tHT?8rsnKT;(MUS~37cW@Ur`ji-%HzvP zVoC1mh_^Ga4|fap?kv8@663N8(E@Od!hvmJs1@7Blq>0BU+$gWIC5VMO9XN(O_RV+ z=8kgI7N>7T%BgW}Ux)=mhB9G_b@@8C;fa`{xvJ^npF@ufth#?W$#p zIXHHpY)II7mdt)WE8q}#SDLlaDdcU-uv^ zmT1qLm3#>6e*C2Mgeug7uguQ*DR6dasXL^Jda*HL_ij7bt!V!I`k`R!l7?5#VAZf4 zO4Ih>`?R7vdcha^Hnl$31^K=5Mh@_U!=`cu0}fb^1UN@yI}C`u3Km0*098(C(|$&tXt9i&3+Z$*D-tb z1WavBZ3~l%!Vd=Xbi%fgVj7LuGnHQY`w`k+6_0lfd%Bm`nV^nNmDt<*vx5Lh;Q(hQbJE!n-0AAVQSxA@z5)Gy5+F7fMwRi@k3#!q2M(E08S z>hQDa0<#KsPGm`gdb`H~_{rq^UQz2Nu%u$UTk(7F@Ab@W^*KkI#^{fk$z_6mp2RTn4ZQ4@?@>4OzkJrt z=Lb8crS7y?CSuEwEH26}E%0__ z1sl_M$P>ZVOc;1$`MP&oTNxs8zOQj8e1rEbhQInHLykV!$BYM4U!uMPx8YJ&6Bp<2 z3)sK^slt+l>8ZwoSnJv5-Rpa>N0e*Ag)C$L_OtQ-_!;e^vJy)*o-e@u(Ji;|oqr zWkj&MH8!-p%mVxi;B?^dugX{(FI)JCu zBzwycmPF|9ymJ{m^@;4z1K0=77o~?)fHO0dhHTPhNsG)L`9H9(qMc{n8tAa3UC-Rk z3LN-u*`zT)@ZAVrG&aB61a-b} zr~92)u*psvflpB!u<)RObKk>_0bZiVY3eBGT9@k|l zUq>R=(lZ-9!~O9#znwcXar4p(9x6-wHV&Ee`77e~y=UH=g&}+EL|>_v6H8{56)2V> z-V{AsZZmTqOS;!(`T2rNohG%eKZLj}k(w+D7CoV`pb&ZG&)c(&?;&2@mt4*N9QO)7 zL+M zvmpU?x?GdHwvZ*C1ut%1ff|wfE_yDava~OHl|flh8uYL#y!#;$^CpNdIg`l}+q;6( zQo&s-4EUULa37)eYWPa9cx>vHV>ehbM8V2W0Ziwjb7LxTG;40XX)Qs|< zt|;an+DByrUVL&<$>z?>cU3ZxA8wye`)Y6vdGo@2mC48tx({DIkVq6ePwa{jAItkJSnLJwoXR5z*%R2YpG%jx}Q zj79ZZoc-zD!M}G2|I`ee-rPrW&%b^D+%r_)TK_-;^R_&Es-h0Pl2;gA=|%lyHQBe5 zyJzSe)iFmfLmRwJ;4j&4?dph;f z&T`a4GLMQ(W`h?WiQwCHk|q2215LcZyalJvY(0Vdx_Cd6X7J%>M{7SHXNmhONB>dq zb9e3Xj_0VCuIZQg#e(VDye=>`nS*%zdp(6E8H47;t)qjHs>RhYENO~U>l}?(s<}ws zVu>QYttD9h6Ji;+jwntBAKFJ{pPpP4>NFKO!}|H;U3m9nxKmKxWccOO2~sKGlwwEA zI60Pt#&zrWfLkuv4>4j{A`;OaKMis9HNQ;>k3s*d4ncEpm{0ThU9v1mp8lXT0^I0( zyJUt8^09-?xz-a*=b~d{$Z=?c zdT8wMeLf(?k~5~cj&|uhA~e%rhPN{N?;ZKCdQe$W1rjE>1?j=t6aa&fHbb;J%_c zQ99wF+hX{encA@9Ebdagwv~)^IrYE$iuO_2g5#PJwy=5dqqcAUc(+IHP4@M2hHM=d zaWfnI{*lbYT{!=5lKh(Y1O9Y#v5dDk&i`LbWWuMTj&BvWo%%Ti_aJjb)=pJ+CHjGB}F z#^W9QrllXu=|$@zDogvQEPWTE6VL|Z$nP=g_u7#gm&TFbWqm|dhYnLfo~~;1;_A## z(wUN{lvol_6nl!}i-zJ3ZK(UM`Q^uOoD^pSl^GaoSefeVaWc(N6pr-saOuLK}ZcXw0Y8kR=msUN$YzmOZx(}P2LDrF~nFb zd*WNf?Zd~cOHJw-QnGx@&p_~UjqPjK;NEb=yPAon;C)L^_wK;GqgAtgV-wCrnN^od zBYGH86q8w)2Ch)vY_e_uchBx$V&C8#HLS2sIg+0xnsavQy+Zx}W0E=1mWKi*0%5HWR2 zX?SY(B#a+p%ikBoBQ8IL&h#OVEvmBi$N)R8bCU`|p3AG>POQN`ZrUGYkt3^rCb2KD zKST^KO^pJFU)Vb87Fcgw+^*1REE)cDvs@tfn9p_XsjzcVRezH+c*mP{T3>PYYG>+5 z_b;}gwt{K6tDZqSu1Z>&6rIYFz=YM4hvMwB^PcVa>kTaV>#@YObGSSF)s9q__NjCY zJ=v1Ml8JwcbjU(h$*kbJMizb_8JB$j8fwm(YoP&q!9JgDI#R%ox7(h2f;!Ujao0U_ zuy9SetRm{jVsFX&L%_$*9X6hH6aMRLpje8!a(>+>BU`Yfk)dMPf7F?N#l52wQRBWB ztPHt>npb~g<~`FQoS*p?Y#sqNRCnTQ1AjW=_d@$3;#AgJ@k?NF_51Sb@jS9Y?VHW2 zTP%5^BC~cm&OynYDUZ@I-f{Cqo3(Ln8a0Bo*$@7lQvYrT^8dBF+I@zQzr6CH^JDn< ze7B>Pq9w>XW?xGWf=7-}yjc#m2{$U;1U~iTjn6o22*HBs7*LFiItsM8(rCuI4Z$NgIk4c{~ z&Qp+KjGy#lPb_kM}WHHMfxUc259JhRHR`v~rzN1pzxXgrweN_B^-4B8j2uA;gM zpYiI?!`|(PS~8^$zZLm5h)M+gbC19UCGMfJw9olPVqu;uL&gO9M;wQ&yA)r51HSXc zf*HI6;G4~Ft5b01Tzp6=e+}&2R^~6ez?UJm{!3-1fDKfQ=lA2jUvjkhA+G+*Xp!~_ zJ;{)Qn|zm|!QMxd;@iM`H%!qE13RZkF%tOwz}(`g%yjU9$cc{!z}_#V^TolTi^^WV zJ;#u~7!jEw_{V0s@g=vBH_amF_wN9g?@JWej{D;K(waQ3Uj3fidFNvSL&^<}WwPKO zwVMKc4k7Qx9x2h*0(YI1)2;`vIHHg*4^ErdAi-tf53MYv5TY{5+Y&>#4jUi!zRoZG`EwRgSZm2_>O7mq# zfy;xQKeWhTNZf{x{qGQW#U7+5M8mFcQ{-gwz{{TouL;BbbnTQ3UIyZDU-pxe!UYV; zs5Q>l1bfQ#IT{o(WSU=je+c3-wI#JJOvs`Q-Ve|mu;Gwhj0wK&_q#35pXvR;?|t+7 zR?U_~eppkuqmjE8yV?0=YZ!i8p<~gpX@0>_a%jn%ztQ-YVO;&;6mrA1f`m+=pZ^~p zxp}!mBKOcfD(k*>QF+BTmJB;>^Pv^*TB>FBEcn6_wT`1=dhp%iB^P4lds(vSb7v4& zBXoVU==}JBB?bGm#T>C7#fe@1N4r^aLEpbC7i?EtHb%D-zaJ~VFjL0*g%ABc#`g`* zC%J3Px!)tHt~4I39ldh;msZ?62dnZQfIV~-_B^t9&64=043VMWrhxoWb?=ck4pn=3 zfML|Fnt+4C$HnZ9Vt9*~M>%mO}R)UI65>m1c_r)Xp;y! z_&*5YBu>)M+$6ofI0+v2@Ve*weSVyjiEIBeT?Zsy9R{Ax-)p7|)PA!5-2dU0_{`6_L5I7ren=>`QTzH>8BwcZtlCi>BYWLh7vxq|W+6=2#nK mPcn=2C0n?Z4(_+6%s%#}O#Po^e@5lj%(3ppSt#U^^URp}WSf_pv)5;f#p2_Ocdy#N!7s@~n8y9>&)xM}d7`-e!K0>sVidpQ ztXWmO^BTpQP1%Fn@VlI4PY;axM@78TKTgZv5s&(}c*yH!YkVCZr`1-x6stUWj9`@i z5$}iCg?B@Y;u%|qEa4&lxdl)2op`;;ZVS$0b8d`fG$-oV>&j=n>6Z@{s8#8ENUL> zusr*GE20O%8Te9U#<4xK+OwcNs{ev%Zuw4qQq=yv1ZEwGA@VMucY&$Z%;C}_PCzIO zeB@&Y;orjOxQ?+fTHugVRtqa*I(EIuZR#TUR1_lNJZ9kLiCHJA{rSvIDCU;${@xrr z%xeE$0<#X>n=leW3B;6O>e4Y`bPVf(T46NhkV8-lD`Psg*|ZEtJPG5lCda&U62@az ze>k?pK}+rs#LRpq>zs4BJvnwwC} zEnl4;IHZr-zmpY4(w;6lZhOp#sIP9Nsb{_mw!tA!oLX2J)3MEtesxAXuFu?rVs7~! z_wD-k=xYB?^y3R!V(266Qsi{9sIC|hg{B{SkOdC8+-hNEOvkppV$_M@kUn!0in--` b$ahI+EVX|pyJExvbb@v#OW1Y-9rn5;d?Q#kUk6XE%jx?I$jj~bgD5slEl89)uso1RP zHr1+jrczB?thHlnT}ow*OJP(P_dS~^jA-|n><`=NJ@fwYnfH00dA{#+8HTYP#tgdj z6Cdd_41Sn#`tdH=+qkg$GsB9dyD22xU4P=5Jr71!oDS$Kr!eUC|9`267&MqK7tfBU zy}1M=WlwV?i4=5(Y(VVqvS)5*Ok6ffxG+6q&f4W@>$uY7{*X>Cm&3a`LR- z8ZG?@#M@W57$Ki9=r|UWL}1FXxBlZ3D6p}z?|`8WVHmysH`>Wz#G)83w9cJMqoov5 zICuG-dOYyXYD)ru{>hm#0*<$Aqzv-whuelV93^o7>+8=;ODQ<_ z)prdS69_pSI%*;Ejg6Nw?BWRQ?=kvASwi9QicyoTw-Q*VO|o!6cH%sl7#xc6%=elW zAg7>Ob!uaph(MKQR8E(S!bD-MIwFX`mPOv?k;s;nCr5_)6FAbIa@qMO3a{>ZOn>A{ z;GBG4?}TCsI}V-SI|*5As|fq02=ABdD6qqKPkZg~U=nhKrboi~5KuoYOI=b(p}URK zJw`yF)xu%X++!5HzNqCZR}knb^z;`0Na0E>^C)uxfya5z%HI@Fs4e);a;PH#x9A9a z5i)Q5#l?EI1a{dUYW*jlg4GIxr5U3MXu1XIQsg|Ts#&T>Apf|Ha3pd8&mb%N0T&`> z=Uh9TMPNqUej3+@>F@oTp`C8Aw(ME`7$3csT?7&hE=27? z+jCteaU~f98fV^E=76k?81m|3F@ZOxe&4=Jqac#`91J-_V4g~0ehRro5ca3(S?sUr zj@DJkcYYnA4!B4lBTwlrLH;H@Id{ty?9Z*e#_`B64R5WzbB{pa3|Z`A#Uxi4ET z?5rT5a+R5SW@0~XkM}UGB~b0It&7RV{_to#xZx53qp@ptF3hDMcvxa~6?J#44wyF< zYiMo0Ji#1w3k9Og9;{d2&732X-{QL^%)7S``$1)H+9l-kK+*O2VHGl)e^v`h8Hzhq zm+zV?N$=tNT(C;{-L46HZ+-5ULAH71!z|b79)Vl8*fZ8P-}>F0A_;-lS1L?=v9CL) z$+rg|!hC4P-~J1^M^d+pgSvzBqvI4f2R2`*qPy@BKrD1(~0Q%RE`z|36#(TY71b3>-8gbzIRr|C9=d1P|I$ziqjX2Q0 z`uu_RiP1|vG0OYfpRJZbnytb-ICNf|Dz?4&7xm{UUoH37FHU`Xss6OAeE^?(p2l7t z@@oEUe?Q_No(Ab*`_rKOazTmx?T5MjQvbfgX+M7fzXH2&Q0Rbt0c~-5|9L<4a?!bi z?GxjZ2WjfPR39#o?ms`{u@@%A0POAmJPm>*gC-mtf4KhC%fe*8{qodH?VnafitE>- zQB?Wqr)*W8|90T|JmvG?1$>q%e+(1<;QA%bf6M=UuzzB7zd<8r;r{-K>wkkucxZg8 z^Vw>DeZZct@(31~{pW*6SpKpGzyG_&rTXK|gZq@n;^W@&gY!8r3-3K2dr#k>%(wW! z{DJc;fG^FT_uG%5125JOlb7qSuRl9<{k8o==YuC$zF>$BozE8DGWZq+$^5531PuS> zKbK%s|CZT({@}YyMYx~l{M0;uW6>!`Y647Gx_Z%p7`NZ z+EQuY)`P`~-ForVW51_7VZT;OcDD-@)uV28_tlbY&W_0s3;4D$HuaKOFk>O~cmnG~ zd6Qx+Ud@WtlrcEZvM|bvkJk*oNIYe}mri*C_Y^n~{+n_Q%Q7Lk=U3 zagE>LSt`w<;F`pwZ7EkltE$^%^-AxrWckD^7 zSJ-zu$?o-D{m&qScZ-DGti*d{6nc_Z6!scp3HwmI|4168>V&{6A<(QZ@5M{$88K#j9i%dae4ZqFGSHHI20aFPVEOZ(l;S91V)7mg<&ewr4FgLv>VJl(B8C zEc0o($QND|Z(K~%RMA>ZHO#Wq9WXo#_Bo8E9=r+Pt)^Kx3xHH%?=;I5P1lUF=pO<1 zQh1?(rlV}w_B2bA=RZ(-w%>?my7vAv-Hw{wsvWHLdvXsec0=OWqGJsA_%26%>k=tuDLVCVIv9+O`h*7Jx# z9c__Y@mnuq>xa&LB6f8YrZDd$eRTNZxVC~@#Jk(|F(#NciEG0|lWe^;1c+;6Y@4-s zYhz6P!9{?$wxM!urh%yv!ksl>O*552w-23bL+0*!Q5_4!w+uEK`Mk5W$ zuVE5Q<|(L!1<7#_f1+m=d7xW^pKD$cy45sLsd+2tx!#$#VzmD`dGf=_Jq7^Hv*lXX zwpxOirMqR~cDiFnHQzo0^7o^DF&Z7$Cb?c=-|hMx9eL}-waH^h7Tzub&GQiH+A5*Z z+Z}D{83qCu%e8$5B?<>mOOW;PfAh|If_ewr&i&eeylrw1BNc!{+Y# z)&rEIy>9Ydd|W zldkP_%*IrMxHh@bYTku$eBQBD+;Y9^zcz7g{bNj57%u|N`a)bAac$$B$LDPM#d2-f zKA+h3G}5(6eGjm?F>HIiaczS0GHk_C9SdB&t9O>dhg8!w;X>+KE_hyV#A|cq6=@<~ z8>SEq;yfRYUYfr@x-m1}6> zIJ^D;KUhGRKyapI-yvG;I#Sy-^8AO&*K~cl74xkXW2^}D2LG(%z8wVWs1sxIA+3Yi z9o4h_g~SjP!2ME>%>IIOln@vJ0@ZzMs7)Q6u1zvt8}{99sN-6-i@3H53HnP2>>@y1 z8~Kp#&I&s65(31vA^BM0b8W+phXy{Rn9RycbF|aBi`BJhmTG9O;;Im~ZHsGz22U6rg${;<0cMojaBaeJ9ft~WGAkfP62=_sh?VP`c90MXBKtngJjjaR0ts}kx z(s>{vZ0@c zyD(t~og3kAJSr)#+4Dmnl4T0&t4l}EQl}X0r|INaZth2o!XE8SZYNCgbd`mVAPMZa zftsxxosRXX8X+(w1c+-J(r8>>TZn7ByfLR9ACExuJcPrw9e-siNeGM+fgy5j*RA*1 zFX4S2CUf@p#{hDy*Q}gv7THN9FvsS`uXKwR5~uC2?EwCWJo z)&hfe34tvHh-=#dgg%}X0pi-u3O8MBWC#$~HnIVu=Ds=t#I+68hg4|&2D}m1CO0_! z>Dp}7avj@tm7REPplNg9KU-5kg4V=q1C|WI9T>+-11fQC4HjMYh-;gWuC2?&p*n=X zt0O>M+p7=3MK^%Bwu>Hq>iiE7Ag=8P0MTheU<(1_+K6l0ni9u-B(Ci^oDP-hk83j> zM**Tu)gVK{rmjubTvbtBkh|G!U7KSl2(&I(L5TX+z(u(c8Z5f(VRLPi8zIf2;JJ#| zCK+qI9j~nkR@Y^m%Q_+>0_8^Njvv(=76O;XwKcKE*Lg!FhhxW{ zQhVdtOv7>vTQ^+AP&c_Y4JbI%byO45k+yRqsD`e~heiV{2IdEJ z!;fkY3xQ^R!Qt8{H^Q*Sms*35zz}t9_`Td?5j(ybwmr&?uxHRtIE2lOVRLu=aBZq_ zWviNPK(s#~*2K1kOu14V+cZs8cPvAYa4jEF;@VD_S9MYBw~s=HhL~)iu!k70jkq@A z+Um*w>N~`>z4{RJ+JNf5)$2thj1$);0p19zk`L(!28o*ZY6uY5_G-}6g-412acx7@ zwNV~Ysjn1qZN#;0Odye&@~_ELHkoc5?)-tcwiwdYca;iBFHsD=J3&&cqw;=Gk(s zJCW3hkgl!s7q6&DT$>zzL$6I-o7@OtuUFW2JGtKVUz@nL{xPO2j2D4seIZ@jct??1 zeoh34Ya_0$(|p5@*M<-=Y@;85+5+K6lGG&hNB>jYa}iF`Mv9N7$$y@mC1je9|D$r_tF^dphpVe zn)^{{`SyW^{T!QdHl>ZKmLTTLhML<5g|v=IVt1p(sP zkiO4<1l&vEmC-}8r`f7Aq>XoZ0re#~Mt+--a zH&?mROxspnO*2fzQ51#E10H=d73JZ|HtnjIMCWYNL4832uC3c^!GS&q_GLMl9-GMW zfR*O(=jYU$g#i}TR@xad*Y=$9ryzm1f@PS5_rcR751#Us_)64dTEVy#?++zc!y@m| zW8Xt~jM~gn8hM#u=tR=d4Z5wUTc##`;3C{ymVjm}G8(K$k3 zUxHK)_cyAImoZzsRJbkL1AaeTwA4d0!)N`BFDr$^CI$IIhkkIX1pNnvPB=}vwiCdjqUZ=z_bqh8 zNCwBtwRKysugVv2{ z_OQ9Y3+39>E8Er-OSfIebT_#+#c?#(uq;JUEs&r!x;E8tH2HTMVFifEGF1SS-l}8I=E=+jJ?t>@;DO>GFTAaUTrHIY<(kV~8 zJSe1YV61Q|Wu=P^kAf8RZGy6k;M7rD!L^x_CvYJ>s{D@%HP4pYTwAwQx{V+7tA*^3 zT0-Xq0u-<9ZO;Sw`%%C6jF)Tcwq8+g1iATiq-&_$MW{9*a3%zr=OG-f?GK*j;1rhS zyMCnCtT_2S*ng5(x-$(qT}BcC;@SqwwW-~Hfs~vO$5y9!ZO0 zo*3bz8HJu>lZwOo(*S531NFuvqPjbD&zS~K5ulMTMx;En4 zdLJ^nz!?!}))ySE?Tk%FyVM77nT9@36ZR?P^8krRKx-Zu8W;kfK}(Qk@SI*wmuL3Q z(oHsDrlG)lde19Ai*Je#wo0%t)RQ=UD7s=M{T)hY#ttHucEYBaC{tbkjZ*`Bq~ z4AoI>Q3hl>WtmUQMZWN&IE093b#pWgqJ~YFrh*8pXzmWnorSXiqL8w8n&pb7YercV z+vgM89yXVSJ@0_Au`z9S`?>XS3($3K8PE9=ggHoCe_WesLQFQxH4S5%Ytt0n1UFLG z0v2vV*QQ(Yv=*c?un@w&)$o$8t%jo0PQl&fA*{PN8|m7D%wGi+*OmqUS`{~zZVOkN zYa?CT=`)mcZKq>4rW%*VwKY1CHJ2|rSUdJ4*DKPs$qfi`ZF2Zg?O`F%tS>lR8|m7H zHNMmud<2dl%kYS>D$U4;)Pp2+4C48C53426V76T_W@{P@zn6O~Vsm5I_8=Y_g4k$J z$B_XGSgjA{}BuZ+N@aczxG1cz(eu_vi(!@k?S^7Q)Q371SXwhiZ`scE8v&tFT|C@`Yc!!edk}>x})BV zBN)c!#<1=6#?#fe|CntS_W%t72M>csQx)XF%X$b!`PVwoEyMtKn5ewuJRx~GX)fukUU35Kd`E4Vwvcx`{H(t*yN z9|94;AHs~GiSSzEgtJ?W_S1CIQDnH&2<*|`)U}04p02X+5!`P(Zs0juCTs0jpQ;f8 zLqg!vxHjzdWk_Q=#c^QfNOL(7$ z$(;TDF-Y+Z0M-DuY>?`Z73e3 zj1kxNN;CMtWwE(2YH@s&S00tI*w{P?!YmU~ zU{JTk;apI9B6QmZ1j54>@8Tz05~Gbc*bIBL1y+-aaJvs80jG?*t)6=2;GB#XRUib0 zguu|bHf*f0*B9*eU79`O+T=!jJJyk$HL6Vr3=e_r_*=tkA+<;bf$cSH>mGfw>02+E zdnpI?!>5$bgG9t@vmA|0n0R+)x8Bd!YHVaG$$PM#VsSAh1R`j8?h zt2P>po*Mx<*FtYxn`^nY0eKM&a2eguwW+oVY?x}=hVE|okgBHZ$fx`iRxq7DAt^CJ zcGPB0lwO5=;x&h)_L^VzF2jfPqxT%75OOW{?tvE)2@+NjqzEWK3V%{ae&9m{%q*Ay z@e?Lcbord}rvP|I@Hu@9(g=>S=`)LhmCt>VUBG8^Z#qr6AnhtI$ZHmHA5uh2MJ@)G z1?dtL&7m-IN^*ru^C6X*$|J0N8$O|i|Ep-v)|{51K1Yb?u$nw{t_?e08}{8~Sfe?< z))3b=zA>W~pAUg%y~5G8-SJhvU=_P@x0+7i`41i!X(I&Q3IapawVef9Y#kc5J@O%y zn+DiAG;Djlac#O|8=7eWW3GyTZEF;ZJfw=Dsg|pOb1CHAXnaT^Z7D)%K(KahJ;8hT)?pv>N@gpuuT-y;KjkpMLZ6h8$YI^SoU~_G~H<&I! z2)r@^L*&{hUK>mb;@afC8Q9zyw!PlCHq9}03;ai2TPboNlrR=~NNp2>xEU@a9niEE z*9Hn!M_xiEbg-CjNRZtDQzlpBMp#UqGMv1WjQ!@Nv*5W*ABqtct>DHGlMP^VZFj*e zj6q`NrO$<`ZSDB@$Ri&-XMHA|MnT$EiqaZQP;pr9tK6lw(6!xZc*EU1Tdq}%S@5q_ zkoW=XCKzt#Aq7!j=!bsfWkTsk+c60ArEA-s)yK4j%HSh_c_+bd9b-f1+OXFv?7Lkb zV}fZDn``^%rSpJQ>RHIi_87v=BP;HtMY5snyO+jr{}dNyH>(UJ;}EkUVIM&My(i{s zGrpze-ywDYUuEp`+A<~#brZt1X)Yv309jl0&H`MUrU=EGs<^fVDG^#+n`tTt?!Y)!nn?ut9T3!Z zjlG-6Z#OF-U!sX0Hb=Yfm2X?O{ueJ`cMA_56og-8dGzi%dJMhOcsL+)QRB72(-5kz z?RGWG;He)%zyUXuuXp`LkV0g-DRdoN`ofealcE}J9=hyx|sj?Il>+tW+~fdJ`mGHNIU zO@Hmf)}dkB>q*z9UO^x|TXSvO1#)eJYlF-Px^7vzp&2%OyBi-;$8r!}ZNLiZge4zR zxi1Gc*G73rDG#aKjHKFxz_AD*+>nmFRHt&-TpO&`Ip+G(sZ&&`Mu51s8VcGW1a=T; z)~n0q+IB#p1BAc}2pmC{ZPT}1QzKm0TF=D;Cd74tZwk}hL>JS32i~w}$j z*R|O;wp-=S;L9A)EDhC%RHiNa>SA+a*!Ftk+BDU$Ekh(nfQW5vA5ujD=@~?8Q*_nV z)fU<DC1ONfTBOD7$(rA*!5wqiSoch}V`*rmZ7#yQQ+>3l~8qAY&$YQp#-wH#XpS zZDlVZpx8!)wkkkdb#NVYOn8rOh9Nt1+_oZKn+j|j65ZB3TW-f|>o!TN$US8m*9twk z8AP=SfwzJHacy!ViM?K7-|ggj*MDtnu8rcg^*?5Gh4UfMtS>mawv*$vov*8OB|-oN zfgy5jC>{He5o~S@+a9*AO+|ish4yue3VvW

sPgE;L>n#AP!K$JA8#-%^CHcexe0 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

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h 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 S3*P&=bk^1Ae|?$kzyAT%W_Sny literal 0 HcmV?d00001 diff --git a/tests/qgis/input/geol_clip_no_gaps.shx b/tests/qgis/input/geol_clip_no_gaps.shx new file mode 100644 index 0000000000000000000000000000000000000000..78d33f6fad506c30cb2a02c956aeb3c09e2ed983 GIT binary patch literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ literal 0 HcmV?d00001 diff --git a/tests/qgis/input/structure_clip.cpg b/tests/qgis/input/structure_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/structure_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/structure_clip.dbf b/tests/qgis/input/structure_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..7f15994662bc5ff545f2de71f06d3f62ed447adb GIT binary patch literal 45216 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 YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- literal 0 HcmV?d00001 diff --git a/tests/qgis/input/structure_clip.shx b/tests/qgis/input/structure_clip.shx new file mode 100644 index 0000000000000000000000000000000000000000..cb743f810babb48dc0aae0199ad824ac44b006f8 GIT binary patch literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu literal 0 HcmV?d00001 From 79b28ad572db08758e9f3c832889b13d3394303d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:41:20 +0800 Subject: [PATCH 059/116] test_basal_contacts --- tests/qgis/test_basal_contacts.py | 116 ++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/qgis/test_basal_contacts.py diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py new file mode 100644 index 0000000..9131188 --- /dev/null +++ b/tests/qgis/test_basal_contacts.py @@ -0,0 +1,116 @@ +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.testing import start_app +from m2l.processing.algorithms.basal_contacts import BasalContactsAlgorithm +from m2l.processing.provider import Map2LoopProvider + +class TestBasalContacts(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + self.faults_file = self.input_dir / "faults_clip.shp" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + + if not self.faults_file.exists(): + QgsMessageLog.logMessage(f"faults not found: {self.faults_file}, will run test without faults", "TestBasalContacts", Qgis.Warning) + + def test_basal_contacts_extraction(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + + faults_layer = None + if self.faults_file.exists(): + faults_layer = QgsVectorLayer(str(self.faults_file), "faults", "ogr") + self.assertTrue(faults_layer.isValid(), "faults layer should be valid") + self.assertGreater(faults_layer.featureCount(), 0, "faults layer should have features") + QgsMessageLog.logMessage(f"faults layer: {faults_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) + + strati_column = [ + ["Turee Creek Group"], + ["Boolgeeda Iron Formation"], + ["Woongarra Rhyolite"], + ["Weeli Wolli Formation"], + ["Brockman Iron Formation"], + ["Mount McRae Shale and Mount Sylvia Formation"], + ["Wittenoom Formation"], + ["Marra Mamba Iron Formation"], + ["Jeerinah Formation"], + ["Bunjinah Formation"], + ["Pyradie Formation"], + ["Fortescue Group"], + ["Hardey Formation"], + ["Boongal Formation"], + ["Mount Roe Basalt"], + ["Rocklea Inlier greenstones"], + ["Rocklea Inlier metagranitic unit"] + ] + + algorithm = BasalContactsAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'GEOLOGY': geology_layer, + 'UNIT_NAME_FIELD': 'unitname', + 'FORMATION_FIELD': 'formation', + 'FAULTS': faults_layer, + 'STRATIGRAPHIC_COLUMN': strati_column, + 'IGNORE_UNITS': [], + 'BASAL_CONTACTS': 'memory:basal_contacts' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + try: + QgsMessageLog.logMessage("Starting basal contacts algorithm...", "TestBasalContacts", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestBasalContacts", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('BASAL_CONTACTS', result, "Result should contain BASAL_CONTACTS key") + + basal_contacts_layer = context.takeResultLayer(result['BASAL_CONTACTS']) + self.assertIsNotNone(basal_contacts_layer, "basal contacts layer should not be None") + self.assertTrue(basal_contacts_layer.isValid(), "basal contacts layer should be valid") + self.assertGreater(basal_contacts_layer.featureCount(), 0, "basal contacts layer should have features") + + QgsMessageLog.logMessage(f"Generated {basal_contacts_layer.featureCount()} basal contacts", + "TestBasalContacts", Qgis.Critical) + + QgsMessageLog.logMessage("Basal contacts test completed successfully!", "TestBasalContacts", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Basal contacts test error: {str(e)}", "TestBasalContacts", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestBasalContacts", Qgis.Critical) + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestBasalContacts", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestBasalContacts", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 82532dc43a4d7408229a2d8a3575db3c5f30a065 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:47:23 +0800 Subject: [PATCH 060/116] fix import in test_basal_contacts.py --- tests/qgis/test_basal_contacts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 9131188..1e0642e 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -2,7 +2,7 @@ from pathlib import Path from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication from qgis.testing import start_app -from m2l.processing.algorithms.basal_contacts import BasalContactsAlgorithm +from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider class TestBasalContacts(unittest.TestCase): @@ -61,7 +61,7 @@ def test_basal_contacts_extraction(self): ["Rocklea Inlier greenstones"], ["Rocklea Inlier metagranitic unit"] ] - + algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() From 02f9cb99617def5ee8863d7423eec3bf11a0dab7 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 8 Sep 2025 15:59:56 +0800 Subject: [PATCH 061/116] fix strati_column in test_basal_contacts --- tests/qgis/test_basal_contacts.py | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 1e0642e..19c56dc 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -43,23 +43,23 @@ def test_basal_contacts_extraction(self): QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) strati_column = [ - ["Turee Creek Group"], - ["Boolgeeda Iron Formation"], - ["Woongarra Rhyolite"], - ["Weeli Wolli Formation"], - ["Brockman Iron Formation"], - ["Mount McRae Shale and Mount Sylvia Formation"], - ["Wittenoom Formation"], - ["Marra Mamba Iron Formation"], - ["Jeerinah Formation"], - ["Bunjinah Formation"], - ["Pyradie Formation"], - ["Fortescue Group"], - ["Hardey Formation"], - ["Boongal Formation"], - ["Mount Roe Basalt"], - ["Rocklea Inlier greenstones"], - ["Rocklea Inlier metagranitic unit"] + "Turee Creek Group", + "Boolgeeda Iron Formation", + "Woongarra Rhyolite", + "Weeli Wolli Formation", + "Brockman Iron Formation", + "Mount McRae Shale and Mount Sylvia Formation", + "Wittenoom Formation", + "Marra Mamba Iron Formation", + "Jeerinah Formation", + "Bunjinah Formation", + "Pyradie Formation", + "Fortescue Group", + "Hardey Formation", + "Boongal Formation", + "Mount Roe Basalt", + "Rocklea Inlier greenstones", + "Rocklea Inlier metagranitic unit" ] algorithm = BasalContactsAlgorithm() From 91bcc09cfee6e450000beacb5c30ee1eee902740 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 10 Sep 2025 21:15:56 +0800 Subject: [PATCH 062/116] add structure and dtm parameters for SorterObservationProjections and column mapping --- m2l/processing/algorithms/sorter.py | 119 +++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 22 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ee081ab..5187d30 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -1,5 +1,7 @@ from typing import Any, Optional +from osgeo import gdal +from PyQt5.QtCore import QMetaType from qgis import processing from qgis.core import ( QgsFeatureSink, @@ -7,6 +9,7 @@ QgsField, QgsFeature, QgsGeometry, + QgsRasterLayer, QgsProcessing, QgsProcessingAlgorithm, QgsProcessingContext, @@ -15,6 +18,8 @@ QgsProcessingParameterEnum, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterField, + QgsProcessingParameterRasterLayer, QgsVectorLayer, QgsWkbTypes ) @@ -48,6 +53,8 @@ class StratigraphySorterAlgorithm(QgsProcessingAlgorithm): """ METHOD = "METHOD" INPUT_GEOLOGY = "INPUT_GEOLOGY" + INPUT_STRUCTURE = "INPUT_STRUCTURE" + INPUT_DTM = "INPUT_DTM" INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" SORTING_ALGORITHM = "SORTING_ALGORITHM" OUTPUT = "OUTPUT" @@ -100,12 +107,62 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRUCTURE, + "Structure", + [QgsProcessing.TypeVectorPoint], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.INPUT_DTM, + "DTM", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterField( + 'MIN_AGE_FIELD', + 'Minimum Age Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='MIN_AGE', + optional=True + ) + ) + + self.addParameter( + QgsProcessingParameterField( + 'MAX_AGE_FIELD', + 'Maximum Age Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='MAX_AGE', + optional=True + ) + ) + + self.addParameter( + QgsProcessingParameterField( + 'GROUP_FIELD', + 'Group Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='GROUP', + optional=True + ) + ) # enum so the user can pick the strategy from a dropdown self.addParameter( QgsProcessingParameterEnum( - self.ALGO, + self.SORTING_ALGORITHM, "Sorting strategy", options=list(SORTER_LIST.keys()), defaultValue=0, # Age-based is safest default @@ -115,7 +172,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - self.tr("Stratigraphic column"), + "Stratigraphic column", ) ) @@ -130,8 +187,10 @@ def processAlgorithm( ) -> dict[str, Any]: # 1 ► fetch user selections - in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT, context) - algo_index: int = self.parameterAsEnum(parameters, self.ALGO, context) + in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + structure: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) + dtm: QgsRasterLayer = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) sorter_cls = list(SORTER_LIST.values())[algo_index] feedback.pushInfo(f"Using sorter: {sorter_cls.__name__}") @@ -150,21 +209,34 @@ def processAlgorithm( # # NB: map2loop does *not* need geometries – only attribute values. # -------------------------------------------------- - units_df, relationships_df, contacts_df, map_data = build_input_frames(in_layer, feedback) + units_df, relationships_df, contacts_df = build_input_frames(in_layer, feedback,parameters) # 3 ► run the sorter sorter = sorter_cls() # instantiation is always zero-argument - order = sorter.sort( - units_df, - relationships_df, - contacts_df, - map_data, - ) + if sorter_cls == SorterObservationProjections: + from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame + geology_gdf = qgsLayerToGeoDataFrame(in_layer) + structure_gdf = qgsLayerToGeoDataFrame(structure) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None + order = sorter.sort( + units_df, + relationships_df, + contacts_df, + geology_gdf, + structure_gdf, + dtm_gdal + ) + else: + order = sorter.sort( + units_df, + relationships_df, + contacts_df, + ) # 4 ► write an in-memory table with the result sink_fields = QgsFields() - sink_fields.append(QgsField("strat_pos", int)) - sink_fields.append(QgsField("unit_name", str)) + sink_fields.append(QgsField("strat_pos", QMetaType.Type.Int)) + sink_fields.append(QgsField("unit_name", QMetaType.Type.QString)) (sink, dest_id) = self.parameterAsSink( parameters, @@ -190,17 +262,21 @@ def createInstance(self) -> QgsProcessingAlgorithm: # ------------------------------------------------------------------------- # Helper stub – you must replace with *your* conversion logic # ------------------------------------------------------------------------- -def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: +def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> tuple: """ Placeholder that turns the geology layer (and any other project layers) into the four objects required by the sorter. Returns ------- - (units_df, relationships_df, contacts_df, map_data) + (units_df, relationships_df, contacts_df) """ import pandas as pd - from m2l.map2loop.mapdata import MapData # adjust import path if needed + + unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' + min_age_field = parameters.get('MIN_AGE_FIELD', 'MIN_AGE') if parameters else 'MIN_AGE' + max_age_field = parameters.get('MAX_AGE_FIELD', 'MAX_AGE') if parameters else 'MAX_AGE' + group_field = parameters.get('GROUP_FIELD', 'GROUP') if parameters else 'GROUP' # Example: convert the geology layer to a very small units_df units_records = [] @@ -208,10 +284,10 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: units_records.append( dict( layerId=f.id(), - name=f["UNITNAME"], # attribute names → your schema - minAge=f.attribute("MIN_AGE"), - maxAge=f.attribute("MAX_AGE"), - group=f["GROUP"], + name=f[unit_name_field], # attribute names → your schema + minAge=float(f[min_age_field]), + maxAge=float(f[max_age_field]), + group=f[group_field], ) ) units_df = pd.DataFrame.from_records(units_records) @@ -221,8 +297,7 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: contacts_df = pd.DataFrame(columns=["UNITNAME_1", "UNITNAME_2", "length"]) # map_data can be mocked if you only use Age-based sorter - map_data = MapData() # or MapData.from_project(…) / MapData.from_files(…) feedback.pushInfo(f"Units → {len(units_df)} records") - return units_df, relationships_df, contacts_df, map_data + return units_df, relationships_df, contacts_df From 9273c176c79a0bfcf961d6a0f7fda289d4e4978f Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 12 Sep 2025 20:13:21 +0800 Subject: [PATCH 063/116] add contact as output layer --- .../algorithms/extract_basal_contacts.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index d7beb34..6223c10 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -41,6 +41,7 @@ class BasalContactsAlgorithm(QgsProcessingAlgorithm): INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' INPUT_IGNORE_UNITS = 'IGNORE_UNITS' OUTPUT = "BASAL_CONTACTS" + ALL_CONTACTS = "ALL_CONTACTS" def name(self) -> str: """Return the algorithm name.""" @@ -116,6 +117,13 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterFeatureSink( + "ALL_CONTACTS", + "All Contacts", + ) + ) + def processAlgorithm( self, parameters: dict[str, Any], @@ -149,6 +157,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) + all_contacts = contact_extractor.extract_all_contacts() basal_contacts = contact_extractor.extract_basal_contacts(strati_column) feedback.pushInfo("Exporting Basal Contacts Layer...") @@ -160,7 +169,15 @@ def processAlgorithm( output_key=self.OUTPUT, feedback=feedback, ) - return {self.OUTPUT: basal_contacts} + contacts_layer = GeoDataFrameToQgsLayer( + self, + all_contacts, + parameters=parameters, + context=context, + output_key=self.ALL_CONTACTS, + feedback=feedback, + ) + return {self.OUTPUT: basal_contacts, self.ALL_CONTACTS: contacts_layer} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" From 3576bd757cd367416fb5cd2d8e6503f501efa828 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 12 Sep 2025 20:20:40 +0800 Subject: [PATCH 064/116] change contacts_df and relationships_df --- m2l/processing/algorithms/sorter.py | 61 ++++++++++++++++------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 5187d30..2fff928 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -1,5 +1,6 @@ from typing import Any, Optional from osgeo import gdal +import pandas as pd from PyQt5.QtCore import QMetaType from qgis import processing @@ -35,6 +36,8 @@ SorterUseNetworkX, SorterUseHint, # kept for backwards compatibility ) +from map2loop.contact_extractor import ContactExtractor +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame # a lookup so we don’t need a giant if/else block SORTER_LIST = { @@ -125,6 +128,15 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterFeatureSource( + "CONTACTS_LAYER", + "Contacts Layer", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) + self.addParameter( QgsProcessingParameterField( 'MIN_AGE_FIELD', @@ -190,6 +202,7 @@ def processAlgorithm( in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) structure: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) dtm: QgsRasterLayer = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + contacts_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) sorter_cls = list(SORTER_LIST.values())[algo_index] @@ -209,29 +222,27 @@ def processAlgorithm( # # NB: map2loop does *not* need geometries – only attribute values. # -------------------------------------------------- - units_df, relationships_df, contacts_df = build_input_frames(in_layer, feedback,parameters) + units_df= build_input_frames(in_layer, feedback,parameters) # 3 ► run the sorter sorter = sorter_cls() # instantiation is always zero-argument - if sorter_cls == SorterObservationProjections: - from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame - geology_gdf = qgsLayerToGeoDataFrame(in_layer) - structure_gdf = qgsLayerToGeoDataFrame(structure) - dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - order = sorter.sort( - units_df, - relationships_df, - contacts_df, - geology_gdf, - structure_gdf, - dtm_gdal - ) - else: - order = sorter.sort( - units_df, - relationships_df, - contacts_df, - ) + geology_gdf = qgsLayerToGeoDataFrame(in_layer) + structure_gdf = qgsLayerToGeoDataFrame(structure) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None + contacts_df = qgsLayerToGeoDataFrame(contacts_layer) + relationships_df = contacts_df.copy() + if 'length' in contacts_df.columns: + relationships_df = relationships_df.drop(columns=['length']) + if 'geometry' in contacts_df.columns: + relationships_df = relationships_df.drop(columns=['geometry']) + order = sorter.sort( + units_df, + relationships_df, + contacts_df, + geology_gdf, + structure_gdf, + dtm_gdal + ) # 4 ► write an in-memory table with the result sink_fields = QgsFields() @@ -262,14 +273,14 @@ def createInstance(self) -> QgsProcessingAlgorithm: # ------------------------------------------------------------------------- # Helper stub – you must replace with *your* conversion logic # ------------------------------------------------------------------------- -def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> tuple: +def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> pd.DataFrame: """ Placeholder that turns the geology layer (and any other project layers) into the four objects required by the sorter. Returns ------- - (units_df, relationships_df, contacts_df) + units_df """ import pandas as pd @@ -292,12 +303,8 @@ def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> tuple: ) units_df = pd.DataFrame.from_records(units_records) - # relationships_df and contacts_df are domain-specific ─ fill them here - relationships_df = pd.DataFrame(columns=["Index1", "UNITNAME_1", "Index2", "UNITNAME_2"]) - contacts_df = pd.DataFrame(columns=["UNITNAME_1", "UNITNAME_2", "length"]) - # map_data can be mocked if you only use Age-based sorter feedback.pushInfo(f"Units → {len(units_df)} records") - return units_df, relationships_df, contacts_df + return units_df From fb873f333b0e5a342e10ae2ba0dfc16cd218733f Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 12 Sep 2025 21:37:00 +0800 Subject: [PATCH 065/116] add contact layer in sorter --- m2l/processing/algorithms/sorter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 8e16f42..ca79b66 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -61,6 +61,7 @@ class StratigraphySorterAlgorithm(QgsProcessingAlgorithm): INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" SORTING_ALGORITHM = "SORTING_ALGORITHM" OUTPUT = "OUTPUT" + CONTACTS_LAYER = "CONTACTS_LAYER" # ---------------------------------------------------------- # Metadata From a9092f455d1f75d431c1161e45787e642530e87d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 15 Sep 2025 18:52:52 +0800 Subject: [PATCH 066/116] handle dip, dipdir, orientation type for structure in sorter.py --- m2l/processing/algorithms/sorter.py | 113 +++++++++++++++++++++------- 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ca79b66..ed402cb 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -36,7 +36,6 @@ SorterUseNetworkX, SorterUseHint, # kept for backwards compatibility ) -from map2loop.contact_extractor import ContactExtractor from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame # a lookup so we don’t need a giant if/else block @@ -113,28 +112,13 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_STRUCTURE, - "Structure", - [QgsProcessing.TypeVectorPoint], - optional=True, - ) - ) - - self.addParameter( - QgsProcessingParameterRasterLayer( - self.INPUT_DTM, - "DTM", - optional=True, - ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSource( - "CONTACTS_LAYER", - "Contacts Layer", - [QgsProcessing.TypeVectorLine], - optional=True, + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.Any, + defaultValue='UNITNAME', + optional=True ) ) @@ -143,7 +127,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: 'MIN_AGE_FIELD', 'Minimum Age Field', parentLayerParameterName=self.INPUT_GEOLOGY, - type=QgsProcessingParameterField.String, + type=QgsProcessingParameterField.Any, defaultValue='MIN_AGE', optional=True ) @@ -154,7 +138,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: 'MAX_AGE_FIELD', 'Maximum Age Field', parentLayerParameterName=self.INPUT_GEOLOGY, - type=QgsProcessingParameterField.String, + type=QgsProcessingParameterField.Any, defaultValue='MAX_AGE', optional=True ) @@ -165,11 +149,67 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: 'GROUP_FIELD', 'Group Field', parentLayerParameterName=self.INPUT_GEOLOGY, - type=QgsProcessingParameterField.String, + type=QgsProcessingParameterField.Any, defaultValue='GROUP', optional=True ) ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + self.INPUT_STRUCTURE, + "Structure", + [QgsProcessing.TypeVectorPoint], + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterField( + 'DIP_FIELD', + 'Dip Field', + parentLayerParameterName=self.INPUT_STRUCTURE, + type=QgsProcessingParameterField.Any, + defaultValue='DIP', + optional=True + ) + ) + self.addParameter( + QgsProcessingParameterField( + 'DIPDIR_FIELD', + 'Dip Direction Field', + parentLayerParameterName=self.INPUT_STRUCTURE, + type=QgsProcessingParameterField.Any, + defaultValue='DIPDIR', + optional=True + ) + ) + + self.addParameter( + QgsProcessingParameterEnum( + 'ORIENTATION_TYPE', + 'Orientation Type', + options=['Dip Direction', 'Strike'], + defaultValue=0 + ) + ) + + self.addParameter( + QgsProcessingParameterRasterLayer( + self.INPUT_DTM, + "DTM", + optional=True, + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + "CONTACTS_LAYER", + "Contacts Layer", + [QgsProcessing.TypeVectorLine], + optional=True, + ) + ) # enum so the user can pick the strategy from a dropdown @@ -236,6 +276,27 @@ def processAlgorithm( relationships_df = relationships_df.drop(columns=['length']) if 'geometry' in contacts_df.columns: relationships_df = relationships_df.drop(columns=['geometry']) + + unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' + if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: + geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'}) + + dip_field = parameters.get('DIP_FIELD', 'DIP') if parameters else 'DIP' + if dip_field != 'DIP' and dip_field in structure_gdf.columns: + structure_gdf = structure_gdf.rename(columns={dip_field: 'DIP'}) + + orientation_type = self.parameterAsEnum(parameters, 'ORIENTATION_TYPE', context) + orientation_type_name = ['Dip Direction', 'Strike'][orientation_type] + dipdir_field = parameters.get('DIPDIR_FIELD', 'DIPDIR') if parameters else 'DIPDIR' + if dipdir_field in structure_gdf.columns: + if orientation_type_name == 'Strike': + structure_gdf['DIPDIR'] = structure_gdf[dipdir_field].apply( + lambda val: (val + 90.0) % 360.0 if pd.notnull(val) else val + ) + else: + structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) + + order = sorter.sort( units_df, relationships_df, From cfd09ecbaae191a9b7d23478975d8f77094a0a89 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 16 Sep 2025 22:38:11 +0800 Subject: [PATCH 067/116] fix syntax in sampler --- m2l/processing/algorithms/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index b98b1ad..726d44a 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -154,7 +154,7 @@ def processAlgorithm( if spatial_data is None: raise QgsProcessingException("Spatial data is required") - if sampler_type is "Decimator": + if sampler_type == "Decimator": if geology is None: raise QgsProcessingException("Geology is required") if dtm is None: From 1e28eb702badf49b417c79ffbb99620058485609 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 16 Sep 2025 22:47:08 +0800 Subject: [PATCH 068/116] update basal contact test --- tests/qgis/test_basal_contacts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 19c56dc..aeb208a 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -72,7 +72,8 @@ def test_basal_contacts_extraction(self): 'FAULTS': faults_layer, 'STRATIGRAPHIC_COLUMN': strati_column, 'IGNORE_UNITS': [], - 'BASAL_CONTACTS': 'memory:basal_contacts' + 'BASAL_CONTACTS': 'memory:basal_contacts', + 'ALL_CONTACTS': 'memory:all_contacts' } context = QgsProcessingContext() From 856cb423b6c6c82cad3953e15364d802c353c1ca Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 16 Sep 2025 22:50:41 +0800 Subject: [PATCH 069/116] update basal contact test to include contacts layer --- tests/qgis/test_basal_contacts.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index aeb208a..0aae78e 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -88,6 +88,7 @@ def test_basal_contacts_extraction(self): self.assertIsNotNone(result, "result should not be None") self.assertIn('BASAL_CONTACTS', result, "Result should contain BASAL_CONTACTS key") + self.assertIn('ALL_CONTACTS', result, "Result should contain ALL_CONTACTS key") basal_contacts_layer = context.takeResultLayer(result['BASAL_CONTACTS']) self.assertIsNotNone(basal_contacts_layer, "basal contacts layer should not be None") @@ -97,6 +98,14 @@ def test_basal_contacts_extraction(self): QgsMessageLog.logMessage(f"Generated {basal_contacts_layer.featureCount()} basal contacts", "TestBasalContacts", Qgis.Critical) + all_contacts_layer = context.takeResultLayer(result['ALL_CONTACTS']) + self.assertIsNotNone(all_contacts_layer, "all contacts layer should not be None") + self.assertTrue(all_contacts_layer.isValid(), "all contacts layer should be valid") + self.assertGreater(all_contacts_layer.featureCount(), 0, "all contacts layer should have features") + + QgsMessageLog.logMessage(f"Generated {all_contacts_layer.featureCount()} total contacts", + "TestBasalContacts", Qgis.Critical) + QgsMessageLog.logMessage("Basal contacts test completed successfully!", "TestBasalContacts", Qgis.Critical) except Exception as e: From 73b6e2263915fbbb4dfddb285f8f8ab702281069 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:55:33 +0930 Subject: [PATCH 070/116] feat: thickness calculator tool --- .../algorithms/thickness_calculator.py | 151 +++++++++++++----- 1 file changed, 115 insertions(+), 36 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 71f72eb..e10604d 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -25,10 +25,20 @@ QgsProcessingParameterEnum, QgsProcessingParameterNumber, QgsProcessingParameterField, - QgsProcessingParameterMatrix + QgsProcessingParameterMatrix, + QgsSettings, + QgsProcessingParameterRasterLayer, ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer +from ...main.vectorLayerWrapper import ( + qgsLayerToGeoDataFrame, + GeoDataFrameToQgsLayer, + qgsLayerToDataFrame, + dataframeToQgsLayer, + qgsRasterToGdalDataset, + matrixToDict, + dataframeToQgsTable + ) from map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint @@ -39,11 +49,13 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_DTM = 'DTM' INPUT_BOUNDING_BOX = 'BOUNDING_BOX' INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' - INPUT_UNITS = 'UNITS' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_DIPDIR_FIELD = 'DIPDIR_FIELD' + INPUT_DIP_FIELD = 'DIP_FIELD' INPUT_GEOLOGY = 'GEOLOGY' + INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' OUTPUT = "THICKNESS" @@ -73,21 +85,27 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "Thickness Calculator Type", options=['InterpolatedStructure','StructuralPoint'], allowMultiple=False, + defaultValue='InterpolatedStructure' ) ) self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterRasterLayer( self.INPUT_DTM, - "DTM", + "DTM (InterpolatedStructure)", [QgsProcessing.TypeRaster], + optional=True, ) ) + + bbox_settings = QgsSettings() + last_bbox = bbox_settings.value("m2l/bounding_box", "") self.addParameter( - QgsProcessingParameterEnum( + QgsProcessingParameterMatrix( self.INPUT_BOUNDING_BOX, - "Bounding Box", - options=['minx','miny','maxx','maxy'], - allowMultiple=True, + description="Bounding Box", + headers=['minx','miny','maxx','maxy'], + numberRows=1, + defaultValue=last_bbox ) ) self.addParameter( @@ -98,18 +116,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue=1000 ) ) - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_UNITS, - "Units", - [QgsProcessing.TypeVectorLine], - ) - ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_BASAL_CONTACTS, "Basal Contacts", [QgsProcessing.TypeVectorLine], + defaultValue='Basal Contacts', ) ) self.addParameter( @@ -119,29 +131,60 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + + self.addParameter( + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field e.g. Formation', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='Formation' + ) + ) + + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( QgsProcessingParameterMatrix( name=self.INPUT_STRATI_COLUMN, description="Stratigraphic Order", headers=["Unit"], numberRows=0, - defaultValue=[] + defaultValue=last_strati_column ) ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_SAMPLED_CONTACTS, - "SAMPLED_CONTACTS", + "Sampled Contacts", [QgsProcessing.TypeVectorPoint], ) ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_STRUCTURE_DATA, - "STRUCTURE_DATA", + "Orientation Data", [QgsProcessing.TypeVectorPoint], ) ) + self.addParameter( + QgsProcessingParameterField( + self.INPUT_DIPDIR_FIELD, + "Dip Direction Column", + parentLayerParameterName=self.INPUT_STRUCTURE_DATA, + type=QgsProcessingParameterField.Numeric, + defaultValue='DIPDIR' + ) + ) + self.addParameter( + QgsProcessingParameterField( + self.INPUT_DIP_FIELD, + "Dip Column", + parentLayerParameterName=self.INPUT_STRUCTURE_DATA, + type=QgsProcessingParameterField.Numeric, + defaultValue='DIP' + ) + ) self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, @@ -157,32 +200,63 @@ def processAlgorithm( ) -> dict[str, Any]: feedback.pushInfo("Initialising Thickness Calculation Algorithm...") - thickness_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) - dtm_data = self.parameterAsSource(parameters, self.INPUT_DTM, context) - bounding_box = self.parameterAsEnum(parameters, self.INPUT_BOUNDING_BOX, context) - max_line_length = self.parameterAsNumber(parameters, self.INPUT_MAX_LINE_LENGTH, context) - units = self.parameterAsSource(parameters, self.INPUT_UNITS, context) + thickness_type_index = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) + thickness_type = ['InterpolatedStructure', 'StructuralPoint'][thickness_type_index] + dtm_data = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + bounding_box = self.parameterAsMatrix(parameters, self.INPUT_BOUNDING_BOX, context) + max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) + structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) + structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) + unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context) + bbox_settings = QgsSettings() + bbox_settings.setValue("m2l/bounding_box", bounding_box) + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', stratigraphic_order) # convert layers to dataframe or geodataframe + units = qgsLayerToDataFrame(geology_data) geology_data = qgsLayerToGeoDataFrame(geology_data) - units = qgsLayerToDataFrame(units) basal_contacts = qgsLayerToGeoDataFrame(basal_contacts) structure_data = qgsLayerToDataFrame(structure_data) + rename_map = {} + missing_fields = [] + if unit_name_field != 'UNITNAME' and unit_name_field in geology_data.columns: + geology_data = geology_data.rename(columns={unit_name_field: 'UNITNAME'}) + units = units.rename(columns={unit_name_field: 'UNITNAME'}) + units = units.drop_duplicates(subset=['UNITNAME']).reset_index(drop=True) + units = units.rename(columns={'UNITNAME': 'name'}) + if structure_data is not None: + if structure_dipdir_field: + if structure_dipdir_field in structure_data.columns: + rename_map[structure_dipdir_field] = 'DIPDIR' + else: + missing_fields.append(structure_dipdir_field) + if structure_dip_field: + if structure_dip_field in structure_data.columns: + rename_map[structure_dip_field] = 'DIP' + else: + missing_fields.append(structure_dip_field) + if missing_fields: + raise QgsProcessingException( + f"Orientation data missing required field(s): {', '.join(missing_fields)}" + ) + if rename_map: + structure_data = structure_data.rename(columns=rename_map) sampled_contacts = qgsLayerToDataFrame(sampled_contacts) - + dtm_data = qgsRasterToGdalDataset(dtm_data) + bounding_box = matrixToDict(bounding_box) feedback.pushInfo("Calculating unit thicknesses...") - if thickness_type == "InterpolatedStructure": thickness_calculator = InterpolatedStructure( dtm_data=dtm_data, bounding_box=bounding_box, ) - thickness_calculator.compute( + thicknesses = thickness_calculator.compute( units, stratigraphic_order, basal_contacts, @@ -197,7 +271,7 @@ def processAlgorithm( bounding_box=bounding_box, max_line_length=max_line_length, ) - thickness_calculator.compute( + thicknesses =thickness_calculator.compute( units, stratigraphic_order, basal_contacts, @@ -206,17 +280,22 @@ def processAlgorithm( sampled_contacts ) - #TODO: convert thicknesses dataframe to qgs layer - thicknesses = dataframeToQgsLayer( - self, - # contact_extractor.basal_contacts, + thicknesses = thicknesses[ + ["name","ThicknessMean","ThicknessMedian", "ThicknessStdDev"] + ].copy() + + feedback.pushInfo("Exporting Thickness Table...") + thicknesses = dataframeToQgsTable( + self, + thicknesses, parameters=parameters, context=context, feedback=feedback, - ) - + param_name=self.OUTPUT + ) + return {self.OUTPUT: thicknesses[1]} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file + return self.__class__() # ThicknessCalculatorAlgorithm() From b483f006e335e735800e971824a4bd53d1aa7069 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:56:00 +0930 Subject: [PATCH 071/116] feat: raster and dataframe handling --- m2l/main/vectorLayerWrapper.py | 356 +++++++++++++++++++++++++++------ 1 file changed, 298 insertions(+), 58 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 72ff281..00c6193 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -1,5 +1,5 @@ # PyQGIS / PyQt imports - +from osgeo import gdal from qgis.core import ( QgsRaster, QgsFields, @@ -12,17 +12,79 @@ QgsProcessingException, QgsPoint, QgsPointXY, + QgsProject, + QgsCoordinateTransform, + QgsRasterLayer ) -from qgis.PyQt.QtCore import QVariant, QDateTime, QVariant - +from qgis.PyQt.QtCore import QVariant, QDateTime +from qgis import processing from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon from shapely.wkb import loads as wkb_loads import pandas as pd import geopandas as gpd import numpy as np - +import tempfile +import os + +def qgsRasterToGdalDataset(rlayer: QgsRasterLayer): + """ + Convert a QgsRasterLayer to an osgeo.gdal.Dataset (read-only). + If the raster is non-file-based (e.g. WMS/WCS/virtual), we create a temp GeoTIFF via gdal:translate. + Returns a gdal.Dataset or None. + """ + if rlayer is None or not rlayer.isValid(): + return None + + # Try direct open on file-backed layers + candidates = [] + try: + candidates.append(rlayer.source()) + except Exception: + pass + try: + if rlayer.dataProvider(): + candidates.append(rlayer.dataProvider().dataSourceUri()) + except Exception: + pass + tried = set() + for uri in candidates: + if not uri: + continue + if uri in tried: + continue + tried.add(uri) + + # Strip QGIS pipe options: "path.tif|layername=..." → "path.tif" + base_uri = uri.split("|")[0] + + # Some providers store “SUBDATASET:” URIs; gdal.OpenEx can usually handle them directly. + ds = gdal.OpenEx(base_uri, gdal.OF_RASTER | gdal.OF_READONLY) + if ds is not None: + return ds + + # If we’re here, it’s likely non-file-backed. Export to a temp GeoTIFF. + tmpdir = tempfile.gettempdir() + tmp_path = os.path.join(tmpdir, f"m2l_dtm_{rlayer.id()}.tif") + + # Use GDAL Translate via QGIS processing (avoids CRS pitfalls) + processing.run( + "gdal:translate", + { + "INPUT": rlayer, # QGIS accepts the layer object here + "TARGET_CRS": None, + "NODATA": None, + "COPY_SUBDATASETS": False, + "OPTIONS": "", + "EXTRA": "", + "DATA_TYPE": 0, # Use input data type + "OUTPUT": tmp_path, + } + ) + + ds = gdal.OpenEx(tmp_path, gdal.OF_RASTER | gdal.OF_READONLY) + return ds def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: if layer is None: @@ -42,63 +104,147 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: data[f.name()].append(str(feature[f.name()])) else: data[f.name()].append(feature[f.name()]) - return gpd.GeoDataFrame(data, crs=layer.crs().authid()) - -def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: - """Convert a vector layer to a pandas DataFrame - samples the geometry using either points or the vertices of the lines - - :param layer: _description_ - :type layer: _type_ - :param dtm: Digital Terrain Model to evaluate Z values - :type dtm: _type_ or None - :return: the dataframe object - :rtype: pd.DataFrame + return gpd.GeoDataFrame(data, crs=layer.sourceCrs().authid()) + +def qgsLayerToDataFrame(src, dtm=None) -> pd.DataFrame: """ - if layer is None: + Convert a vector layer or processing feature source to a pandas DataFrame. + Samples geometry using points or vertices of lines/polygons. + Optionally samples Z from a DTM raster. + + :param src: QgsVectorLayer or QgsProcessingFeatureSource + :param dtm: QgsRasterLayer or None + :return: pd.DataFrame with columns: X, Y, Z, and all layer fields + """ + + if src is None: return None - fields = layer.fields() - data = {} - data['X'] = [] - data['Y'] = [] - data['Z'] = [] - - for field in fields: - data[field.name()] = [] - for feature in layer.getFeatures(): - geom = feature.geometry() - points = [] - if geom.isMultipart(): - if geom.type() == QgsWkbTypes.PointGeometry: - points = geom.asMultiPoint() - elif geom.type() == QgsWkbTypes.LineGeometry: + + # --- Resolve fields and source CRS (works for both layer and feature source) --- + fields = src.fields() if hasattr(src, "fields") else None + if fields is None: + # Fallback: take fields from first feature if needed + feat_iter = src.getFeatures() + try: + first = next(feat_iter) + except StopIteration: + return pd.DataFrame(columns=["X", "Y", "Z"]) + fields = first.fields() + # Rewind iterator by building a new one + feats = [first] + list(src.getFeatures()) + else: + feats = src.getFeatures() + + # Get source CRS + if hasattr(src, "crs"): + src_crs = src.crs() + elif hasattr(src, "sourceCrs"): + src_crs = src.sourceCrs() + else: + src_crs = None + + # --- Prepare optional transform to DTM CRS for sampling --- + to_dtm = None + if dtm is not None and src_crs is not None and dtm.crs().isValid() and src_crs.isValid(): + if src_crs != dtm.crs(): + to_dtm = QgsCoordinateTransform(src_crs, dtm.crs(), QgsProject.instance()) + + # --- Helper: sample Z from DTM (returns float or -9999) --- + def sample_dtm_xy(x, y): + if dtm is None: + return 0.0 + # Transform coordinate if needed + if to_dtm is not None: + try: + from qgis.core import QgsPointXY + x, y = to_dtm.transform(QgsPointXY(x, y)) + except Exception: + return -9999.0 + from qgis.core import QgsPointXY + ident = dtm.dataProvider().identify(QgsPointXY(x, y), QgsRaster.IdentifyFormatValue) + if not ident.isValid(): + return -9999.0 + res = ident.results() + if not res: + return -9999.0 + # take first band value (band keys are 1-based) + try: + # Prefer band 1 if present + return float(res.get(1, next(iter(res.values())))) + except Exception: + return -9999.0 + + # --- Geometry -> list of vertices (QgsPoint or QgsPointXY) --- + def vertices_from_geometry(geom): + if geom is None or geom.isEmpty(): + return [] + gtype = QgsWkbTypes.geometryType(geom.wkbType()) + is_multi = QgsWkbTypes.isMultiType(geom.wkbType()) + + if gtype == QgsWkbTypes.PointGeometry: + if is_multi: + return list(geom.asMultiPoint()) + else: + return [geom.asPoint()] + + elif gtype == QgsWkbTypes.LineGeometry: + pts = [] + if is_multi: for line in geom.asMultiPolyline(): - points.extend(line) - # points = geom.asMultiPolyline()[0] - else: - if geom.type() == QgsWkbTypes.PointGeometry: - points = [geom.asPoint()] - elif geom.type() == QgsWkbTypes.LineGeometry: - points = geom.asPolyline() - - for p in points: - data['X'].append(p.x()) - data['Y'].append(p.y()) - if dtm is not None: - # Replace with your coordinates - - # Extract the value at the point - z_value = dtm.dataProvider().identify(p, QgsRaster.IdentifyFormatValue) - if z_value.isValid(): - z_value = z_value.results()[1] - else: - z_value = -9999 - data['Z'].append(z_value) - if dtm is None: - data['Z'].append(0) - for field in fields: - data[field.name()].append(feature[field.name()]) - return pd.DataFrame(data) + pts.extend(line) + else: + pts.extend(geom.asPolyline()) + return pts + + elif gtype == QgsWkbTypes.PolygonGeometry: + pts = [] + if is_multi: + mpoly = geom.asMultiPolygon() + for poly in mpoly: + for ring in poly: # exterior + interior rings + pts.extend(ring) + else: + poly = geom.asPolygon() + for ring in poly: + pts.extend(ring) + return pts + + # Other geometry types not handled + return [] + + # --- Build rows safely (one dict per sampled point) --- + rows = [] + field_names = [f.name() for f in fields] + + for f in feats: + geom = f.geometry() + pts = vertices_from_geometry(geom) + + if not pts: + # If you want to keep attribute rows even when no vertices: uncomment below + # row = {name: f[name] for name in field_names} + # row.update({"X": None, "Y": None, "Z": None}) + # rows.append(row) + continue + + # Cache attributes once per feature and reuse for each sampled point + base_attrs = {name: f[name] for name in field_names} + + for p in pts: + # QgsPoint vs QgsPointXY both have x()/y() + x, y = float(p.x()), float(p.y()) + z = sample_dtm_xy(x, y) + + row = {"X": x, "Y": y, "Z": z} + row.update(base_attrs) + rows.append(row) + + # Create DataFrame; if empty, return with expected columns + if not rows: + cols = ["X", "Y", "Z"] + field_names + return pd.DataFrame(columns=cols) + + return pd.DataFrame.from_records(rows) def GeoDataFrameToQgsLayer(qgs_algorithm, geodataframe, parameters, context, output_key, feedback=None): """ @@ -454,3 +600,97 @@ def dataframeToQgsLayer( feedback.pushInfo("Done.") feedback.setProgress(100) return sink, sink_id + + +def matrixToDict(matrix, headers=("minx", "miny", "maxx", "maxy")) -> dict: + """ + Convert a QgsProcessingParameterMatrix value to a dict with float values. + Accepts: [[minx,miny,maxx,maxy]] or [minx,miny,maxx,maxy]. + Raises a clear error if an enum index (int) was passed by mistake. + """ + # Guard: common mistake → using parameterAsEnum + if isinstance(matrix, int): + raise QgsProcessingException( + "Bounding Box was read with parameterAsEnum (got an int). " + "Use parameterAsMatrix for QgsProcessingParameterMatrix." + ) + + if matrix is None: + raise QgsProcessingException("Bounding box matrix is None.") + + # Allow empty string from settings/defaults + if isinstance(matrix, str) and not matrix.strip(): + raise QgsProcessingException("Bounding box matrix is empty.") + + # Accept single-row matrix or flat list + if isinstance(matrix, (list, tuple)): + if matrix and isinstance(matrix[0], (list, tuple)): + row = matrix[0] + else: + row = matrix + else: + # last resort: try comma-separated string "minx,miny,maxx,maxy" + if isinstance(matrix, str) and "," in matrix: + row = [v.strip() for v in matrix.split(",")] + else: + raise QgsProcessingException(f"Unrecognized bounding box value: {type(matrix)}") + + if len(row) < 4: + raise QgsProcessingException(f"Bounding box needs 4 numbers, got {len(row)}: {row}") + + def _to_float(v): + if isinstance(v, str): + v = v.strip() + return float(v) + + vals = list(map(_to_float, row[:4])) + bbox = dict(zip(headers, vals)) + + if not (bbox["minx"] < bbox["maxx"] and bbox["miny"] < bbox["maxy"]): + raise QgsProcessingException(f"Invalid bounding box: {bbox} (expect minx Date: Wed, 17 Sep 2025 14:08:26 +0800 Subject: [PATCH 072/116] fix clean up process for tests --- tests/qgis/test_basal_contacts.py | 6 +++++- tests/qgis/test_sampler_decimator.py | 6 +++++- tests/qgis/test_sampler_spacing.py | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 0aae78e..c6f9256 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -120,7 +120,11 @@ def test_basal_contacts_extraction(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index 088fd29..bb1864c 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -92,7 +92,11 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index a542b85..a49042f 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -74,7 +74,11 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() From beef36db179bf4067cbabad07d3e4a26df9f7397 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 18 Sep 2025 14:36:36 +0800 Subject: [PATCH 073/116] fix remove duplicated units_df in sorter --- m2l/processing/algorithms/sorter.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ed402cb..ac5be49 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -364,8 +364,14 @@ def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> pd.DataFr ) units_df = pd.DataFrame.from_records(units_records) + total_num_of_units = len(units_df) + units_df = units_df.drop_duplicates(subset=['name']) + unique_num_of_units = len(units_df) + + feedback.pushInfo(f"Removed duplicated units: {total_num_of_units - unique_num_of_units}") + # map_data can be mocked if you only use Age-based sorter - feedback.pushInfo(f"Units → {len(units_df)} records") + feedback.pushInfo(f"Units → {unique_num_of_units} records") return units_df From 81f267055550eda6d44ed76e09ffb2a8cd9d70e8 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Thu, 18 Sep 2025 16:41:37 +0800 Subject: [PATCH 074/116] fix build_input_frames in sorter --- m2l/processing/algorithms/sorter.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ac5be49..ee37b32 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -263,19 +263,13 @@ def processAlgorithm( # # NB: map2loop does *not* need geometries – only attribute values. # -------------------------------------------------- - units_df= build_input_frames(in_layer, feedback,parameters) + units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) # 3 ► run the sorter sorter = sorter_cls() # instantiation is always zero-argument geology_gdf = qgsLayerToGeoDataFrame(in_layer) structure_gdf = qgsLayerToGeoDataFrame(structure) dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - contacts_df = qgsLayerToGeoDataFrame(contacts_layer) - relationships_df = contacts_df.copy() - if 'length' in contacts_df.columns: - relationships_df = relationships_df.drop(columns=['length']) - if 'geometry' in contacts_df.columns: - relationships_df = relationships_df.drop(columns=['geometry']) unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: @@ -335,14 +329,14 @@ def createInstance(self) -> QgsProcessingAlgorithm: # ------------------------------------------------------------------------- # Helper stub – you must replace with *your* conversion logic # ------------------------------------------------------------------------- -def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> pd.DataFrame: +def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, feedback, parameters) -> tuple: """ Placeholder that turns the geology layer (and any other project layers) into the four objects required by the sorter. Returns ------- - units_df + (units_df, relationships_df, contacts_df) """ unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' @@ -374,4 +368,16 @@ def build_input_frames(layer: QgsVectorLayer, feedback, parameters) -> pd.DataFr feedback.pushInfo(f"Units → {unique_num_of_units} records") - return units_df + contacts_df = qgsLayerToGeoDataFrame(contacts_layer) if contacts_layer else pd.DataFrame() + if not contacts_df.empty: + relationships_df = contacts_df.copy() + if 'length' in contacts_df.columns: + relationships_df = relationships_df.drop(columns=['length']) + if 'geometry' in contacts_df.columns: + relationships_df = relationships_df.drop(columns=['geometry']) + feedback.pushInfo(f"Contacts → {len(contacts_df)} records") + feedback.pushInfo(f"Relationships → {len(relationships_df)} records") + else: + relationships_df = pd.DataFrame() + + return units_df, relationships_df, contacts_df \ No newline at end of file From a3a7082d9876fee419a9ca37e6c433f9ec83859b Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 19 Sep 2025 13:19:15 +0800 Subject: [PATCH 075/116] add contacts and basal contacts data for testing --- tests/qgis/input/all_contacts.gpkg | Bin 0 -> 176128 bytes tests/qgis/input/basal_contact.gpkg | Bin 0 -> 167936 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/qgis/input/all_contacts.gpkg create mode 100644 tests/qgis/input/basal_contact.gpkg diff --git a/tests/qgis/input/all_contacts.gpkg b/tests/qgis/input/all_contacts.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..d116bff4aba6685c7ffeb21cc3abe3395e3d3049 GIT binary patch literal 176128 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

C|3QNdSyAAf1zd?V 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 YVDz6}WTrd(%aPc>Js6evn}gB+19}Be4gdfE literal 0 HcmV?d00001 diff --git a/tests/qgis/input/basal_contact.gpkg b/tests/qgis/input/basal_contact.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..107a6899bc51e4a950df60fc17ef519820d62dc0 GIT binary patch literal 167936 zcmeFa30zKH+xLH#=F)&lhR$82d7f#oqlsoE6iJ;;nlzx2W-64-b2LyWQwU{>3W-Xl z3MFGik)hE0+YxcOuJ?Mb`}x23b3gyxC+T~xwfA28x2EGb*4n#i_Ex@OEW#@&B)~n4 z&}0ZR*w`2o34+032;iSQ{`G$`@e4;k;D0s-LvAO<=5|xfssDfTCBRTg9ma_ScAYHR*IQCev->F8vQtgIjpR$y3& zuZP=Y_ptB)w;(S!GCVXa#NFT5-A!FzUrSnPnw^EIgRSLcguU$ z>MCjI>#1mH>uP9dsH>?fkxq^lwhoq#GhL*q5=kpb>#C{iOV6JDn-m=lJxwab&cV{! zlqyLGixs%Y*TY9zN$oe;_4L)$sq9WRmX0pco-A)6m|D^3t)x#diuH}U0q%GryQXk?*1${FLw{>?6?bk zeSMvuGI+WBqW~=T&@kUXZ)qiMHR|g(IRb-1!hHI&sP|=|GJTaOfE5-3g??8l`f6Xb zBxhKnwV3u_#FU)oh??sA(B}JjZEPJJEq+&l&`w(oy1FvB4H=?5%mAlAPnMT&Aj^~R zW(9eNxCi_AdJxkbrrFrqn1=VKeg9=(CXb+?5KrGgcdYe>u)J6ytUwPI5gHL1#tLxh z?f>GQzz}6)&i`?hN$w$GtWfCNf05qrC;$En@gvh;TqI%GE-zk1^sYqTxY5q$(+E8+ zs(7BPFkcU%1=zjq%XI*Hy-ouog@AG(O9w$AUGr>>eNCF5@zu8yXL4wdr% zu#5CnzSXxDF0IsG`CmT{3iR%4`O-??n#v!#yVlnd`}gNm?e8qa5A{O{-$25X<>4FP z?vMYejvPt^1rlLCEaIE?p}{_^kRV@A%Kv8*+Q`5+{Y5K4D?lqiD?lqiD?lqiD?lqi zD?lqiD?lqiEAanT0dF>b+0n9t4Y~2fKF>WAp)QZ0z%WF!yjTb#hOk0^`cOknLt9x* zUs+wxQC(xa8ou>av~)CQk<5tghOvW2lCJTVhd70uNGZkoVz-tA()HjM&(cF8fv5R! zUu;6J82F~YXa#5mXa#5mXa#5mXa#5mXa#5mXa#5mXa#5m{&y%KJV=)Jr^o^A|Nrl( zSo$t#1!x6m1!x6m1!x6m1!x6m1!x6m1!x6m1qLYa2mk-;{{NQ@p_c=4(ZA9P&o(NS5uV<$rN{{r^9=I_Z+r3eXDB3eXDB z3eXDB3eXDB3eXDB3eXDB3jB!zf3p5x&k(BrlOyPlX$5EnXa#5mXa#5mXa#5mXa#5m zXa#5mXa#5m{%0y6&&Dk)s>sg6^V3>C{@g-jrIn?P=`=?NOB?(_ldtRl&ly6` z|7YF`T`F1uS^-)CS^-)CS^-)CS^-)CS^-)CS^-)CT7mzH0-_wUqCda;j|vFT`~Uwd z8q#@b1!x6m1!x6m1!x6m1!x6m1!x6m1!x6m1^y!x`0f6GhVXwx+37;k3eXDB3eXDB z3eXDB3eXDB3eXDB3eXDB3eXDtLj|byf7<{5hvLzP(F)KC&+tos@YJMZ!XSy}el>-of*| z-NJnRS#AOD_-Jwee~WxQ!+hL)14CIMVZY{?AyRNEMa15 zX6s-|SlUc8b#NqXZT@(iEP)(^DLKi4aIkg80p^xA#57YY(@Bnm18F(URE{*Ub#PQ7 zWWSb+2nr;^+~@hT2-!cBK~|Pn=o{qk9>xkKJc0s4!$RDB1H;A>-|n*S_6QI6z`&p| zVjhd|XN86mVLtAGgu1Le;cQ`wlFHebI;;GY-+)k;pFo(}Opc!-#68j;GD`iKjFCY> z0dD@Rg)D!!@L*3A<5!XXuOj~84BsVmvYSllALK>+$n!4>`n8PzM9=|6_??_;Kg&5F zw*jH{O-uv9uVQlk4Ke>=i~MojZ*BU!nE&SX_@iLIZH<3JKI6e$;?tDb7`}m?ti_=V z{C&e%Ztme>L4EA@YqN7x|Me5Wgh>M2;>yZw%k=v?)352j^5FiKOPW}j62GPra(yRp z3v>@)5ss!a90?m+{ByFhQW|ipmNt&2)X?!g?Wb<`XQ@6cUvHlXlZWq>lx$;2U9bQM>dm8r+t+h<5?IhFf4R{ z)bjEPl6>6a+S+VMUr+gKy|{(4{xFVHpYi`?ZBjV~-p$W7I`E#ALPJ8`d_DhIIRU>lRp z?(Y`D@^TA}2qomcWt2DP=Ms0({@eW&%Kx#iQuQH_$l&G{*VScP&iv6|srN6l!a^e4 zJc9hg0|G<)s{hvydHz_}zh?RK%2xV%=HGSf?sw@4RN6FU{gTAkqFDvA0!q?WT5z_t(O=j?j;s z`h))dp9r;E1Vt8A5tp&IK=Q-5zGsVC9%oG01KN)B${!@4F zH|O~7qkMDtY|V&)x&Cb~-%o`I2oLS^puIyd4r6HQbE1E^EmV{1bEJPd$mqN8XDRko z+mU}u+kfc&4ZQaMF#m60(TIOh%bB8otDS!JcmB0@`qMf7w4HwQtp2rF&p!`MeAV__ zA^vOHemiJDJ0147+UYl4{-t*M(>cCt`)`g@{C8?Ob13&nX9Umm|E$k{iw1mk+y7}p z{oP5vtNCL@{VChOZAksO6u%3k{XQ=FEwb?=FljO5tG42QPuqXkP=6}!|F9ADr?UMv zsP^}y9Q>ZK^xtQ!#0`_=7B@F%i<9j$DMP|SSS+`n{s!|`PdG5h6A}8qNR<7_$dvxe zywY;i=>mhkM{x!OwUj8gL_Ti#Xl`*!OSU-0zb;lt5aMxvdab_HaeuR5eVL`@`u_WE z!M>JRK5pnJZgE3Hp128rS+qYZ)VI=1w{&oHBCQC=zUbzk$|X%WO~W6(Ci>1M=j++$ z4=wIzlF6!Sqs8hfgY@1aL7;h{Y#=8{pk?#E%#rJsb8;04Pf$d#>4vpO!0<&Mf_=~{|Q8X zzWh&V(u5o#?d9t!{kwsZAe|g-Ep2ePwduf+DKf!Zt{j?Wes5;(h)A~9;i4p%ZfGRBP)Y-nE8vL4*qa^G`!3X^;szszL? zIQzyTUfZQia!}!Fm(}3@EAn4_UYa?8ITt_vOmW|ZWO1SXj0w@TDF=4_IB?(I^Oq<- zzag&81}DC!*0ikzJTtfMx!npTnfvCU!6Wdw6XC4+8<^xF#WhWXvs}riF7;ZUb}-3} zR*X67VD4;-=9)Yv8JOtRI|sbcCG>gKAr$;Z{^?NgN<~|{5qFrRJdyU`BzThOqtQGJ zf=pn5Wf=j%Xb6glrkW&x2m)C$p zeCqP!WC?QHTI=M`;A_uc2pv!%$SVsr${R!Kgyc1;<8=vAuB!C8AJ{p{@#-uCg1m3o zq(D{0i{wc~VH1h(RdZ+BR^8-D1UcdL=gYB3?=mg7J~^2nhsa&ya{}ilb@4t1@4j=( zZzy<$=Chi4;M>=p`klk|C8iC%#ew5n+ng@PfX6up1z*-DNaaa)E3e~zTjP&cEz%@N znT$9e53qsOrLJCOg48>(`-vEM`;-gjU&ayS;=^vclcD$8{gV2Q1VIv>Y8MIcL4C{8 zB|``@F8@VY*-lsTK$+=6ZEk`bw(fCr0hl$i`^t+KOwy*~*xc!0kEhX_6snozjTiHJ zzht_S%p`$fi_4f~_}Uqn&%lkct$a!)Oj2q3Iiq^8;PoUU&cjTyn!l&%6!@U{5Gise zlYARe^=T=1e&TFb`kbg*K%ypvEelg#ECnk);>Wk2{r0Q|aM;S|Mv4bK4$ zufH#BR4A|l8J`8V?!PiGvNhu8kSuB_Kx)K~W(qZnZI|LcMTHty!_%)Z;3Hy3nKhH{)C&QHtUYyYF zUx$j8`1E)*Sh{fC!V%YSeB6n44{%HJJ>6YZ1i9S!wn`j0;PT-U=dYk+bRD!=4Q?(G zYV-te+;zrg3s^@mHBt;5LvRJ|29xWJjhZeKWaG+%?MJ|!HG`awf@jH`5iA40;nnKM z1am4}Zm$K0E_Tq{0rnis+uj0xD<Y}|W6Tn@n7l%7u zC&-RX6)H~PQ|j7`h0vq<_Ucl9u)yBK92B#A7<&eQx388@dJnymR=-I<4bFCyIiK}_ zAVXDlU%v_-b5u;?VhcfDf5R1A2fi_(_1v6Rf_$ePBU}r%oiS)0^C?04wWeJ^1J1XG^yFF)nP&@RhnQnPxx?D8aeu}}HA!H> z_lL6=fOV&;9SQ>%Y3=Ar1q;i}IOGm~Sa^538(8hK;UN_;=Y!0~dEjSD^g0M|rPLtK z6<~wuvunh_o{e#?EnpFE6?+lz-kay&xpWZZIi05>!eHZWpPp9m&fzucL%<^Kg!CKh$~od!zMj>^p~Yy^bZAt3mfL_ zkamPyJT>T#PFr~=M1zhz{!P(0%o2|ik^xo-Q7nRf5WkE`Up?zTw-fAVr#A^b{)t_m6iP8>3%c9KcqF(+*JVi1%%G5EyoP`3xRxp^I%Tecv8Sr` zLU;GCjw|(^o_>GQukgTJ?g5UcUHgh%6gvtSX#_?x$+_`KQJmmyGdA#<$1}+b)=MVz zY{#@ZMc*zM{=x+rxlf=T`#AB5@HeFPU6YgrCoN?!Hvzvp z81iZyxX-^ZML~LsUXHF?$t2enIBqzP1o5_I0oTDM3sbLBtUhuTYaUMg!v3n*Rq&^W zN3S|;Vv?MSnvJi)6`j{y1G1Uq^Fh|p5^!9$1a53LDS#tv`#5*GA6Vrw@9hvRf{Yc) zG2aiK)_E>nW;{WzesB5o4%mE;MuG1Hf}Fg}813oN7O?lUFGd!`*el;b}?z|nu11woEmW$*V1{7yB;|GgDKW)Cw+odn0( z!jmg=hAlykVk7yy!KE+!9a`)OlI?U!Y7$r=YWn&w4g^{Gwqb8Ncwur(&N@dp?kg=7 z3c*9I?;0jM!M|_~xY7XDXCK4AWI90#Y|Oag2FE>qN(IL`XM)_mnS1{>aOs3q*)cN- z^6*pN6*s`n!aXtRE(F<`wnXtWIJ;FmkujGb<>&eBo0kiJD*i$JD0hNfcUp60Cb-;a zLu4e<-Om}n=mMX8cR_dU9MmU!>&h|cI%zl5Cl%v-Au(sFG{9$P>F;=k>+dYS+{Zrg zPTu8q(C=GUD^?O+vcMYUuQlmQ4=nI)crt|`x4wPScRntAhg%EH2+}3Jvg$Rick0Sk2AxLfxZ^g;r_VG)-PN9C^T-Kh#4R+Z<@JY%OB>&`t`S);t zU9pW{-btdqMxT1J4;<(})ZY;8EsbqvIi*)!o21G`euB)?K9)Tk+>tJC?aNM(cBNag zs6fn`*ymZ}-!aLSRSQgt!N!~h#d^;W!MgJ}E)}esQaa&IBaJgx}xT1)lfGTK<`s6xl!4 zUb|s)L|asf+{YTNO{J$D^IUihj?9(u>xy=RMLC}91tTbmd?+7Z zsMP>wOw2!G#Ve`jH{^0mFuB*my)FSMnYNNpC?1xrtgCluk+D-L(aQOC5 zUJ7mKm{Hmy3J8p-YCI?pyGM|xAD3Nk1UpU9xHYU15jmAD!kh?nj9W8l`zHiG-W`9a zsssK&DxUX+Bk$PL8MGT*6fbgy;>xm@pK`%#*t`zyyiSk@Qr@4f2irX6I_-x*0f(>e zH30-3_THaR6@|b>@!rzZ9F8Mf>AvHsHW@Ro4&Vd?ltmR~Lex-8a8U zU2kW};`Tk@>0C|oXQMoyb{E-Ej%Cgh)yz1Q_d3%xs}#I1B!AIj+)rZhTiZ&o7-xDP z?@MG_QM`5gB8j@Y1Q~DK`RWaLbJL3AM+juRX_2wxTZ@V5Q`fnJ2xNTn78ORN?tMu`sbGgQ~b85!mAS{Yy zeJG9La>Tf3^LX$%{f(z6o;a#Upx^+LY&?|p@zF}e1;qqd8T*)IT6)CHaPV?z@$6Yx z@Ms8FD=`SH&%f>VtsEvPm-zC-+7+(k;`W`-pKrxH6nAn_Tq^8_b$5G*uVa#zE#K*E zO2P0shry=0nn~*1d?zLb#uS~l7%M+Q?rvg-lU>Q#A)kkuU;*Y_(DDt=;PM3@k4a-C zsNU#o_=Dvb4)^dCSL2Q|YYug>z}qfM%1s7O^cXB&yA0j^n<8)0&4!zS_iGJ_&kIrww{k>bj6}uElPq1%(IcYodyYi9UD?*J2aG&th11}mM zd0$E)^aqi#?UmrET*KA!(+qUd9ZxXH)Tt(SM}a3kp7Zt<42qGdiIzNIi+1606wj`5aiUmt z%;)DXz(?0ggi_pBFn$zFaB0hjFZY>b=Tp`DgE8Thhg3(fJz$dCyu0^NJf`cxbE1Js z9`r18ZN&sPa@=j%`g$fQW3I6z7W|3eY`K1wNv;YSWk9VoOlljNwe>QSG`7gv3Uib^ zz(p!(RWixz>ut6%z%x~!*?E_v;O|%V+(tpB&m0|k_aZ!%*vBKUgY_B?>o$Q+{I*!wzO zlpv#49h*A=ToTogPGD{6=;03IVPH!iA7-l{Lf{piZf&^nM<>R&t%3>Y6+O$X2ppt7 z!{Im&LevwR?xldAKS(Ja1{cQU+=9$#@O3YH`NM+0kVd)w0o>r0NvT&hZ zDEs8B0{a!uwjT%Xu3=oKSc}Wyo(wo*|Jr*L_X$HiU?r4%vwAOl7mvz!qHB>L7Z;Jd zA3UQ%=iC}_{GxRCNIev6(i-V}aPI@&WnS9wrRLl^R|2-Vxx8$$20=HbA6ePkj^-Q+<(C!E)%?Iv3vB=u>@(`p=4eF zR+D`g9SuHXuwg01eFfD-K|6!baGiv$)IV&^d3TtjcmVrz9-Mz4ym_SCp;&ab{$WmO z-~AdUI^)aNxBTQW?0xJsQ%?u;dO5B;gZ$&8J2HaMnYYK;)<}XiWoI0E3NC$QYX1nf z$A~RP3tZ6oE&W&LJh)AeZcFEjG=Tj%me<~|C&-15kH21x!Q*bGp2LV61f0XE3pg>@ zB)%%Mk%w(_(`oLFX<&IX%^4I=5hB!`z;@$pEzZ=S1&p1NaS6<-kf~~fZB$=-JeU`Q zSyW9*Rq{209Delp!Fk|q8w4vgFTqyH={O*c!EmG4=aLrKW-_M6>Hgp-*@Gs`^C+K7 zPGJd{t!ZIdekp9fQG2UJFt~2pDCVqj5>D>t-0C^tPf6xmB93DbOs=|pH+Z=Bu3Jx! z5M<_hHNp4bP@_G9&IeI3E7b($F<3iJm10iVN02Y~stC>nKMh>pkhGg1i#kSC7gB?{ zmz+Z7{%Z`9c7R1D zO|04jC%SgrSecXH!sZdd;&8&R5AsfY0$!cFUt>6&QrXe>A4;NQ?0;)ooM29nPpe*x z(FLnU95fW4j37bky)p-IaH~&g9-P!UHC2Dx~v$o0)?W$s|zT;>rTBXqPk zsqw+!#}adgw<5^k6$ukM7DCV<^2yyrHW&N<4ND~`d2W!%5)#O3k zj4iX+l)uNejK@sfd@K_at9G})9ol4n9~16AQ%#LFKbr;;=#7^vavR4W#M7TMXwj7> z<^Vou`rN(sl=0@5#pGh9yv!y`+y|t#> zSl?&ztq-{4XIpqT z&p_|(SD;#X^P)Rob<)Sj5H2;NqIbG6dgrPkFS0Uld|YqW^i$x`ch_5p;d-;Yx89nB z-nm(NOwJ>iI%BlU#gBonSE`q>P~Ow3a>GT@8>LT}oshu&wsCaMXa}zgTHj`cy#?0g zQsZZ%w`t2(1r=ciKe@|n#3iusl%Pl(Ol+IWCyr15}{fN4whRwnc|C{#`enBm?ZzbWfOLS zNr{tkk+n>c1nj}r4!o~22Cu#pF<~b7tY7i0* z(mlrDB)Ds=mV@#=Cb^kuWu;MynYNLjPR&oI|tw2?}z@XLcd@c49m0 zejAg#&sO{C4R~^@W@N-GCaJ9-M2M=(VGZkrz=EUhC{o-f z1U2F|sZqPXXSrO3%6T z;K@n+hWYB)){s82nFVHX`)6k$tj1N|9yc2NiT#}wwZCV-YWTdT5UO?|t2o?{AdhHU z4k`vmXUsUPj1f+tlVwdAgI-Lh`zqiZzC#ie_Z5^tLAO8HpEn0X1=HUEI zh^G!Mb>lDtZ+UY0b)h5Tw~e7WUr^(V%gfK5n}hggSFULR_^3Syrj7!k<&xkbCs)PBU~1XJyPo?anl4AUcfL#{;;k1$E}aACTa93E*=1+;bZ-6>{ZkzPN!txP8>ybMk(0&C6KH`QU)_tvtslb{!+nc?xFov5(1j!E@!~l!t;}Z{mN}1Ku7%^hzVY zX+eX9bODC(8Iu;o;{1g+`z}oc2NtdNOMo>Ou=OafC-~hIHBmjVz>Q7MqQHr-eU2o7 zuhe8e+W~eT*Th!`7OagCu7E!+xT|>zEU|O)-e$1h0;yqKXhOy_ z+Z3i3!UL5!yJfWATo$%@_J>7N<5Z|;Gc z(H%Np;s7Fj^NUsVVJ(*IXcdqLE6b#03S<)G!DZ7{P6SJP5bO7#i3=?@7l;8TwR~K9 zW&_G&AhWU*tgW2yGHVUG*v;Ek7r|S%_t=M|!Fs&gEl>|Gv{2MrjV{)@D5-$|AjXo3 z=cTij5oE)lrOH#ls&?ygKExBGzMJ`~)nK0cgI4gNi)O2E*=z#K%8rd21#5EEyP@r| zU`P49#~~5$7MeI!sN-{p442YH*ivTlUU>{yE?kSlY$3+F`9&(VxPEPg%XCw4t=?^$ zGhqI4O|c5#A*QwM+rbAFPvkU)qrSLB+e5&Ml8iQmgP(e_sZ0aE+q__lZ5X=PdEr7u zu<`v<#kwI_G8w{N{TX@&=Dq*27S^la&im~*z=q?F-V_VKpznS-?I<|@?3?qUux1y$ z8Wy-2Om^g&JoAQSx9n)35BNZ~aU}_6V>rE1qmD00lp6s~TXxn)0KB++xGVmM3wggF zPvtu5|Mc#8IrHZcr06SW)iYofpMo8yF+`Hvr=?#2qrOoV}(PaamY$79ls9;6lwPFfkPk_WCze3w2ItUR;9ITXy##&vKwSTr8vpr&y5Nk^_B?eh)L$~Euo`&vlgjS1 znA^tnIM`Ch-(WbngsEW**~qN4Tj-CiGgf5I09&VTtKJ4a9NIdO2l?BkZVz+;3(fAT zAEkjI`j^CXxc?RdZ_~m4wJNTpQ zZ2Q&VPVwq$NAM>B!<)h2jtgh1&A=y?EK>FZ?|2oUss&ESZk-whR@`u{S`mD!FuLXn z_@j@5?P#!vpY)ChTwj@GuiCK}X5rAt#8H}PpXny)cfmx_+)XpVvw9rTFN2kMQs;St zjW220o&!4;u$6g$H+-CMpctI_&|)1T$6#l$ zu>?zbF8JBRC=)|4V^N*bOfb=PZLB7E{i*%__TW3>(KS=RpR5P3)CI4u)wsot^J|Wr zaFrXpSn%wvg-QhZrBbT;R=z7~EoXJhiz-iCgmNLcXYY>M9I)AKf8_*lPUszb53ppa z+e>@!{$;*=mvBDfaKVeuc_^R7v%`GySTf8|R5}Viq&l)J3`U-0hN8ey@TK_z>-R}x z$#wLTyMvw(8ow6g~N|)DuA`I*$?|n699r%$=<}{-(Ofu?F>!4-e@<)4v_`#(C zQ_JGPJ(p%3pWVYG%WqdaTmydhaN>eq82KWRtFpYohaYN|y?PBdXxm;BC9qiD)?_=g?#H~H@(lx?se_b@m`qQm7W+@#>pwXs{kOH&?l$-)1ssyvvG1P-)s zDI5lWcj8!EPdD&KpRUL^Cz)iKf@|hXaBtb(jGiJUd9g8JnH5-kcGb4`;1l!rq*L5y z>r(z(^nONu?{y|=zF}+Lcx?LMUG(0o2dvw(?jFV4N*vFRu4j_!EZ2FaV4ite%fwrl zWV)=gn-_RTnfBh9FW}18b9Al+4}W{H&K0~v`t4E=2)UbIN&;|Cs{;5l#h)*eQS^$Lm~+YNRN zT>DxCExhj5A`W(J8fi_Ll|Bw5fd1mEoD`RG@|C{E2t8LPX15;Lx&F4^0|OYr?3yd= zz`cAr8=Jw_@6uc;?kgy@$J1CLddY!wSjRI-WonV^uB<@QL9qX4-=1D9q8)wFyMqCK zU39>=3%q6+=V|X%1gZW`c?z}L^i6bXpDH6@9FA(q#c~MABgcA@G>fwtROX1a~%9t}EPx>#y$lWrCTx zRxT0iaU&k%{H(#o{1Q1!)}SD6kDg0|ZH<>7KfM|ey+f7?!eCZflh=H3T9CFPKiFGf zm9!t2-8jpY;=Y28Lk;vV+r?ZPJJ)u@k7c~(*&GDL>-MZDDZ{d$;^O9MGr`lRN}QgG zWkmNe_ulw|`B$q*m}39%mq{gS-N3KQh_P;1#$V=U8fpN}&yL-Zi)GKV`WBiN;KXG! zH!BE9kr9m*Aqrr-(1VN~>~D@)@TlhVdh`K*ukaJxQslfr5gIk%K_+cADTA<0U`}Y~ zKJfcZ7v|RV!gxR5erOpu#J{I__$Sn0o?gu&Fjv`X!?_<28m!9~o&~=6+t;D74r*D~OVY|4%4_h4LJpL#$D9ML$*q2?Ar z8uA9JP6b~Oebk;(hfwFk^TX`H4eOS^UQ~;&Vmczt0laQjsk22jJbWd$f=OWRH9c}o zm$1x#dj4Q>@PwTjHA5<}Km1g1z;jgmRr%v>-SE!>^Ny@+0Gl3I!0Ue=gYD@?rHkN7 z>v5cm;m=!ViEgHuAY7x~ciWAQ4@-u+5a<+Wb{wnaM_)+OPhhWSNPUOD< zZZxb)J5c~uIklHcpP8ZLelZ`D?`_HFh2Xs_-=wSMAk^xkp6UdCk)=>MJPQqTwAmCj z@SHEMoQrqDpA^+l;0JTusA@~egdX=={9d9V@hPbY*YCiZ03jq$2X^>$=0Y77$HCQ7>R+mM?Qq23MI(2xq*;P{Dd$^K2!A z*wk>`s~c9dkeZ5-f*b)+Q=0M4Yf^n5a5&|>nZ{}M}ilxHEFQHgEN91 zb)l81*hsTFxNr&flhn+(_hvJA_u=;Gkpft+e_Ifm0M3sY-hB+~`^766H{8M8ry~vv zVPig9^@C6s@Qou0v3l6AA}8~#?o$eaZ)Rm0rr3y|&?>P$5$xGBea9y3hv5^{JZcV( zNVD3p4D0`v6(UE4z)VfS9&fDoPp!JW{(ds{3uK(v@L)=jq35)^j)IlNU3)mOzP~Q^ z_R;O&-m|9NvSXykV{dkLC4gVtN|acD_5EzGhAtQIv5}ghwb-xo?0m_3KJc8>Y;y^$ z?=vdSh3{XE<+}OSvD2`=-&0$nJr#@%cronq*vMbLTlDiXY^3^V$9DxxEa7x6057zP zAF&MU{}KtaZd!w-x_8?}GM zpl^=^wJ_f2%}{T#_rl2aA^a*S%BS&Lk7p>=5)r@M!L}mLxrJ>)u;EzTkZg z>3rAmAQ0p1tR_vcllZDc;Z;o1pR2f$dR`;pea&0DW!OAWTUJsFUJ-P0K}HOo*WtZY zava=W@Jd~SpO}BD!6z#HdqGC_dd06!Vv_5_j+u6CfxW+s&wmwo=Zn%s6tnCCf+u1F zWw`U(5hA#Qz?#m8%1zjXvC5=mBRE$lwbvQDG)jbQ8>_+TMr6|AJxucU(2`F#z#bNJ zCrsLh;Fb#e%O_xkA$R>IgJrMXJ3w(?L8uLrGm_^;Ek{E5-29y?+u+EgZm?PgJ~sYD z6UD~Y7WOVG#J)qeC8MW-3oq2js$(bX;^2x;*mIs}7F<-6>|y*zK|QRClxL-S zNr0a^D&#j{>H6T*vL|ZbDxu>K4%s43&UUcE4Se>1Moq48Q1Y1aOHQGTv7tZaIfLC^}`bd*CQ-N z!4dqUl0F0=Zfj@kN97-7IWyBalpwdXrsv0lZCZ#5{l!=c7R~lO0WK@N;q@>Ip_->* z^5S?@s`b#p^%rCDC|A}|-T-iLk80A91U%|=JC3&u{KBzAbM|tqTYM~%*ULhBP-RJa z3Le$JK6}bh@O`!o$GKKu-78;tibgg((p6I`7zpMubd{c;20xuh)FdE%eyF;F3z~8P z*E>67q(3XSoRSZ=@fGWMi(sCMypi8~a8TIksJ#d-zC6s@D~7HVpy(GYm4T&pE4#~v zV8Y2TbqCmT#U9I#;AdiWNfa-QvsB1MTsgTmAteVfwVZo8Q!Eh&zvOn)dmqj>`(>x~ zG4KXr&pPV-;?=YEQe5QB?21NtETXRNrQ-DL<$EHlaev3T6IY~wPenR&N+e@5Na!oR zN^tqTt1&mgpYz)kUV&MMmPb{lVx1|^c*+xS5G!Sv7p~t?rE@tBd^KmI(Uo<${>GJ- zlfa!%M)$aGM2v9Kic3Sl8FOZI^dhGC!8ImD4xBamvjMdZ`?2m2?`*K1!K=GXU}?YQ zDQCdX<2%Pp2iH8Zf3BF1CvFnE_+-JB8S;Fqz~&~?tQUee8%}tB7kqxKLgfX-FxirO zo*V7OmTB+H`5XuZU9FYhyAix7AX)fMHp-(Xp}>oEE3A_kJj_LudQOVv2=FJ7fa!I6 z2=pv5DmKi{5Mgj^#t-T%g_E0`k>S9m{(Z~4=A|(bB9(YigZ{jHWjxn&ZLj?V?ae4< zu=h1ytg7~n+0=W6`uZz1W@jz1Nk~G(s6S_9a@+DmaM9VWw)!c3?sAz-ONzaNxR^sdL{Vv^BVhcMbS%T)E%AcNmEHHdvkm=O^w9 z7Wqh!D~_$_9RcB=sqdgXivl^EMHjFt5jAKPn<7 zMmV()?D^rWFDDi_%gSZ>I>E!PH^|KgUwChy@7;q1pt9b|QgAJMyW}9mU^O=`OW`R% z!`X5!wemBnCdF>bUU0{*=GgOJ2y$%cMFCYTK-azLOf?1jH1QR#16$i)Nu%3GcZNG9Hp}|KFy{6=WD<#ds zyKsGvth;-y!3q1E^jcBA60QBd@zCeho+lOe!MP@hzSIVkqfKTVY`CA=w{G&e(6c02 z>I5G+tb3!SF73MccVfMFBkas%dRh3FEKD3=el$l?WaOrb&?m_OSFL~Zyxy7$cML_A7eq$)qrOz zIK%1w@edEMV5?hJ!2#B5I3?)Rh?uRjhg>EGBBKqXg`eNX0_9}0JbiH4C)@C{8w8#q zmXoW(z{b=)tjh*o{H2)Z5cq*h>+@~b39@&SVT~?rn+AOsM^AlN9Snu}t*)d!?~>0>n*WTPrxVIdDNao;_W= zHEcT<>Q#oT^~$!*OtR(z+i5d!Le>47J=lo*!RquJfAEejDakBs?7h}-+-NO$(@V|Z zO$V9eoi=5 zclxyG>)`F?D+p6O>~w}zRdO7>%4ET<8+a)G#I>?dtHGbjwAilOE2 z;y}f$y<*GpOw8b8SzjK5uBLRtP_W_iA=stHB%2H$#<79Br1MiDrs!Rz-~jIa^J)2jL+xtqW*rUVmEl zvI^+J78j$^$4V2VQrGi57Wjrh@;YZZytZmzW9lhzD?ec~EcXWR3;iXXiB9Mi#yF*}fw+?53!d=e z8T42c_-eWYZ0lvM%iGq1zwAj5>xv-AF5CTzl%ArGdrCE;U_zK*^V<)-Z!DEB6^zCd za^%Qm4{+_JSFdB2!X%mA!e;>X=n#)giNTcYBI`E{tRj0-{td<3TP|-vR~vTF$bT`8 zf1RTH{3yEma@iCKg-Gn7a~+;K7HmY$b3U*L^q z8uGZJBkCTT{E!!XkxaWc1FZgXIv*dnuOQSOk71E-OPjCs)Ai`b~Cxc!H^L-F$1o6{Tpx5Byk5lKoL!tC72dR#m4OYCEZ!a##V$ChB%8C~Y~iWf z4}qE9giRbm8LSWA^(Aa(ac1trzx4JYP^-YzMw+ada8Q&z@{dtK0{-I7iz5 z3|Kg>yEh2TD%up$ zJcFU>q|>Tc@EP;QhC3Ir*=6zcfI{$_a?A7w*RcIGuwIZ_(SA7Dd}(YQx=Px^S)E{; zJFLyyU~SZ{J8eU4?J$jfOGd%kYEI$`9t9>H19V*;qU(pVt(E}`_*FEz!Cj;ehX*#^pCe9@S4}Q`okNk^vd{?61rG{ULfPR za0^&`LR$7{JdqwX-CHLQTotLlPy$c1za6S6dIo&bPE)@OE8qfPH@)D?6#D%%0A&G*9b<+Y?`36AFI0ujytA{5dn(7b4Uf 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 af7|f;$B&t5R{p-h^KTn|(*LUA_rCz>Iv}b5 literal 0 HcmV?d00001 From a3fe1de3290a32cfa3d220ebde47fd3e94b61749 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 19 Sep 2025 13:19:32 +0800 Subject: [PATCH 076/116] add validation in sorter --- m2l/processing/algorithms/sorter.py | 57 ++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index ee37b32..dd30da3 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -79,6 +79,8 @@ def groupId(self) -> str: def updateParameters(self, parameters): selected_method = parameters.get(self.METHOD, 0) + selected_algorithm = parameters.get(self.SORTING_ALGORITHM, 0) + if selected_method == 0: # User-Defined selected self.parameterDefinition(self.INPUT_STRATI_COLUMN).setMetadata({'widget_wrapper': {'visible': True}}) self.parameterDefinition(self.SORTING_ALGORITHM).setMetadata({'widget_wrapper': {'visible': False}}) @@ -88,6 +90,14 @@ def updateParameters(self, parameters): self.parameterDefinition(self.SORTING_ALGORITHM).setMetadata({'widget_wrapper': {'visible': True}}) self.parameterDefinition(self.INPUT_STRATI_COLUMN).setMetadata({'widget_wrapper': {'visible': False}}) + # observation projects + is_observation_projections = selected_algorithm == 5 + self.parameterDefinition(self.INPUT_STRUCTURE).setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + self.parameterDefinition(self.INPUT_DTM).setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + self.parameterDefinition('DIP_FIELD').setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + self.parameterDefinition('DIPDIR_FIELD').setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + self.parameterDefinition('ORIENTATION_TYPE').setMetadata({'widget_wrapper': {'visible': is_observation_projections}}) + return super().updateParameters(parameters) # ---------------------------------------------------------- @@ -241,11 +251,11 @@ def processAlgorithm( # 1 ► fetch user selections in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) - structure: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) - dtm: QgsRasterLayer = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) contacts_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) + method = self.parameterAsEnum(parameters, self.METHOD, context) algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) sorter_cls = list(SORTER_LIST.values())[algo_index] + is_observation_projections = (method == 1) and (sorter_cls == SorterObservationProjections) feedback.pushInfo(f"Using sorter: {sorter_cls.__name__}") @@ -263,33 +273,44 @@ def processAlgorithm( # # NB: map2loop does *not* need geometries – only attribute values. # -------------------------------------------------- + + structure = None + dtm = None + if is_observation_projections: + structure = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) + dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + if not structure or not structure.isValid() or not dtm or not dtm.isValid(): + raise QgsProcessingException("Structure and DTM layer are required for observation projections") + else: + structure = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) + dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) # 3 ► run the sorter sorter = sorter_cls() # instantiation is always zero-argument geology_gdf = qgsLayerToGeoDataFrame(in_layer) - structure_gdf = qgsLayerToGeoDataFrame(structure) + structure_gdf = qgsLayerToGeoDataFrame(structure) if structure else None dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'}) - dip_field = parameters.get('DIP_FIELD', 'DIP') if parameters else 'DIP' - if dip_field != 'DIP' and dip_field in structure_gdf.columns: - structure_gdf = structure_gdf.rename(columns={dip_field: 'DIP'}) - - orientation_type = self.parameterAsEnum(parameters, 'ORIENTATION_TYPE', context) - orientation_type_name = ['Dip Direction', 'Strike'][orientation_type] - dipdir_field = parameters.get('DIPDIR_FIELD', 'DIPDIR') if parameters else 'DIPDIR' - if dipdir_field in structure_gdf.columns: - if orientation_type_name == 'Strike': - structure_gdf['DIPDIR'] = structure_gdf[dipdir_field].apply( - lambda val: (val + 90.0) % 360.0 if pd.notnull(val) else val - ) - else: - structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) - + if structure_gdf: + dip_field = parameters.get('DIP_FIELD', 'DIP') if parameters else 'DIP' + if dip_field != 'DIP' and dip_field in structure_gdf.columns: + structure_gdf = structure_gdf.rename(columns={dip_field: 'DIP'}) + orientation_type = self.parameterAsEnum(parameters, 'ORIENTATION_TYPE', context) + orientation_type_name = ['Dip Direction', 'Strike'][orientation_type] + dipdir_field = parameters.get('DIPDIR_FIELD', 'DIPDIR') if parameters else 'DIPDIR' + if dipdir_field in structure_gdf.columns: + if orientation_type_name == 'Strike': + structure_gdf['DIPDIR'] = structure_gdf[dipdir_field].apply( + lambda val: (val + 90.0) % 360.0 if pd.notnull(val) else val + ) + else: + structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) order = sorter.sort( units_df, From 170c1b83b559e76674ac242e8f9cdb9b4610cca4 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 19 Sep 2025 15:15:08 +0800 Subject: [PATCH 077/116] handle user defined strati column and add validation --- m2l/processing/algorithms/sorter.py | 177 ++++++++++++++++------------ 1 file changed, 104 insertions(+), 73 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index dd30da3..174d8ee 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -21,8 +21,10 @@ QgsProcessingParameterFeatureSource, QgsProcessingParameterField, QgsProcessingParameterRasterLayer, + QgsProcessingParameterMatrix, QgsVectorLayer, - QgsWkbTypes + QgsWkbTypes, + QgsSettings ) # ──────────────────────────────────────────────── @@ -113,11 +115,25 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue=0 ) ) + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/sorter_strati_column", "") + + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["layerId", "name", "minAge", "maxAge", "group"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_GEOLOGY, "Geology polygons", [QgsProcessing.TypeVectorPolygon], + optional=True ) ) @@ -199,7 +215,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterEnum( 'ORIENTATION_TYPE', 'Orientation Type', - options=['Dip Direction', 'Strike'], + options=['','Dip Direction', 'Strike'], defaultValue=0 ) ) @@ -217,7 +233,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "CONTACTS_LAYER", "Contacts Layer", [QgsProcessing.TypeVectorLine], - optional=True, + optional=False, ) ) @@ -249,69 +265,64 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - # 1 ► fetch user selections - in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) - contacts_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) method = self.parameterAsEnum(parameters, self.METHOD, context) - algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) - sorter_cls = list(SORTER_LIST.values())[algo_index] - is_observation_projections = (method == 1) and (sorter_cls == SorterObservationProjections) - - feedback.pushInfo(f"Using sorter: {sorter_cls.__name__}") - - # 2 ► convert QGIS layers / tables to pandas - # -------------------------------------------------- - # You must supply these three DataFrames: - # units_df — required (layerId, name, minAge, maxAge, group) - # relationships_df — required (Index1 / Unitname1, Index2 / Unitname2 …) - # contacts_df — required for all but Age‐based - # - # Typical workflow: - # • iterate over in_layer.getFeatures() - # • build dicts/lists - # • pd.DataFrame(…) - # - # NB: map2loop does *not* need geometries – only attribute values. - # -------------------------------------------------- - - structure = None - dtm = None - if is_observation_projections: - structure = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) - dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) - if not structure or not structure.isValid() or not dtm or not dtm.isValid(): - raise QgsProcessingException("Structure and DTM layer are required for observation projections") + algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) + sorter_cls = list(SORTER_LIST.values())[algo_index] + contacts_layer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) + in_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + + if method == 0: # User-Defined + strati_column_matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', strati_column_matrix) + if not strati_column_matrix or len(strati_column_matrix) == 0 or not strati_column_matrix[0]: + raise QgsProcessingException("No stratigraphic column provided") + + + units_df, relationships_df, contacts_df= build_input_frames(None,contacts_layer, feedback,parameters, strati_column_matrix) + else: + units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) + + if sorter_cls == SorterObservationProjections: + geology_gdf = qgsLayerToGeoDataFrame(in_layer) structure = self.parameterAsVectorLayer(parameters, self.INPUT_STRUCTURE, context) dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + if geology_gdf is None or geology_gdf.empty or not structure or not structure.isValid() or not dtm or not dtm.isValid(): + raise QgsProcessingException("Structure and DTM layer are required for observation projections") - units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) + structure_gdf = qgsLayerToGeoDataFrame(structure) if structure else None + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - # 3 ► run the sorter - sorter = sorter_cls() # instantiation is always zero-argument - geology_gdf = qgsLayerToGeoDataFrame(in_layer) - structure_gdf = qgsLayerToGeoDataFrame(structure) if structure else None - dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None + unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' + if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: + geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'}) - unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' - if unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns: - geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'}) - - if structure_gdf: dip_field = parameters.get('DIP_FIELD', 'DIP') if parameters else 'DIP' + if not dip_field: + raise QgsProcessingException("Dip Field is required") if dip_field != 'DIP' and dip_field in structure_gdf.columns: structure_gdf = structure_gdf.rename(columns={dip_field: 'DIP'}) orientation_type = self.parameterAsEnum(parameters, 'ORIENTATION_TYPE', context) - orientation_type_name = ['Dip Direction', 'Strike'][orientation_type] + orientation_type_name = ['','Dip Direction', 'Strike'][orientation_type] + if not orientation_type_name: + raise QgsProcessingException("Orientation Type is required") dipdir_field = parameters.get('DIPDIR_FIELD', 'DIPDIR') if parameters else 'DIPDIR' + if not dipdir_field: + raise QgsProcessingException("Dip Direction Field is required") if dipdir_field in structure_gdf.columns: if orientation_type_name == 'Strike': structure_gdf['DIPDIR'] = structure_gdf[dipdir_field].apply( lambda val: (val + 90.0) % 360.0 if pd.notnull(val) else val ) - else: + elif orientation_type_name == 'Dip Direction': structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) + else: + geology_gdf = None + structure_gdf = None + dtm_gdal = None + sorter = sorter_cls() order = sorter.sort( units_df, relationships_df, @@ -332,7 +343,7 @@ def processAlgorithm( context, sink_fields, QgsWkbTypes.NoGeometry, - in_layer.sourceCrs(), + in_layer.sourceCrs() if in_layer else None, ) for pos, name in enumerate(order, start=1): @@ -350,7 +361,7 @@ def createInstance(self) -> QgsProcessingAlgorithm: # ------------------------------------------------------------------------- # Helper stub – you must replace with *your* conversion logic # ------------------------------------------------------------------------- -def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, feedback, parameters) -> tuple: +def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, feedback, parameters, user_defined_units=None) -> tuple: """ Placeholder that turns the geology layer (and any other project layers) into the four objects required by the sorter. @@ -360,34 +371,54 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee (units_df, relationships_df, contacts_df) """ - unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' - min_age_field = parameters.get('MIN_AGE_FIELD', 'MIN_AGE') if parameters else 'MIN_AGE' - max_age_field = parameters.get('MAX_AGE_FIELD', 'MAX_AGE') if parameters else 'MAX_AGE' - group_field = parameters.get('GROUP_FIELD', 'GROUP') if parameters else 'GROUP' - - # Example: convert the geology layer to a very small units_df - units_records = [] - for f in layer.getFeatures(): - units_records.append( - dict( - layerId=f.id(), - name=f[unit_name_field], # attribute names → your schema - minAge=float(f[min_age_field]), - maxAge=float(f[max_age_field]), - group=f[group_field], + if user_defined_units: + units_record = [] + for i, row in enumerate(user_defined_units): + units_record.append( + dict( + layerId=i, + name=row[1], + minAge=row[2], + maxAge=row[3], + group=row[4] + ) ) - ) - units_df = pd.DataFrame.from_records(units_records) - - total_num_of_units = len(units_df) - units_df = units_df.drop_duplicates(subset=['name']) - unique_num_of_units = len(units_df) - - feedback.pushInfo(f"Removed duplicated units: {total_num_of_units - unique_num_of_units}") + units_df = pd.DataFrame.from_records(units_record) + else: + unit_name_field = parameters.get('UNIT_NAME_FIELD', 'UNITNAME') if parameters else 'UNITNAME' + min_age_field = parameters.get('MIN_AGE_FIELD', 'MIN_AGE') if parameters else 'MIN_AGE' + max_age_field = parameters.get('MAX_AGE_FIELD', 'MAX_AGE') if parameters else 'MAX_AGE' + group_field = parameters.get('GROUP_FIELD', 'GROUP') if parameters else 'GROUP' + + if not layer or not layer.isValid(): + raise QgsProcessingException("No geology layer provided") + if not unit_name_field: + raise QgsProcessingException("Unit Name Field is required") + if not min_age_field: + raise QgsProcessingException("Minimum Age Field is required") + if not max_age_field: + raise QgsProcessingException("Maximum Age Field is required") + if not group_field: + raise QgsProcessingException("Group Field is required") + + units_records = [] + for f in layer.getFeatures(): + units_records.append( + dict( + layerId=f.id(), + name=f[unit_name_field], # attribute names → your schema + minAge=float(f[min_age_field]), + maxAge=float(f[max_age_field]), + group=f[group_field], + ) + ) + units_df = pd.DataFrame.from_records(units_records) + feedback.pushInfo(f"Units → {len(units_df)} records") # map_data can be mocked if you only use Age-based sorter - feedback.pushInfo(f"Units → {unique_num_of_units} records") + if not contacts_layer or not contacts_layer.isValid(): + raise QgsProcessingException("No contacts layer provided") contacts_df = qgsLayerToGeoDataFrame(contacts_layer) if contacts_layer else pd.DataFrame() if not contacts_df.empty: From 569f39bb0f4c5fc5bf0f0fddbc614ab51a18e216 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Fri, 19 Sep 2025 15:36:40 +0800 Subject: [PATCH 078/116] add json file output for sorter --- m2l/processing/algorithms/sorter.py | 47 ++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 174d8ee..93a2726 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -1,6 +1,7 @@ from typing import Any, Optional from osgeo import gdal import pandas as pd +import json from PyQt5.QtCore import QMetaType from qgis import processing @@ -17,6 +18,7 @@ QgsProcessingException, QgsProcessingFeedback, QgsProcessingParameterEnum, + QgsProcessingParameterFileDestination, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, QgsProcessingParameterField, @@ -255,6 +257,14 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterFileDestination( + "JSON_OUTPUT", + "Stratigraphic column json", + fileFilter="JSON files (*.json)" + ) + ) + # ---------------------------------------------------------- # Core # ---------------------------------------------------------- @@ -270,6 +280,7 @@ def processAlgorithm( sorter_cls = list(SORTER_LIST.values())[algo_index] contacts_layer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) in_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + output_file = self.parameterAsFileOutput(parameters, 'JSON_OUTPUT', context) if method == 0: # User-Defined strati_column_matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) @@ -317,20 +328,20 @@ def processAlgorithm( ) elif orientation_type_name == 'Dip Direction': structure_gdf = structure_gdf.rename(columns={dipdir_field: 'DIPDIR'}) + order = sorter_cls().sort( + units_df, + relationships_df, + contacts_df, + geology_gdf, + structure_gdf, + dtm_gdal + ) else: - geology_gdf = None - structure_gdf = None - dtm_gdal = None - - sorter = sorter_cls() - order = sorter.sort( - units_df, - relationships_df, - contacts_df, - geology_gdf, - structure_gdf, - dtm_gdal - ) + order = sorter_cls().sort( + units_df, + relationships_df, + contacts_df + ) # 4 ► write an in-memory table with the result sink_fields = QgsFields() @@ -350,8 +361,14 @@ def processAlgorithm( f = QgsFeature(sink_fields) f.setAttributes([pos, name]) sink.addFeature(f, QgsFeatureSink.FastInsert) - - return {self.OUTPUT: dest_id} + try: + with open(output_file, 'w') as f: + json.dump(order, f) + except Exception as e: + with open(output_file, 'w') as f: + json.dump([], f) + + return {self.OUTPUT: dest_id, 'JSON_OUTPUT': output_file} # ---------------------------------------------------------- def createInstance(self) -> QgsProcessingAlgorithm: From 9435579cde827bbd0a4bc54098b975963799b870 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:43:20 +0930 Subject: [PATCH 079/116] refactor: remove user defined column --- m2l/processing/algorithms/sorter.py | 60 +++++++---------------------- 1 file changed, 14 insertions(+), 46 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 93a2726..20d3cd7 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -3,7 +3,7 @@ import pandas as pd import json -from PyQt5.QtCore import QMetaType +from PyQt5.QtCore import QVariant from qgis import processing from qgis.core import ( QgsFeatureSink, @@ -40,7 +40,7 @@ SorterUseNetworkX, SorterUseHint, # kept for backwards compatibility ) -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame +from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, qvariantToFloat # a lookup so we don’t need a giant if/else block SORTER_LIST = { @@ -109,26 +109,17 @@ def updateParameters(self, parameters): # ---------------------------------------------------------- def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + # enum so the user can pick the strategy from a dropdown self.addParameter( QgsProcessingParameterEnum( - name=self.METHOD, - description='Select Method', - options=['User-Defined', 'Automatic'], - defaultValue=0 + self.SORTING_ALGORITHM, + "Sorting strategy", + options=list(SORTER_LIST.keys()), + defaultValue="Observation projections", # Age-based is safest default ) ) strati_settings = QgsSettings() last_strati_column = strati_settings.value("m2l/sorter_strati_column", "") - - self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["layerId", "name", "minAge", "maxAge", "group"], - numberRows=0, - defaultValue=last_strati_column - ) - ) self.addParameter( QgsProcessingParameterFeatureSource( @@ -239,17 +230,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) - - # enum so the user can pick the strategy from a dropdown - self.addParameter( - QgsProcessingParameterEnum( - self.SORTING_ALGORITHM, - "Sorting strategy", - options=list(SORTER_LIST.keys()), - defaultValue=0, # Age-based is safest default - ) - ) #:contentReference[oaicite:0]{index=0} - self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, @@ -275,25 +255,13 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - method = self.parameterAsEnum(parameters, self.METHOD, context) algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) sorter_cls = list(SORTER_LIST.values())[algo_index] contacts_layer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) in_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) output_file = self.parameterAsFileOutput(parameters, 'JSON_OUTPUT', context) - - if method == 0: # User-Defined - strati_column_matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', strati_column_matrix) - if not strati_column_matrix or len(strati_column_matrix) == 0 or not strati_column_matrix[0]: - raise QgsProcessingException("No stratigraphic column provided") - - - units_df, relationships_df, contacts_df= build_input_frames(None,contacts_layer, feedback,parameters, strati_column_matrix) - - else: - units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) + + units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) if sorter_cls == SorterObservationProjections: geology_gdf = qgsLayerToGeoDataFrame(in_layer) @@ -345,8 +313,8 @@ def processAlgorithm( # 4 ► write an in-memory table with the result sink_fields = QgsFields() - sink_fields.append(QgsField("strat_pos", QMetaType.Type.Int)) - sink_fields.append(QgsField("unit_name", QMetaType.Type.QString)) + sink_fields.append(QgsField("order", QVariant.Int)) + sink_fields.append(QgsField("unit_name", QVariant.String)) (sink, dest_id) = self.parameterAsSink( parameters, @@ -423,9 +391,9 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee units_records.append( dict( layerId=f.id(), - name=f[unit_name_field], # attribute names → your schema - minAge=float(f[min_age_field]), - maxAge=float(f[max_age_field]), + name=f[unit_name_field], + minAge=qvariantToFloat(f, min_age_field), + maxAge=qvariantToFloat(f, max_age_field), group=f[group_field], ) ) From a660002ee17d02e18b28cf5e2d50f2955503b1e5 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:43:35 +0930 Subject: [PATCH 080/116] feat: add qvariantToFloat function --- m2l/main/vectorLayerWrapper.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 72ff281..5bf2da0 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -454,3 +454,35 @@ def dataframeToQgsLayer( feedback.pushInfo("Done.") feedback.setProgress(100) return sink, sink_id + +from qgis.core import NULL +from PyQt5.QtCore import QVariant + +def qvariantToFloat(f, field_name): + val = f.attribute(field_name) # usually returns a native Python type + # null / empty values + if val in (None, NULL, ''): + return None + # strings with decimal comma (depending on locale) + if isinstance(val, str): + val = val.strip() + if val == '': + return None + val = val.replace(',', '.') # replace comma with dot if present + try: + return float(val) + except ValueError: + pass + # residual QVariant + if isinstance(val, QVariant): + # toDouble() -> (value, ok) + d, ok = val.toDouble() + return float(d) if ok else None + # native int/float + if isinstance(val, (int, float)): + return float(val) + # fallback conversion attempt + try: + return float(val) + except Exception: + return None From b74bc79480048e0d2f77f45beafb74ca7d5d9a27 Mon Sep 17 00:00:00 2001 From: noellehmcheng <143368485+noellehmcheng@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:13:46 +0800 Subject: [PATCH 081/116] feat: Processing/processing tools sampler (#19) * sampler * fix sorter * fix data type of spacing and decimator in sampler * rename unused loop idx in sampler * add dtm to input * add validation in sampler * spacing decimator test * refactor sampler tests for cicd compatibility * update tester.yml workflow for all branches * change image * update testing.txt * install map2loop in tester.yml * fix: update field types to use QVariant * fix optional dtm parameter in decimator --------- Co-authored-by: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> --- .github/workflows/tester.yml | 115 +++++++++++----------- m2l/processing/algorithms/sampler.py | 128 +++++++++++++++---------- m2l/processing/algorithms/sorter.py | 4 +- requirements/testing.txt | 2 + tests/qgis/input/dtm_rp.tif | Bin 0 -> 161316 bytes tests/qgis/input/dtm_rp.tif.aux.xml | 11 +++ tests/qgis/input/faults_clip.cpg | 1 + tests/qgis/input/faults_clip.dbf | Bin 0 -> 49957 bytes tests/qgis/input/faults_clip.prj | 1 + tests/qgis/input/faults_clip.shp | Bin 0 -> 9308 bytes tests/qgis/input/faults_clip.shx | Bin 0 -> 380 bytes tests/qgis/input/folds_clip.cpg | 1 + tests/qgis/input/folds_clip.dbf | Bin 0 -> 9713 bytes tests/qgis/input/folds_clip.prj | 1 + tests/qgis/input/folds_clip.shp | Bin 0 -> 1804 bytes tests/qgis/input/folds_clip.shx | Bin 0 -> 156 bytes tests/qgis/input/geol_clip_no_gaps.cpg | 1 + tests/qgis/input/geol_clip_no_gaps.dbf | Bin 0 -> 305246 bytes tests/qgis/input/geol_clip_no_gaps.prj | 1 + tests/qgis/input/geol_clip_no_gaps.shp | Bin 0 -> 103704 bytes tests/qgis/input/geol_clip_no_gaps.shx | Bin 0 -> 588 bytes tests/qgis/input/structure_clip.cpg | 1 + tests/qgis/input/structure_clip.dbf | Bin 0 -> 45216 bytes tests/qgis/input/structure_clip.prj | 1 + tests/qgis/input/structure_clip.shp | Bin 0 -> 3404 bytes tests/qgis/input/structure_clip.shx | Bin 0 -> 1044 bytes tests/qgis/test_sampler_decimator.py | 93 ++++++++++++++++++ tests/qgis/test_sampler_spacing.py | 80 ++++++++++++++++ 28 files changed, 334 insertions(+), 107 deletions(-) create mode 100644 tests/qgis/input/dtm_rp.tif create mode 100644 tests/qgis/input/dtm_rp.tif.aux.xml create mode 100644 tests/qgis/input/faults_clip.cpg create mode 100644 tests/qgis/input/faults_clip.dbf create mode 100644 tests/qgis/input/faults_clip.prj create mode 100644 tests/qgis/input/faults_clip.shp create mode 100644 tests/qgis/input/faults_clip.shx create mode 100644 tests/qgis/input/folds_clip.cpg create mode 100644 tests/qgis/input/folds_clip.dbf create mode 100644 tests/qgis/input/folds_clip.prj create mode 100644 tests/qgis/input/folds_clip.shp create mode 100644 tests/qgis/input/folds_clip.shx create mode 100644 tests/qgis/input/geol_clip_no_gaps.cpg create mode 100644 tests/qgis/input/geol_clip_no_gaps.dbf create mode 100644 tests/qgis/input/geol_clip_no_gaps.prj create mode 100644 tests/qgis/input/geol_clip_no_gaps.shp create mode 100644 tests/qgis/input/geol_clip_no_gaps.shx create mode 100644 tests/qgis/input/structure_clip.cpg create mode 100644 tests/qgis/input/structure_clip.dbf create mode 100644 tests/qgis/input/structure_clip.prj create mode 100644 tests/qgis/input/structure_clip.shp create mode 100644 tests/qgis/input/structure_clip.shx create mode 100644 tests/qgis/test_sampler_decimator.py create mode 100644 tests/qgis/test_sampler_spacing.py diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 4543564..cf13a1b 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -2,16 +2,12 @@ name: "🎳 Tester" on: push: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml - requirements/testing.txt pull_request: - branches: - - main paths: - '**.py' - .github/workflows/tester.yml @@ -45,55 +41,62 @@ jobs: - name: Run Unit tests run: pytest -p no:qgis tests/unit/ - # test-qgis: - # runs-on: ubuntu-latest - - # container: - # image: qgis/qgis:3.4 - # env: - # CI: true - # DISPLAY: ":1" - # MUTE_LOGS: true - # NO_MODALS: 1 - # PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." - # QT_QPA_PLATFORM: "offscreen" - # WITH_PYTHON_PEP: false - # # be careful, things have changed since QGIS 3.40. So if you are using this setup - # # with a QGIS version older than 3.40, you may need to change the way you set up the container - # volumes: - # # Mount the X11 socket to allow GUI applications to run - # - /tmp/.X11-unix:/tmp/.X11-unix - # # Mount the workspace directory to the container - # - ${{ github.workspace }}:/home/root/ - - # steps: - # - name: Get source code - # uses: actions/checkout@v4 - - # - name: Print QGIS version - # run: qgis --version - - # # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 - # # - name: Setup plugin - # # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} - - # - name: Install Python requirements - # run: | - # apt update && apt install -y python3-pip python3-venv pipx - # # Create a virtual environment - # cd /home/root/ - # pipx run qgis-venv-creator --venv-name ".venv" - # # Activate the virtual environment - # . .venv/bin/activate - # # Install the requirements - # python3 -m pip install -U -r requirements/testing.txt - - # - name: Run Unit tests - # run: | - # cd /home/root/ - # # Activate the virtual environment - # . .venv/bin/activate - # # Run the tests - # # xvfb-run is used to run the tests in a virtual framebuffer - # # This is necessary because QGIS requires a display to run - # xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml + test-qgis: + runs-on: ubuntu-latest + + container: + image: qgis/qgis:latest + env: + CI: true + DISPLAY: ":1" + MUTE_LOGS: true + NO_MODALS: 1 + PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:." + QT_QPA_PLATFORM: "offscreen" + WITH_PYTHON_PEP: false + # be careful, things have changed since QGIS 3.40. So if you are using this setup + # with a QGIS version older than 3.40, you may need to change the way you set up the container + volumes: + # Mount the X11 socket to allow GUI applications to run + - /tmp/.X11-unix:/tmp/.X11-unix + # Mount the workspace directory to the container + - ${{ github.workspace }}:/home/root/ + + steps: + - name: Get source code + uses: actions/checkout@v4 + + - name: Print QGIS version + run: qgis --version + + # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40 + # - name: Setup plugin + # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }} + + - name: Install Python requirements + run: | + apt update && apt install -y python3-pip python3-venv pipx + # Create a virtual environment + cd /home/root/ + pipx run qgis-venv-creator --venv-name ".venv" + # Activate the virtual environment + . .venv/bin/activate + # Install the requirements + python3 -m pip install -U -r requirements/testing.txt + python3 -m pip install git+https://github.com/Loop3D/map2loop.git@noelle/contact_extractor + + - name: verify input data + run: | + cd /home/root/ + . .venv/bin/activate + ls -la tests/qgis/input/ || echo "Input directory not found" + + - name: Run Unit tests + run: | + cd /home/root/ + # Activate the virtual environment + . .venv/bin/activate + # Run the tests + # xvfb-run is used to run the tests in a virtual framebuffer + # This is necessary because QGIS requires a display to run + xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index a4b61f6..28e5184 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -10,7 +10,9 @@ """ # Python imports from typing import Any, Optional -from qgis.PyQt.QtCore import QMetaType +from qgis.PyQt.QtCore import QVariant +from osgeo import gdal +import pandas as pd # QGIS imports from qgis.core import ( @@ -22,13 +24,17 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, - QgsProcessingParameterString, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterEnum, QgsProcessingParameterNumber, + QgsFields, QgsField, QgsFeature, QgsGeometry, QgsPointXY, - QgsVectorLayer + QgsVectorLayer, + QgsWkbTypes, + QgsCoordinateReferenceSystem ) # Internal imports from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame @@ -68,17 +74,20 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( - QgsProcessingParameterString( + QgsProcessingParameterEnum( self.INPUT_SAMPLER_TYPE, "SAMPLER_TYPE", + ["Decimator", "Spacing"], + defaultValue=0 ) ) self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterRasterLayer( self.INPUT_DTM, "DTM", [QgsProcessing.TypeRaster], + optional=True, ) ) @@ -104,6 +113,8 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_DECIMATION, "DECIMATION", + QgsProcessingParameterNumber.Integer, + defaultValue=1, optional=True, ) ) @@ -112,6 +123,8 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterNumber( self.INPUT_SPACING, "SPACING", + QgsProcessingParameterNumber.Double, + defaultValue=200.0, optional=True, ) ) @@ -119,7 +132,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - "Sampled Contacts", + "Sampled Points", ) ) @@ -130,60 +143,77 @@ def processAlgorithm( feedback: QgsProcessingFeedback, ) -> dict[str, Any]: - dtm = self.parameterAsSource(parameters, self.INPUT_DTM, context) - geology = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - spatial_data = self.parameterAsSource(parameters, self.INPUT_SPATIAL_DATA, context) - decimation = self.parameterAsSource(parameters, self.INPUT_DECIMATION, context) - spacing = self.parameterAsSource(parameters, self.INPUT_SPACING, context) - sampler_type = self.parameterAsString(parameters, self.INPUT_SAMPLER_TYPE, context) + dtm = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + spatial_data = self.parameterAsVectorLayer(parameters, self.INPUT_SPATIAL_DATA, context) + decimation = self.parameterAsInt(parameters, self.INPUT_DECIMATION, context) + spacing = self.parameterAsDouble(parameters, self.INPUT_SPACING, context) + sampler_type_index = self.parameterAsEnum(parameters, self.INPUT_SAMPLER_TYPE, context) + sampler_type = ["Decimator", "Spacing"][sampler_type_index] + + if spatial_data is None: + raise QgsProcessingException("Spatial data is required") + + if sampler_type == "Decimator" and geology is None: + raise QgsProcessingException("Geology is required") # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) - spatial_data = qgsLayerToGeoDataFrame(spatial_data) + spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) + dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - if sampler_type == "decimator": + if sampler_type == "Decimator": feedback.pushInfo("Sampling...") - sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) + sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) + samples = sampler.sample(spatial_data_gdf) - if sampler_type == "spacing": + if sampler_type == "Spacing": feedback.pushInfo("Sampling...") - sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm, geology_data=geology, feedback=feedback) - samples = sampler.sample(spatial_data) - - - # create layer - vector_layer = QgsVectorLayer("Point", "sampled_points", "memory") - provider = vector_layer.dataProvider() - - # add fields - provider.addAttributes([QgsField("ID", QMetaType.Type.QString), - QgsField("X", QMetaType.Type.Float), - QgsField("Y", QMetaType.Type.Float), - QgsField("Z", QMetaType.Type.Float), - QgsField("featureId", QMetaType.Type.QString) - ]) - vector_layer.updateFields() # tell the vector layer to fetch changes from the provider - - # add a feature - for i in range(len(samples)): - feature = QgsFeature() - feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(samples.X[i], samples.Y[i], samples.Z[i]))) - feature.setAttributes([samples.ID[i], samples.X[i], samples.Y[i], samples.Z[i], samples.featureId[i]]) - provider.addFeatures([feature]) - - # update layer's extent when new features have been added - # because change of extent in provider is not propagated to the layer - vector_layer.updateExtents() - # --- create sink + sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm_gdal, geology_data=geology) + samples = sampler.sample(spatial_data_gdf) + + fields = QgsFields() + fields.append(QgsField("ID", QVariant.String)) + fields.append(QgsField("X", QVariant.Double)) + fields.append(QgsField("Y", QVariant.Double)) + fields.append(QgsField("Z", QVariant.Double)) + fields.append(QgsField("featureId", QVariant.String)) + + crs = None + if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: + crs = QgsCoordinateReferenceSystem.fromWkt(spatial_data_gdf.crs.to_wkt()) + sink, dest_id = self.parameterAsSink( parameters, self.OUTPUT, context, - vector_layer.fields(), - QgsGeometry.Type.Point, - spatial_data.crs, - ) + fields, + QgsWkbTypes.PointZ if 'Z' in (samples.columns if samples is not None else []) else QgsWkbTypes.Point, + crs + ) + + if samples is not None and not samples.empty: + for _index, row in samples.iterrows(): + feature = QgsFeature(fields) + + # decimator has z values + if 'Z' in samples.columns and pd.notna(row.get('Z')): + wkt = f"POINT Z ({row['X']} {row['Y']} {row['Z']})" + feature.setGeometry(QgsGeometry.fromWkt(wkt)) + else: + #spacing has no z values + feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y']))) + + feature.setAttributes([ + str(row.get('ID', '')), + float(row.get('X', 0)), + float(row.get('Y', 0)), + float(row.get('Z', 0)) if pd.notna(row.get('Z')) else 0.0, + str(row.get('featureId', '')) + ]) + + sink.addFeature(feature) + return {self.OUTPUT: dest_id} def createInstance(self) -> QgsProcessingAlgorithm: diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 215e79a..849a18a 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -92,7 +92,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, - self.tr("Stratigraphic column"), + "Stratigraphic column", ) ) @@ -177,7 +177,7 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple: (units_df, relationships_df, contacts_df, map_data) """ import pandas as pd - from m2l.map2loop.mapdata import MapData # adjust import path if needed + from map2loop.map2loop.mapdata import MapData # adjust import path if needed # Example: convert the geology layer to a very small units_df units_records = [] diff --git a/requirements/testing.txt b/requirements/testing.txt index 1940035..d238575 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -3,3 +3,5 @@ pytest-cov>=4 packaging>=23 +shapely +geopandas \ No newline at end of file diff --git a/tests/qgis/input/dtm_rp.tif b/tests/qgis/input/dtm_rp.tif new file mode 100644 index 0000000000000000000000000000000000000000..a9ba843ebb8ff37ef232f9f26cbed3d5e2c1cdd4 GIT binary patch literal 161316 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ℜ>~F(~nYh9g>_mohw!n3*@fJVdYJs(3Pb9wGv7sov0GnP(&ts4yconB+oSMyva z7S>@l_w|BlHr&Fz-|E3T+{1ryp})UyLcH-}$w(VN%x6r^$0yDk(X!Idt~VlU?W|pV zXnw5bM%6V~)1L64_QBdW-<#C{+?yf6pJoV$g6+B5cOIlkIUk28LE zzgT_ZPpmz{7OZW1^=>^9JF*^)*waH9quuY@urGUWe6Jo~^IWWJ-}_QBzdY>bmGiC4 zVeOhDvBO;Sg@LQSfcd|FbJ8}HM?>sn%x%15f%Q$ltAKdGU zz_KZ|Azhu4w7lk>x>Pq$x0hHPXDqNN#C?)`fC38tfgioe6#mi}!|Vx23pdoJr)XGcbBKJDpz89&22 zzvrFTT6M0>Wj}ZG*q1TU<&S;l8SUdI@LwG0zoVY^z=`gweq14kH~l-Keh08|!#7ZL zeQ$XKuCm6#Xs`#H8*XE7e)xplV1Kk2!@r|P^ucp|3mJEvc$kKD(sAOKe(S)yZz^qb z@mVh-#%~IJv*-5>rr#R1PPBFSCm(G-??d>212b6O-jaR;3hUYtTo6<50FL27#|^w_ z`&)71?ze^72XnK(ZD#DkLh7~&>_k@T4_CAA-NFTY9KPrmmoe~Cu@6N*9AB^rSLUcU z#`}U(?sxNBQ=h%SOJv1wJ~qACiJ3=RqeWkh&wlWt1Q)V?{oeMD^S$%xp(c)o`Y))%f^B~DDnz<6P_H7-Zop2%hod(fF)XA;R+vOdk=?cs_GvF9 z_6~361GlkfUq_!k(KQygBFkDNJg&K&(b;<6;@3HcGqr!y&Z57yadq$eK7w<9Gxbg6 z&S$&xy$eLU-wEGM)#vxO@A|w61BT%<_%5HZFdj@6=B}e7?&=vm*ax#$X*VD6W2cQ9 za6WJ*BKF+nJGfnNj05__M^4;^b9hh8Ce{kB34BjJZ%NO&63ch6@4E^|CJx|H+spTn zJ3j5gIUK_^etZi~@K=9gybn|CTcmh_8~BuX??myyH`KAuTao&mnhv(wlg4<}w(S8M z5gPV(3;X!t&3r4eTc7s0X!Gem+B&P}^&7#ziOh%M3pVS{4z5@53a8$NS~Kge-h;9G z%+Y(~n_^(wJlUH)91YiO;>`F{?~XCewQY`w{n;y7#5i-c@R_6HTHp1#<9++|?`wP* z2jjkf&hK9K;fwfVh-x`sbG2VckIo$cA_5}Z#W9g4QgL~K=upgXG_MgFt-F&E8 z1@Fd^9l?p&+`)ydKW%ff7oWB`e287|P4mH9am06w+K#U8Y-|)hV4&r)&TIlJg~#ZF zn-f2bi+@Beea7*@j~Hz>>`dBR!K3$JJ{tSM=*Z4HQFnIV--X@84YB5nn0KG~&)m^9 zXT}Zhg>}zb&KkSU-+trGg-7JfJI!NUYjcd)^zNLkv}-;(QtfEt>b{3--#h1ScSpn2 zRr-S~_=8RTa9*)AZoqzXLOdA}_k8(-H+U|*UE~2-)y-J zreS>H#KF71_8%WUjQGgB-~=w1Z?)9C8DE^R-gzsc@uTXSz0g&6wePxDZL_D*^`<90 z(vjVwUA}`;7-RECZ{IlXc{)BKz9jt%7Twoy{9xqa*uI~9)9`29zQAnFneoFK@mHVN zjGc)!uQdia=V`4yTh@iWV{6m1MSDa__k25T!bjIS*#}MdmsnW3PE7Ez;3neRWW#Lm zQ<#ApID^-||3`Q**!}yV{Atg`z%YT;owha3oOKUpUigDeSX|<>{x&9S6{g`IW@&3j zhIR{k;YasXOxoi4vkId|Rfcmvn)2=j28@jLop8BXiFMt|@rb_L@V z6U2X2dNZtr4>UeR&$bt?Hb<)FUHj3PpsVKE z@Z9Zr+HeXZksaUG#sydnehb5m8)L=sRljc?-$@Up9ZvXeTKGT`V?1q6_KXZ#|KUV9 zRQ=Y7%*c3)t-afK`c}7(-+9Ps)1!U#k%|%5eRSC{1q*O`9Uly)y@I2G8w7siA21h; z`@MhUkI)B?cNi6i1IaN=kK}N{Y3f{9Odm{7 zbGXYRQ+#~50RMO(W*gt}`bPIH<6Fe6#E>nv`C(q`k~{nG0!PA&!;gU{_StnW9B03< zSC|YgSGP+yUSamgYP-f5*YVn0UvORDI=+>H@A|z9-|b(~VlHJT)_YR#i*bn`iK#vG ze&tuLVVWLoSKec<_BmhmM{m9hy50Myy}>2u7y6F?fNUy3f=iHBdKq zCZn7LLOMn1;UYMsNYMaLs)xKL_i8v4_&@K&@GfW*_>Sz@6Q-{tN1G?@1-k@}J6=rrza1y^8J9UKUfUV$ zKib^2-)J}-xDNZiU+BbnD~R}=_8mX;nFs#84e%)k5&GDLP1si-*i4_;lKAn$y&{*! zOuGAv3*ytid)yQg8QLw2Rs-`yYMtf+9!%>t7jS|UC$gv2xEZOt+QTu-O%nXUA)M}L zGGoJQVYd6lk-{px_V^K>F%{eR#)oab$TW|4;3i`ph|YHv{EHc}@qr%E_4g|MncE)W z)!u9WeFpYEK4%yYk z+C%1mNih|t-BRDCG?Bjsn(;FAkFJ7EE zhIy@J&wYl~XKv~UBH1dgNOc?RT<^_dZSp zC-#BE&avWS!!zu&3)lGVeB$({57xyk>xs|#rn6hyS@|Ny)cs)1&XaWt?~TpEWOBY> zv-Ohy;L{#qxg_=`S!a>hb%yBCGNTFq<};^yL@)D1?{2f#g;-DB4;E;$BU7z|x7FCL zdmIno_<|3x%(vqk-ACgAPMBlkfO|gvBa0pX7K=;TI05^Q#(pAW=? z2cz@))-EjS3nuG5$UE_8abkNzGJZwi8}8trJzGIeT>XOif_Gldmb&@*<$mR zPJZ?M317GOg&yjlEuP(C9rnKP4?a1HsDqrUgSt=nfz`-{TLNQ|!vA&rk#EL{>0QX1 zFuWJui3q;b{U1KyM2+kDtTSUzI1DE?fo1%3FWXOJcH39@Yk%%pcRFm(#BX~e%_;cS zHzM#oaz=88IrFx7N4E2tKQTDOXWTTub&{V;?82lv!GrqCw;ZZd>GYp^!79JDF+;9< zz6Jm6$d>=eVn@n<@E#5OBjf2wM`{f*`n!GBvZvWKuiC438v~$rr zdY>G&7RN`gw&~ULu+5`g^I7Yx6U|?J?CzWF3*e{jlIJjtG6y2b?8;+<#1 zHEzY$CiNCNC%bVE`-{&w+{ql32m7!-4D!Ql^p-DsNWN$GBZqYDtueCW+p*QL;rX|MFxK6+0pzG3aNADHfUVZv<$ZZ{3D z-Da=0XY1+HMu(jnwAFVy3%f}xPufByBxHz1E(|#j%Z1*tidLLG>?7m;& z0Ko*t8OCRZd*52UO9UW{I z#)H!h$M71NB<+&KEU}UDKa_Ux?#}luG}vll&Xye?S&yWTG#@4|M9j643$nAXvp4&# zJ?9M0;hc1x=|Yd#=d5DbM<0KUIpaHKX?HA}SWkX-M4L?(fsMvL4fn+XIBPDz>WCk% z7Y*CNdI>w&UlCjpQ<65nn3WhZ(pTS3Y~M-dwI)7fz3GmogX6-D{K07M=cN|vC1=5= znBo2w3+wh9P8Vmoe;o{V%<6j{?&SB(ZLaCP@n*Oe4o`5&oHQR!bl%`$ygRY)kA82) zz9)T8Mjngrk+kjAo*#^RR z7YCyCZ|f#z+Oxf9kBemABf8EFm(DLAYI9*%j`$*C2%ng;qt6(6w11kvWyfc|il-xd zyZbAy_cVjY8En#Z?>0^gzj+(H4>;jlAJ*%RXTx=b9~X)fX`5$>i*Mh%jT2iU*4T)# zHLvxu#=wW*&mGO)!Kr+~?aJQ$tzxP}@a%qdU$0K+pG_Z3 zm!0+{H}C<*ix1|4dzg>E_;O!#+Gos8zZh!`9E!m946eNu@Ok#Q`%d4%iJDYi@S^cu z-#{6MJ9tp@;1e4!a{kr1r6=LT3H(Sbyc<_6WrP9+wRGJ8@vzoMg(W@ z3k!uK7{LSemuG9X)cW?{XB+lsU7|L|;gY%FKit^(ku~R!#f~iN@+B`bd0BYVIbt7t zFZfQsJ;Cb4jreBpj~DByNO&Q)-ng0v7qsyK_M`Db%;1Oj`0cOm|6`x|qnGpJKr~sg z>s*b$-58XcIy0T~%fpT=cJiWTggxo_>8;&yVztf1CTHGte9?SIcI-)TBATp7aU>X? z!DsORKCf>DhF1ipt3UQk-+o7=<3`4L4_1pfF+*JJMS70I34MoGD{F7!EeaQKV&G-O zeqnZH&$aK=t9T)|5jl1S_j=*E^8)Mceq(WhEpD4HV|HYyWooO=17>np*Eo2$UVLh9 z-C=CzG2eMt)C4XHH{xKze6RpF!GQXYjIVmBQ|6g!6`_eb?&#{XmNC7@X`k?9udS^K zqxQB)_tlxiw9E_pPx$OFXM^$a$gRxebv>FnlM7OO5d-_UV9n^VqbECS+t;#A@2C7} z?~6Uymvihsmwc|s$!q0DURvZ@e&mFfXE~zFf7<$5#ELPNEthQi$e&v0?4fn-$KEoA z?T&_9SYDCBY_L4|f_3^}UVmYoj==m(|4v-Rq|Hz2cLDyuynbhfqL^{bkxNkmOxdFKvymdcN|Hv)!fB5#?aFr1ne9$**l%%$Hr zxlR3G19pt5HQecVA;#UF_?dBVuV3w9#(exRuus0t9U0omdF`cr+Eq96Lj$O65~wP8_5Tb zuxDiT+I3cYIBo0q-k0^XTUTC_19{l;&}Wx}5;-E;(PX6_C2i|hqtj3GRok5UwR`T( zcVVBkk6!w+AF^BQbKpcU3+sjFgWKrB`Bh@l5Bq~$-^S56LBqMe!g#RlJFLEg@;x$q zcewY{zaz%nt%Wa`A^@BR2*)|DZhMLJNcYq*+hNp zk)%(5?3j% z`-q&a&X;`38$H?T6FGjqiq{tz#3ln z-}qNJvF}C4#*M;%`p0GL+x|a4E-cuF>EN=y^Su}0gRwKRGQPwd-ihXfvGqoT6Ll{K zt93WsBu*{eXR$}>qzzZ_OGffG*E(@EPYnYFi+ZyT|y}EsDan>Hmhw+8`yc;v?;D!4G|8S2UH@C9w zGiw@GYo@Ih^15>lvhc#(q}MKOFUiM(yVeJ%Bl%jVCUO^%*U@=BT5jc7Jm1o8;XC$b zM^%m$A3yM@aDJV31dmGhw~sp> zF5#!X#nWH!15QMC+TJKq??m`8h_@wUH%`H3a(n8qVyhGUZ2Nbf#;*LV*b^@4jD_uy zwzvr+sr67hHBEcOUwJ*_&VGB&X-;{iu5BA2aa?4gg2=+i!N?A5WyuU++3;}&bP`PpRC_J_aGWyAJ$YLsN z(S$EC`rY%mFpu!%4OsY;IlMCm)39poW&ioMnd$dN(AM+L;NJ=-cE;Bm;ho60R&glz zwR?Nrq|Y68YD}jdg}I!eFbVgtNyBLbC$>89!9n;?e9HJ08y>b?W`ENjd(0h=_&cx8 zQTY>ROy-`uI0_> zkKX$Cm(%y9Zod+n|Kkz-2v62~EVOd_~=&AU<$4{q? zf7z$s;Uj(9{E=EGXL7EcaohPKhYu_BE*Oh!0&{AWJkpiR&X>HM+{q&ubmxCx>e$-H zHxoyUIl5|)bvD2CTC7QGeS7IW?fA${d)phiFa43?0j+&6{t*c$c5Lmf?`^ko7oT=v z`Z_vN_x^WfT<&_9v`-k_Y2!fg0R9t;7c+b?#^191j_~)+8U7eke2B)0`n#u^rMB*- zmHf-~aE|0?Mew1d?Va`2M9tN$>UtAf-Ba_SmU1DVcw*kDqqf3KOg<&jLkc-!i)KZ_=5R0pyAaNZ#Sfg|^VaNsBYba-oO3#>eWoUbVYQI=i2B02oQU0xV`GRmY!{9b zv(*j;9!dXWu_J0H4?~{Bh7ZQE<&|HK3ga*nS>)81ZePU(8!%F0ANZ9u%)9J6YvaWT zcHwt_Eb@iO&!qo@@qZ}#lZgu__KRu9S8u~l#`jYCUQYWXk*`Mb9kss_`E=rD`XlSL z*l$MSpXBoqYrGJhJHQ)I_q;v+XxhDZXBs5Fku$jOXWj~YSjiJE_%{*%9&+9aF}%n>rM-^s+^oa)U8=8c2@ z3IB=H7LOP8{e=_ZPT@MdkWY2Qhy8f$s)5k&$?Pg@1TSth?VFOWk3|cz!-(;3K@nx7u&^jt9OyM({O)GuRrz)_Nr| zABoPl5-C2w?uTN#^IwWidxsysr&jFsV%jf9z8ncp)*F#<%h*ZJ_+N|t3z1)r{nhk; zB|5wqS!oIyTc;MaGc|Yp6Mdxp29(9H$QtoSXg+w~k$MBZGk)039S_U%I`X#k(`kE4Ms(f(9qTuS-wECg+TU7N_&{*t z^iT1LDeibrzAtvZpPWhFXKJzBOX{X(YLMJ7=g67VQCmK6;mDK=IZ6y{!@-KEp)q`7 zmKfuj561J?IO`-HP6*7y2<*bGZ~ys`*ssO^;l#v0UykG+U+(gV-^=?;(O*c+C!=49 zd@S;*w0|>wUyL?K<{Q4>U&z?k)BpUfUEf-m*Rx_v^`EJiKx=C!`~Bnby_~V(fct;^mGu7$k&nm5gK%O$6UqED`1jku?}C9JX_xQC_#(Ks#APny z_fNMrPu9Va5gzz98`0OlC$9h*{#{`iyaxsvz+blqJEbo2hLx7iof!S z51B_!)xlv@Q(xg?XBTI zvV(a%@Lm*m=y2e0rTm#^@kRU{c7+p_-{g9xChBD0Gc`H2-054S&se#T^K)kBQ{TjF z-!6G?M()Rv_J9F#@}^!t6kmibuhow?!GXWSjGv1H+w~oX8|DjEcjI-t!#@>YFjv13 z;QfP%|4?+kbLQ6~>92op204n>yf8mbycqw>k#M0n z`O#=&zY+c0>HqoIeBp?1DBK)>Exuok{TtE0mUi}0^CquyJabN*Ad^m8zN-)3Blxi+ zm2dewd1UMRk?7=8zH5%OVQj!;@+qfVextSd#f`)mTjP7oq|FuCblQC7Z(aWMP5qf` zTPL=8t;MI!FQ)pU*+<5iS6uAXQv142+y9;=UW|;%o8WzzSD7n%<4IzCN8wCyBG{k! zknbG78Q>Wo4ku>py8oLKc;TJ!7U0Dqv3JHzzRWSgE#FT&_B5|O-o%c;@pd*^jnq%u zd16;BV$YKccIs2_3{IINbzG_6P;dD#_S|3Yu;zrkj^Lu^e0SnrOkD8gyQjX7o=W@m z_+O2{YVP*>)(^f%uvx#8^~FD68a}@o|7+1R@h`=nSnmn!uEf^256-=r{&2wW4*&ju zcgy@)$2;M>3KzurJzc*a@b4Gn!-+4)_p!7;5&i3tzxu{_;1AON8(Gxi|HCda$|E4h_xad-iLBWsb} zQfu?~IJ)N0ZvTm~mbpgH5Bp?A)bF(KbjKRQXT15(8p~R}pAlU+5B|Lm#f$r5cYCt0 zGY`4{Hg@D4*f0;f-m7{a0r5^0C*rHV@Bt5Ce+Ba+c&DfF#zxFlebG3v660^F z@GJlJ=^XN3Igh{2CI{y%ecl~Cb4E2?YW7&#Pez`~m?sh+Ug#gHA{(g6+c9XJWtH{lV;dJ@!W;pNs#)k=*qJNBo@?CkAXs@2^CDA^qv| zEhQd5HvYXo5qBhff@*lmDOMkoY zw>P=Qi;9)w#B6m?<4DbE9AD2tiy{2xoY6CD?FhTHwzVR6^LKlRi_rQ;#$3uaFP}Bd zPhkB!qVop$J#pR#e&100zVyegetZbV7f!&ld;OiU? zUW5lYf*bXFB3$zZRzB^OeG?r~s~z8p&lz@~w{)Lx8IwG(#MkeD_eKY6{^nC}hnmY- z^I$e7)NN+I4G(JlWbEK`{z~Mpy)pjI9|mK;AOF`PpH1HfBQL}bZsFhGfM6eP{hfap z`?2Wor+)Y5&i7lQesg?0+SuS84;C&M^Hgj;??d#=-0uGQ#YpaWzaP96*21sxx%j@E z*guH=?X-V2@e$ut>$f65b34X2pgqBM<>kTX=qc`@?d`%H*aD;8V$qY z#xzI9jFZdcU;eZ!Mt;2!;RNm8k>ho8j9;05W}V_w@57$#rD|=h-lx3{{ubC)_ZdBC z^B$c&{3&1h)`>C3e(7G19`*qD;lqgdcB=N(ZF__dxG=(*dMkqY8Em`vVST`NMB8r+ zF?cb;2iy@Wj=+4yhePy?;L{ARM*8q;eebR8KNlTwzf|3_?^6?bIp=mh*e-s=cAlNs zO*@;KJrWxiB0Kj!jx2rZs7{fUb=cAqZYyRkr%nfy#TF*~^RJ8C|j zSbUJXNZ~X&(uTddU(`VUwat~h7VgrP?-@>rHFwp<_{@)68}85Ct@+h&*#|K$GSY93 z!ZU1GFJi4b(T?al{l}j1ZTzQ?R>c7t?*mLf7>cga_bwa#GV_wb>}3{ z>QVVrF`yT7ENr$VcWVKKx)}f9Q6Oscv#;OlqJWc(k97|K-Rp#P^pY zzZ>6gM*l?Q#rT6ecwc!J28_Qq@>F~uN;~+iZ>X%lpG-UcHDiAy5)Lh~>6?+d$HTAU z+8fb7pT1At#`6v1em3%r^nE7nH)CfWe`~F8rSM|?OKE>0`eW&fe`Y^JE^cB+eEV!z zOm4PZ=B#g(?fm+a17nS=_aeS29u8qiKVNbtciw?Pllc6%Zk5l(E%WgaW5X}`Rnyu} zVrEONl|929ez0^Ll=S%#WYxaKZX=Q%0vj=Mp(lInSF=g}hoFiwGle=#v_{R$p zo{R_^2fQQRfkj6K8$Vz>7>4h9N8-neLEh1J`os}$$0FgvY}u{r_e6NHGavD96mmL( zS-&m(&B>irxyOm@eZ%#sh1xunxgU!!c`iJQk)z~yrElLvq7Rq6Lp66evD7oMQx4TV z^QlGMjlpewAHi5KIlq*4bp72wyqG^1c`C%hFa+?ub&_sPg_CiWMjKNZP3{$}JifxmD0X3}1Xn;%dASJVFW$YP}0{K7CS=6zTC?5UgeN9MqX!h6U4wnBm62f$&CqT}ZtF z>6`D2KT>=W%l_`jyCQfIPWW%0YBTHWgk`F)YzTxcSpS~YMvPNpWc~pVr32e?)vf3NakL@ll8)p{a4cd7t@a3 zzZA*(^>&0a##y^~a1+~_&hT&q59GkvE+yyN)(-C0qtOu@SlRn}Fzth#j4>9TH%!Mb zk9DWW<*7sRDfdh5B67aco|$WBt;%J_+uI;F>F@Kqe|FlNKY3ZtN95f?BeD1em$0+tQ|{$ZPJg&HJO2IsiPguiF4@bJLwL^`mCq;JmviH>wefO`aj$$J zzU<3gffEC-?72BH?Za7GcJp9Hv(NtRbtQ&WtaHU4mttOLj1(8}A=uv0iw)y%PYhpV z()ZGb8?jgV`B!B07~}Zy!nYNS<4w(zw(q7LUU)aoHxzE*13hrUnDC!b01Irgu_{(9`+iT>@hSLCZ{eQH_1o7|oIWXOGTxa6GHpZto^S8HY;>VbbFVoiC}t~W9{ zS;>`rR1WaumBb|X^0ICs8E?<_Q+rpZ2z(QJ+mQ(`_S&+w(?7)Uwaw3WCvp>UpO47? zj*PpGMDxKjypP}>4<r1+8f%)6|?4lleLM`rkf8#^+? ziQ)y!C$E)vJWx|-kZU|hzwfK!29Bg2JM}Bhq!!!RoSKGx2{i)klPw`n} z!;CxW`N&5j&twe2hhWJ4zu$=dbjAda^}FKpu|FGmIei)D{vWBO_`+SVjT^;}Xf-ZQ zB+mO&m^QEb5$9spzlG1VvxeUk^TV+}8vmPV|5WVoVf}3UuSH<%vGhF>oxK-sQtzqe z@EuI<^y8#;-MKSohS`;KkJxhPPFeC7pD{DJz>P)ApSCXd;Vec%J^l0kAt`=~uf*Phxp z(w;=SrQ&1LyZg+!TMDx|eZuq%a?>@AzOI{zvp;N5s__cE;lZ zUGuSVfn=TiooPq3SJobU(Kz9)7Unkm`}|ja3e=@v+&`O*faJAlUGKnWqrV*a>BN0L@t=sinK)xo z>$>-Pj5^m?^WLQYv9xoy!-x3!M u`9Q?|`E=UPCFX@l`u8i5mu_PcJXSd3UBZ!R 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

J@~-neKlvhm@zs1gyjwY}`PBS(6$ABM$z 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≪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 + + + 1175 + 561.72162965205 + 297 + 107.52060579962 + 99.73 + + + diff --git a/tests/qgis/input/faults_clip.cpg b/tests/qgis/input/faults_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/faults_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/faults_clip.dbf b/tests/qgis/input/faults_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..35f7f0eb882f291f8a96c7470670505b2ea251e9 GIT binary patch literal 49957 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

sPgE;L>n#AP!K$JA8#-%^CHcexe0 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

S0n(lX{%g^Q0Xh?F4B@NIOH?A<|Bfc8s)h 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 S3*P&=bk^1Ae|?$kzyAT%W_Sny literal 0 HcmV?d00001 diff --git a/tests/qgis/input/geol_clip_no_gaps.shx b/tests/qgis/input/geol_clip_no_gaps.shx new file mode 100644 index 0000000000000000000000000000000000000000..78d33f6fad506c30cb2a02c956aeb3c09e2ed983 GIT binary patch literal 588 zcmZvZPe>GT7>1vj*;SE<4TG$Ov`fY6r7n6Xa6ux8z=OdeTkb*n10I$RQ4l%(0}-Pn z0tt*IJQzf*9>hbbhobco5_OkO9y)aB&|#6DaZkby{CMX3e((E!-!O2hou*H4awmec zciAXDKYy<)eXN)Js}LQN6}hG+1F7%U+is&j(BG;)DnR=+Xsba-PE8C)KA6!BN7W~~>~Y|xoR$Of z8yp;kTrafzgEOaPC-eKk9CgbFLQ6 zsl&Jox8cTsjN#U2_p`pes88+vkIpggL8sh;$=~dKSL{1xKb&!1H$1IMd(%(MFTt}N z^<{W&e&TQrjy^C87|GOXhD)9cU p`?tYzGOy-uQcu5hpH^qDc^|~EUiTjq;EN~Bz}Jb!{7t<<{2!hAUl0HQ literal 0 HcmV?d00001 diff --git a/tests/qgis/input/structure_clip.cpg b/tests/qgis/input/structure_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/structure_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/structure_clip.dbf b/tests/qgis/input/structure_clip.dbf new file mode 100644 index 0000000000000000000000000000000000000000..7f15994662bc5ff545f2de71f06d3f62ed447adb GIT binary patch literal 45216 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 YjoSzI&4S92uNH8y@4L;dnxqf@8_}D4@c;k- literal 0 HcmV?d00001 diff --git a/tests/qgis/input/structure_clip.shx b/tests/qgis/input/structure_clip.shx new file mode 100644 index 0000000000000000000000000000000000000000..cb743f810babb48dc0aae0199ad824ac44b006f8 GIT binary patch literal 1044 zcmZwEUnoOy6u|Mjd$&C-X~~0@Fb@bxlC*>?Ns^Ex-Lxc0LXsp&OG1()NkUSzwB$h& zk|eD~NfI9XOUnb2e-Dy-`+Z;3elMSX=X5%!b0jIzCWU;mEvQJ6Nzc7}Rk%L(W7YXU zo^#lvsSfYX{Yi>bUAEs|wfQtPWcmMKhW<7BVky>Q6L#SM zPT&Iia2x%2fsgnuv>1oe_hYeR45p$Bi?JFTumk&X3}?}cn|O$4c!#g}E3}581CuZd P3$YU2*owV4g42e7fhbUu literal 0 HcmV?d00001 diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py new file mode 100644 index 0000000..b12f56a --- /dev/null +++ b/tests/qgis/test_sampler_decimator.py @@ -0,0 +1,93 @@ +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsRasterLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis,QgsApplication +from qgis.testing import start_app +from m2l.processing.algorithms.sampler import SamplerAlgorithm +from m2l.processing.provider import Map2LoopProvider + +class TestSamplerDecimator(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + self.structure_file = self.input_dir / "structure_clip.shp" + self.dtm_file = self.input_dir / "dtm_rp.tif" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + self.assertTrue(self.structure_file.exists(), f"structure not found: {self.structure_file}") + self.assertTrue(self.dtm_file.exists(), f"dtm not found: {self.dtm_file}") + + def test_decimator_1_with_structure(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + structure_layer = QgsVectorLayer(str(self.structure_file), "structure", "ogr") + dtm_layer = QgsRasterLayer(str(self.dtm_file), "dtm") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertTrue(structure_layer.isValid(), "structure layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + self.assertGreater(structure_layer.featureCount(), 0, "structure layer should have features") + + QgsMessageLog.logMessage(f"geology layer valid: {geology_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"structure layer valid: {structure_layer.isValid()}", "TestDecimator", Qgis.Critical) + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"structure layer: {structure_layer.featureCount()} features", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"spatial data- structure layer", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"sampler type: Decimator", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"decimation: 1", "TestDecimator", Qgis.Critical) + + algorithm = SamplerAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'GEOLOGY': geology_layer, + 'SPATIAL_DATA': structure_layer, + 'SAMPLER_TYPE': 0, + 'DECIMATION': 1, + 'SPACING': 200.0, + 'SAMPLED_CONTACTS': 'memory:decimated_points' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + + try: + QgsMessageLog.logMessage("Starting decimator sampler algorithm...", "TestDecimator", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestDecimator", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('SAMPLED_CONTACTS', result, "Result should contain SAMPLED_CONTACTS key") + + QgsMessageLog.logMessage("Decimator sampler test completed successfully!", "TestDecimator", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Decimator sampler test error: {str(e)}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestDecimator", Qgis.Critical) + + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestDecimator", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestDecimator", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py new file mode 100644 index 0000000..a542b85 --- /dev/null +++ b/tests/qgis/test_sampler_spacing.py @@ -0,0 +1,80 @@ +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.testing import start_app +from m2l.processing.algorithms.sampler import SamplerAlgorithm +from m2l.processing.provider import Map2LoopProvider + +class TestSamplerSpacing(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + + def test_spacing_50_with_geology(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"spatial data- geology layer", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"sampler type: Spacing", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"spacing: 50", "TestSampler", Qgis.Critical) + + algorithm = SamplerAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'DTM': None, + 'GEOLOGY': None, + 'SPATIAL_DATA': geology_layer, + 'SAMPLER_TYPE': 1, + 'DECIMATION': 1, + 'SPACING': 50.0, + 'SAMPLED_CONTACTS': 'memory:sampled_points' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + try: + QgsMessageLog.logMessage("Starting spacing sampler algorithm...", "TestSampler", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestSampler", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('SAMPLED_CONTACTS', result, "Result should contain SAMPLED_CONTACTS key") + + QgsMessageLog.logMessage("Spacing sampler test completed successfully!", "TestSampler", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Spacing sampler test error: {str(e)}", "TestSampler", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestSampler", Qgis.Critical) + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestSampler", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestSampler", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + +if __name__ == '__main__': + unittest.main() From 26399b2307a225aca2cb5d3fd4c1b392b0e699ac Mon Sep 17 00:00:00 2001 From: noellehmcheng <143368485+noellehmcheng@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:15:11 +0800 Subject: [PATCH 082/116] Processing/processing tools basal contacts test (#21) * input files * fix: change stratigraphic column QgsProcessingParameterFeatureSink description parameter to string * feat add formation column mapping and validation for strat column * tester.yml * input file * test_basal_contacts * fix import in test_basal_contacts.py * fix strati_column in test_basal_contacts * removed duplicated input files --- .../algorithms/extract_basal_contacts.py | 31 ++++- tests/qgis/test_basal_contacts.py | 116 ++++++++++++++++++ 2 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 tests/qgis/test_basal_contacts.py diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index d7beb34..c78653e 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -78,6 +78,17 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterField( + 'FORMATION_FIELD', + 'Formation Field', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='formation', + optional=True + ) + ) + self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_FAULTS, @@ -127,7 +138,14 @@ def processAlgorithm( geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) - ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) + + if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): + raise QgsProcessingException("no stratigraphic column found") + + if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): + feedback.pushInfo("no units to ignore specified") + # if strati_column and strati_column.strip(): # strati_column = [unit.strip() for unit in strati_column.split(',')] # Save stratigraphic column settings @@ -138,10 +156,15 @@ def processAlgorithm( ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) + formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - mask = ~geology['Formation'].astype(str).str.strip().isin(ignore_units) - geology = geology[mask].reset_index(drop=True) + if formation_field and formation_field in geology.columns: + mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + geology = geology[mask].reset_index(drop=True) + feedback.pushInfo(f"filtered by formation field: {formation_field}") + else: + feedback.pushInfo(f"no formation field found: {formation_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -154,7 +177,7 @@ def processAlgorithm( feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( self, - contact_extractor.basal_contacts, + basal_contacts, parameters=parameters, context=context, output_key=self.OUTPUT, diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py new file mode 100644 index 0000000..19c56dc --- /dev/null +++ b/tests/qgis/test_basal_contacts.py @@ -0,0 +1,116 @@ +import unittest +from pathlib import Path +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.testing import start_app +from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm +from m2l.processing.provider import Map2LoopProvider + +class TestBasalContacts(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.qgs = start_app() + + cls.provider = Map2LoopProvider() + QgsApplication.processingRegistry().addProvider(cls.provider) + + def setUp(self): + self.test_dir = Path(__file__).parent + self.input_dir = self.test_dir / "input" + + self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" + self.faults_file = self.input_dir / "faults_clip.shp" + + self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") + + if not self.faults_file.exists(): + QgsMessageLog.logMessage(f"faults not found: {self.faults_file}, will run test without faults", "TestBasalContacts", Qgis.Warning) + + def test_basal_contacts_extraction(self): + + geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr") + + self.assertTrue(geology_layer.isValid(), "geology layer should be valid") + self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") + + faults_layer = None + if self.faults_file.exists(): + faults_layer = QgsVectorLayer(str(self.faults_file), "faults", "ogr") + self.assertTrue(faults_layer.isValid(), "faults layer should be valid") + self.assertGreater(faults_layer.featureCount(), 0, "faults layer should have features") + QgsMessageLog.logMessage(f"faults layer: {faults_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) + + QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) + + strati_column = [ + "Turee Creek Group", + "Boolgeeda Iron Formation", + "Woongarra Rhyolite", + "Weeli Wolli Formation", + "Brockman Iron Formation", + "Mount McRae Shale and Mount Sylvia Formation", + "Wittenoom Formation", + "Marra Mamba Iron Formation", + "Jeerinah Formation", + "Bunjinah Formation", + "Pyradie Formation", + "Fortescue Group", + "Hardey Formation", + "Boongal Formation", + "Mount Roe Basalt", + "Rocklea Inlier greenstones", + "Rocklea Inlier metagranitic unit" + ] + + algorithm = BasalContactsAlgorithm() + algorithm.initAlgorithm() + + parameters = { + 'GEOLOGY': geology_layer, + 'UNIT_NAME_FIELD': 'unitname', + 'FORMATION_FIELD': 'formation', + 'FAULTS': faults_layer, + 'STRATIGRAPHIC_COLUMN': strati_column, + 'IGNORE_UNITS': [], + 'BASAL_CONTACTS': 'memory:basal_contacts' + } + + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + + try: + QgsMessageLog.logMessage("Starting basal contacts algorithm...", "TestBasalContacts", Qgis.Critical) + + result = algorithm.processAlgorithm(parameters, context, feedback) + + QgsMessageLog.logMessage(f"Result: {result}", "TestBasalContacts", Qgis.Critical) + + self.assertIsNotNone(result, "result should not be None") + self.assertIn('BASAL_CONTACTS', result, "Result should contain BASAL_CONTACTS key") + + basal_contacts_layer = context.takeResultLayer(result['BASAL_CONTACTS']) + self.assertIsNotNone(basal_contacts_layer, "basal contacts layer should not be None") + self.assertTrue(basal_contacts_layer.isValid(), "basal contacts layer should be valid") + self.assertGreater(basal_contacts_layer.featureCount(), 0, "basal contacts layer should have features") + + QgsMessageLog.logMessage(f"Generated {basal_contacts_layer.featureCount()} basal contacts", + "TestBasalContacts", Qgis.Critical) + + QgsMessageLog.logMessage("Basal contacts test completed successfully!", "TestBasalContacts", Qgis.Critical) + + except Exception as e: + QgsMessageLog.logMessage(f"Basal contacts test error: {str(e)}", "TestBasalContacts", Qgis.Critical) + QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestBasalContacts", Qgis.Critical) + import traceback + QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestBasalContacts", Qgis.Critical) + raise + + finally: + QgsMessageLog.logMessage("=" * 50, "TestBasalContacts", Qgis.Critical) + + @classmethod + def tearDownClass(cls): + QgsApplication.processingRegistry().removeProvider(cls.provider) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 72f59498754e2febae672f2f1bf819f38a94a62c Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:59:25 +0930 Subject: [PATCH 083/116] fix: fix syntac error --- m2l/processing/algorithms/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index a76218a..2cdc5a3 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -37,7 +37,7 @@ QgsPointXY, QgsVectorLayer, QgsWkbTypes, - QgsCoordinateReferenceSystem + QgsCoordinateReferenceSystem, QgsVectorLayer, QgsWkbTypes, QgsCoordinateReferenceSystem From 30c7e2047c63dd5f4648fb8771c3d8bee68f47cb Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:03:23 +0930 Subject: [PATCH 084/116] fix: remove redundant parameters in SamplerAlgorithm --- m2l/processing/algorithms/sampler.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 2cdc5a3..3f4c5cf 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -80,25 +80,20 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.addParameter( - QgsProcessingParameterEnum( QgsProcessingParameterEnum( self.INPUT_SAMPLER_TYPE, "SAMPLER_TYPE", ["Decimator", "Spacing"], defaultValue=0 - ["Decimator", "Spacing"], - defaultValue=0 ) ) self.addParameter( - QgsProcessingParameterRasterLayer( QgsProcessingParameterRasterLayer( self.INPUT_DTM, "DTM", [QgsProcessing.TypeRaster], optional=True, - optional=True, ) ) @@ -126,8 +121,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "DECIMATION", QgsProcessingParameterNumber.Integer, defaultValue=1, - QgsProcessingParameterNumber.Integer, - defaultValue=1, optional=True, ) ) @@ -138,8 +131,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "SPACING", QgsProcessingParameterNumber.Double, defaultValue=200.0, - QgsProcessingParameterNumber.Double, - defaultValue=200.0, optional=True, ) ) @@ -148,7 +139,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: QgsProcessingParameterFeatureSink( self.OUTPUT, "Sampled Points", - "Sampled Points", ) ) @@ -180,7 +170,6 @@ def processAlgorithm( spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - if sampler_type == "Decimator": if sampler_type == "Decimator": feedback.pushInfo("Sampling...") sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) @@ -188,7 +177,7 @@ def processAlgorithm( sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) samples = sampler.sample(spatial_data_gdf) - if sampler_type == "Spacing": + if sampler_type == "Spacing": feedback.pushInfo("Sampling...") sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm_gdal, geology_data=geology) From 5dbcfca11be21d3718a9f8ae62a5ef95552876eb Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:04:39 +0930 Subject: [PATCH 085/116] fix: remove redundant imports --- m2l/processing/algorithms/sampler.py | 38 ---------------------------- 1 file changed, 38 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 3f4c5cf..28e5184 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -26,20 +26,14 @@ QgsProcessingParameterFeatureSource, QgsProcessingParameterRasterLayer, QgsProcessingParameterEnum, - QgsProcessingParameterRasterLayer, - QgsProcessingParameterEnum, QgsProcessingParameterNumber, QgsFields, - QgsFields, QgsField, QgsFeature, QgsGeometry, QgsPointXY, QgsVectorLayer, QgsWkbTypes, - QgsCoordinateReferenceSystem, - QgsVectorLayer, - QgsWkbTypes, QgsCoordinateReferenceSystem ) # Internal imports @@ -167,17 +161,12 @@ def processAlgorithm( geology = qgsLayerToGeoDataFrame(geology) spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None - spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data) - dtm_gdal = gdal.Open(dtm.source()) if dtm is not None and dtm.isValid() else None if sampler_type == "Decimator": feedback.pushInfo("Sampling...") sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) samples = sampler.sample(spatial_data_gdf) - sampler = SamplerDecimator(decimation=decimation, dtm_data=dtm_gdal, geology_data=geology) - samples = sampler.sample(spatial_data_gdf) - if sampler_type == "Spacing": feedback.pushInfo("Sampling...") sampler = SamplerSpacing(spacing=spacing, dtm_data=dtm_gdal, geology_data=geology) @@ -203,33 +192,6 @@ def processAlgorithm( crs ) - if samples is not None and not samples.empty: - for _index, row in samples.iterrows(): - feature = QgsFeature(fields) - - # decimator has z values - if 'Z' in samples.columns and pd.notna(row.get('Z')): - wkt = f"POINT Z ({row['X']} {row['Y']} {row['Z']})" - feature.setGeometry(QgsGeometry.fromWkt(wkt)) - else: - #spacing has no z values - feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y']))) - - feature.setAttributes([ - str(row.get('ID', '')), - float(row.get('X', 0)), - float(row.get('Y', 0)), - float(row.get('Z', 0)) if pd.notna(row.get('Z')) else 0.0, - str(row.get('featureId', '')) - ]) - - sink.addFeature(feature) - - fields, - QgsWkbTypes.PointZ if 'Z' in (samples.columns if samples is not None else []) else QgsWkbTypes.Point, - crs - ) - if samples is not None and not samples.empty: for _index, row in samples.iterrows(): feature = QgsFeature(fields) From 90663cf2ce192a087d56a79488a6abd62bd6dc5f Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:11:23 +0930 Subject: [PATCH 086/116] fix: remove unused QgsSettings --- m2l/processing/algorithms/sorter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 20d3cd7..f80cfe0 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -118,8 +118,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue="Observation projections", # Age-based is safest default ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/sorter_strati_column", "") self.addParameter( QgsProcessingParameterFeatureSource( From 4ff75469fea8ce5d46db503e2f71e7ef29113ea5 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:59:22 +0930 Subject: [PATCH 087/116] fix: updated tearDownClass --- tests/qgis/test_sampler_decimator.py | 2 +- tests/qgis/test_sampler_spacing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index b12f56a..13e7228 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -87,7 +87,7 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + # QgsApplication.processingRegistry().removeProvider(cls.provider) if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index a542b85..fc3a523 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -74,7 +74,7 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + # QgsApplication.processingRegistry().removeProvider(cls.provider) if __name__ == '__main__': unittest.main() From 14db8d05abc7bb12983083001f5e52ce0e87bda1 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:00:52 +0930 Subject: [PATCH 088/116] fix: add pass statement --- tests/qgis/test_sampler_decimator.py | 1 + tests/qgis/test_sampler_spacing.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index 13e7228..15782bc 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -88,6 +88,7 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): # QgsApplication.processingRegistry().removeProvider(cls.provider) + pass if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index fc3a523..4016d5f 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -75,6 +75,6 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): # QgsApplication.processingRegistry().removeProvider(cls.provider) - + pass if __name__ == '__main__': unittest.main() From ec993bb982116e28936bfa9b2f184475c182b952 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 22 Sep 2025 13:22:54 +0800 Subject: [PATCH 089/116] update sampler tests --- tests/qgis/test_sampler_decimator.py | 11 ++++++++++- tests/qgis/test_sampler_spacing.py | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index b12f56a..bb1864c 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -34,22 +34,27 @@ def test_decimator_1_with_structure(self): self.assertTrue(geology_layer.isValid(), "geology layer should be valid") self.assertTrue(structure_layer.isValid(), "structure layer should be valid") + self.assertTrue(dtm_layer.isValid(), "dtm layer should be valid") self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") self.assertGreater(structure_layer.featureCount(), 0, "structure layer should have features") QgsMessageLog.logMessage(f"geology layer valid: {geology_layer.isValid()}", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"structure layer valid: {structure_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm layer valid: {dtm_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm source: {dtm_layer.source()}", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"structure layer: {structure_layer.featureCount()} features", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"spatial data- structure layer", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"sampler type: Decimator", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"decimation: 1", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm: {self.dtm_file.name}", "TestDecimator", Qgis.Critical) algorithm = SamplerAlgorithm() algorithm.initAlgorithm() parameters = { + 'DTM': dtm_layer, 'GEOLOGY': geology_layer, 'SPATIAL_DATA': structure_layer, 'SAMPLER_TYPE': 0, @@ -87,7 +92,11 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index a542b85..a49042f 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -74,7 +74,11 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() From 83c170376803fef39e6ba36dfc7f15c255b5a44d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 22 Sep 2025 13:24:13 +0800 Subject: [PATCH 090/116] update sampler --- m2l/processing/algorithms/sampler.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 28e5184..726d44a 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -10,7 +10,7 @@ """ # Python imports from typing import Any, Optional -from qgis.PyQt.QtCore import QVariant +from qgis.PyQt.QtCore import QMetaType from osgeo import gdal import pandas as pd @@ -154,8 +154,11 @@ def processAlgorithm( if spatial_data is None: raise QgsProcessingException("Spatial data is required") - if sampler_type == "Decimator" and geology is None: - raise QgsProcessingException("Geology is required") + if sampler_type == "Decimator": + if geology is None: + raise QgsProcessingException("Geology is required") + if dtm is None: + raise QgsProcessingException("DTM is required") # Convert geology layers to GeoDataFrames geology = qgsLayerToGeoDataFrame(geology) @@ -173,11 +176,11 @@ def processAlgorithm( samples = sampler.sample(spatial_data_gdf) fields = QgsFields() - fields.append(QgsField("ID", QVariant.String)) - fields.append(QgsField("X", QVariant.Double)) - fields.append(QgsField("Y", QVariant.Double)) - fields.append(QgsField("Z", QVariant.Double)) - fields.append(QgsField("featureId", QVariant.String)) + fields.append(QgsField("ID", QMetaType.Type.QString)) + fields.append(QgsField("X", QMetaType.Type.Float)) + fields.append(QgsField("Y", QMetaType.Type.Float)) + fields.append(QgsField("Z", QMetaType.Type.Float)) + fields.append(QgsField("featureId", QMetaType.Type.QString)) crs = None if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: From 5205cf562e1e42d4b8f3b1ff607175f21d30c901 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 23 Sep 2025 18:26:48 +0800 Subject: [PATCH 091/116] update strati column and bounding box --- .../algorithms/thickness_calculator.py | 58 +++++++++++++------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index e10604d..6172a94 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -57,6 +57,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_GEOLOGY = 'GEOLOGY' INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' + INPUT_STRATIGRAPHIC_COLUMN_LAYER = 'STRATIGRAPHIC_COLUMN_LAYER' OUTPUT = "THICKNESS" @@ -97,17 +98,6 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) - bbox_settings = QgsSettings() - last_bbox = bbox_settings.value("m2l/bounding_box", "") - self.addParameter( - QgsProcessingParameterMatrix( - self.INPUT_BOUNDING_BOX, - description="Bounding Box", - headers=['minx','miny','maxx','maxy'], - numberRows=1, - defaultValue=last_bbox - ) - ) self.addParameter( QgsProcessingParameterNumber( self.INPUT_MAX_LINE_LENGTH, @@ -141,6 +131,15 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue='Formation' ) ) + + self.addParameter( + QgsProcessingParameterFeatureSource( + 'STRATIGRAPHIC_COLUMN_LAYER', + 'Stratigraphic Column Layer (from sorter)', + [QgsProcessing.TypeVector], + optional=True + ) + ) strati_settings = QgsSettings() last_strati_column = strati_settings.value("m2l/strati_column", "") @@ -150,7 +149,8 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: description="Stratigraphic Order", headers=["Unit"], numberRows=0, - defaultValue=last_strati_column + defaultValue=last_strati_column, + optional=True ) ) self.addParameter( @@ -207,17 +207,39 @@ def processAlgorithm( max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context) - bbox_settings = QgsSettings() - bbox_settings.setValue("m2l/bounding_box", bounding_box) - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', stratigraphic_order) + geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + extent = geology_layer.extent() + bounding_box = { + 'minx': extent.xMinimum(), + 'miny': extent.yMinimum(), + 'maxx': extent.xMaximum(), + 'maxy': extent.yMaximum() + } + stratigraphic_column_layer = self.parameterAsVectorLayer(parameters, self.INPUT_STRATIGRAPHIC_COLUMN_LAYER, context) + stratigraphic_order = None + if stratigraphic_column_layer is not None and stratigraphic_column_layer.isValid(): + stratigraphic_order=[] + stratigraphic_order_df = qgsLayerToDataFrame(stratigraphic_column_layer) + stratigraphic_order_df = stratigraphic_order_df.sort_values('order') + for _, row in stratigraphic_order_df.iterrows(): + stratigraphic_order.append(row['unit_name']) + + else: + matrix_stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + if matrix_stratigraphic_order: + stratigraphic_order = [row[0] for row in matrix_stratigraphic_order if row and len(row) > 0] + else: + raise QgsProcessingException("Stratigraphic column layer is required") + if stratigraphic_order: + matrix = [[unit] for unit in stratigraphic_order] + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', matrix) # convert layers to dataframe or geodataframe units = qgsLayerToDataFrame(geology_data) geology_data = qgsLayerToGeoDataFrame(geology_data) @@ -249,8 +271,6 @@ def processAlgorithm( structure_data = structure_data.rename(columns=rename_map) sampled_contacts = qgsLayerToDataFrame(sampled_contacts) dtm_data = qgsRasterToGdalDataset(dtm_data) - bounding_box = matrixToDict(bounding_box) - feedback.pushInfo("Calculating unit thicknesses...") if thickness_type == "InterpolatedStructure": thickness_calculator = InterpolatedStructure( dtm_data=dtm_data, From a259caef14bfb67736a65de6e2581ccd33803a7c Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 23 Sep 2025 18:27:29 +0800 Subject: [PATCH 092/116] fix sample contacts data type --- m2l/processing/algorithms/thickness_calculator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 6172a94..afba1e8 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -270,6 +270,9 @@ def processAlgorithm( if rename_map: structure_data = structure_data.rename(columns=rename_map) sampled_contacts = qgsLayerToDataFrame(sampled_contacts) + sampled_contacts['X'] = sampled_contacts['X'].astype(float) + sampled_contacts['Y'] = sampled_contacts['Y'].astype(float) + sampled_contacts['Z'] = sampled_contacts['Z'].astype(float) dtm_data = qgsRasterToGdalDataset(dtm_data) if thickness_type == "InterpolatedStructure": thickness_calculator = InterpolatedStructure( From 00d4bb82b4b6a9dfdd21264cdafc75dc8d71990b Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 23 Sep 2025 18:47:40 +0800 Subject: [PATCH 093/116] add orientation type for structure data --- m2l/processing/algorithms/thickness_calculator.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index afba1e8..0210f68 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -55,6 +55,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_DIPDIR_FIELD = 'DIPDIR_FIELD' INPUT_DIP_FIELD = 'DIP_FIELD' INPUT_GEOLOGY = 'GEOLOGY' + INPUT_THICKNESS_ORIENTATION_TYPE = 'THICKNESS_ORIENTATION_TYPE' INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' INPUT_STRATIGRAPHIC_COLUMN_LAYER = 'STRATIGRAPHIC_COLUMN_LAYER' @@ -167,6 +168,14 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPoint], ) ) + self.addParameter( + QgsProcessingParameterEnum( + 'THICKNESS_ORIENTATION_TYPE', + 'Thickness Orientation Type', + options=['Dip Direction', 'Strike'], + defaultValue=0 # Default to Dip Direction + ) + ) self.addParameter( QgsProcessingParameterField( self.INPUT_DIPDIR_FIELD, @@ -208,6 +217,8 @@ def processAlgorithm( basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) + thickness_orientation_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_ORIENTATION_TYPE, context) + is_strike = (thickness_orientation_type == 1) structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) @@ -269,6 +280,7 @@ def processAlgorithm( ) if rename_map: structure_data = structure_data.rename(columns=rename_map) + sampled_contacts = qgsLayerToDataFrame(sampled_contacts) sampled_contacts['X'] = sampled_contacts['X'].astype(float) sampled_contacts['Y'] = sampled_contacts['Y'].astype(float) @@ -278,6 +290,7 @@ def processAlgorithm( thickness_calculator = InterpolatedStructure( dtm_data=dtm_data, bounding_box=bounding_box, + is_strike=is_strike ) thicknesses = thickness_calculator.compute( units, @@ -293,6 +306,7 @@ def processAlgorithm( dtm_data=dtm_data, bounding_box=bounding_box, max_line_length=max_line_length, + is_strike=is_strike ) thicknesses =thickness_calculator.compute( units, From d2d292545e25acd7893c11764b01491699aae3c4 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:56:04 +0930 Subject: [PATCH 094/116] Processing/thickness calculator (#23) * feat: thickness calculator tool * feat: raster and dataframe handling * fix: remove unused QgsSettings * fix: updated tearDownClass * fix: add pass statement * update sampler tests * update sampler --------- Co-authored-by: Noelle Cheng --- m2l/main/vectorLayerWrapper.py | 355 +++++++++++++++--- m2l/processing/algorithms/sampler.py | 19 +- m2l/processing/algorithms/sorter.py | 2 - .../algorithms/thickness_calculator.py | 151 ++++++-- tests/qgis/test_sampler_decimator.py | 11 +- tests/qgis/test_sampler_spacing.py | 6 +- 6 files changed, 438 insertions(+), 106 deletions(-) diff --git a/m2l/main/vectorLayerWrapper.py b/m2l/main/vectorLayerWrapper.py index 5bf2da0..4476e6a 100644 --- a/m2l/main/vectorLayerWrapper.py +++ b/m2l/main/vectorLayerWrapper.py @@ -1,5 +1,5 @@ # PyQGIS / PyQt imports - +from osgeo import gdal from qgis.core import ( QgsRaster, QgsFields, @@ -12,17 +12,79 @@ QgsProcessingException, QgsPoint, QgsPointXY, + QgsProject, + QgsCoordinateTransform, + QgsRasterLayer ) -from qgis.PyQt.QtCore import QVariant, QDateTime, QVariant - +from qgis.PyQt.QtCore import QVariant, QDateTime +from qgis import processing from shapely.geometry import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon from shapely.wkb import loads as wkb_loads import pandas as pd import geopandas as gpd import numpy as np - +import tempfile +import os + +def qgsRasterToGdalDataset(rlayer: QgsRasterLayer): + """ + Convert a QgsRasterLayer to an osgeo.gdal.Dataset (read-only). + If the raster is non-file-based (e.g. WMS/WCS/virtual), we create a temp GeoTIFF via gdal:translate. + Returns a gdal.Dataset or None. + """ + if rlayer is None or not rlayer.isValid(): + return None + + # Try direct open on file-backed layers + candidates = [] + try: + candidates.append(rlayer.source()) + except Exception: + pass + try: + if rlayer.dataProvider(): + candidates.append(rlayer.dataProvider().dataSourceUri()) + except Exception: + pass + tried = set() + for uri in candidates: + if not uri: + continue + if uri in tried: + continue + tried.add(uri) + + # Strip QGIS pipe options: "path.tif|layername=..." → "path.tif" + base_uri = uri.split("|")[0] + + # Some providers store “SUBDATASET:” URIs; gdal.OpenEx can usually handle them directly. + ds = gdal.OpenEx(base_uri, gdal.OF_RASTER | gdal.OF_READONLY) + if ds is not None: + return ds + + # If we’re here, it’s likely non-file-backed. Export to a temp GeoTIFF. + tmpdir = tempfile.gettempdir() + tmp_path = os.path.join(tmpdir, f"m2l_dtm_{rlayer.id()}.tif") + + # Use GDAL Translate via QGIS processing (avoids CRS pitfalls) + processing.run( + "gdal:translate", + { + "INPUT": rlayer, # QGIS accepts the layer object here + "TARGET_CRS": None, + "NODATA": None, + "COPY_SUBDATASETS": False, + "OPTIONS": "", + "EXTRA": "", + "DATA_TYPE": 0, # Use input data type + "OUTPUT": tmp_path, + } + ) + + ds = gdal.OpenEx(tmp_path, gdal.OF_RASTER | gdal.OF_READONLY) + return ds def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: if layer is None: @@ -42,63 +104,147 @@ def qgsLayerToGeoDataFrame(layer) -> gpd.GeoDataFrame: data[f.name()].append(str(feature[f.name()])) else: data[f.name()].append(feature[f.name()]) - return gpd.GeoDataFrame(data, crs=layer.crs().authid()) - -def qgsLayerToDataFrame(layer, dtm) -> pd.DataFrame: - """Convert a vector layer to a pandas DataFrame - samples the geometry using either points or the vertices of the lines - - :param layer: _description_ - :type layer: _type_ - :param dtm: Digital Terrain Model to evaluate Z values - :type dtm: _type_ or None - :return: the dataframe object - :rtype: pd.DataFrame + return gpd.GeoDataFrame(data, crs=layer.sourceCrs().authid()) + +def qgsLayerToDataFrame(src, dtm=None) -> pd.DataFrame: """ - if layer is None: + Convert a vector layer or processing feature source to a pandas DataFrame. + Samples geometry using points or vertices of lines/polygons. + Optionally samples Z from a DTM raster. + + :param src: QgsVectorLayer or QgsProcessingFeatureSource + :param dtm: QgsRasterLayer or None + :return: pd.DataFrame with columns: X, Y, Z, and all layer fields + """ + + if src is None: return None - fields = layer.fields() - data = {} - data['X'] = [] - data['Y'] = [] - data['Z'] = [] - - for field in fields: - data[field.name()] = [] - for feature in layer.getFeatures(): - geom = feature.geometry() - points = [] - if geom.isMultipart(): - if geom.type() == QgsWkbTypes.PointGeometry: - points = geom.asMultiPoint() - elif geom.type() == QgsWkbTypes.LineGeometry: + + # --- Resolve fields and source CRS (works for both layer and feature source) --- + fields = src.fields() if hasattr(src, "fields") else None + if fields is None: + # Fallback: take fields from first feature if needed + feat_iter = src.getFeatures() + try: + first = next(feat_iter) + except StopIteration: + return pd.DataFrame(columns=["X", "Y", "Z"]) + fields = first.fields() + # Rewind iterator by building a new one + feats = [first] + list(src.getFeatures()) + else: + feats = src.getFeatures() + + # Get source CRS + if hasattr(src, "crs"): + src_crs = src.crs() + elif hasattr(src, "sourceCrs"): + src_crs = src.sourceCrs() + else: + src_crs = None + + # --- Prepare optional transform to DTM CRS for sampling --- + to_dtm = None + if dtm is not None and src_crs is not None and dtm.crs().isValid() and src_crs.isValid(): + if src_crs != dtm.crs(): + to_dtm = QgsCoordinateTransform(src_crs, dtm.crs(), QgsProject.instance()) + + # --- Helper: sample Z from DTM (returns float or -9999) --- + def sample_dtm_xy(x, y): + if dtm is None: + return 0.0 + # Transform coordinate if needed + if to_dtm is not None: + try: + from qgis.core import QgsPointXY + x, y = to_dtm.transform(QgsPointXY(x, y)) + except Exception: + return -9999.0 + from qgis.core import QgsPointXY + ident = dtm.dataProvider().identify(QgsPointXY(x, y), QgsRaster.IdentifyFormatValue) + if not ident.isValid(): + return -9999.0 + res = ident.results() + if not res: + return -9999.0 + # take first band value (band keys are 1-based) + try: + # Prefer band 1 if present + return float(res.get(1, next(iter(res.values())))) + except Exception: + return -9999.0 + + # --- Geometry -> list of vertices (QgsPoint or QgsPointXY) --- + def vertices_from_geometry(geom): + if geom is None or geom.isEmpty(): + return [] + gtype = QgsWkbTypes.geometryType(geom.wkbType()) + is_multi = QgsWkbTypes.isMultiType(geom.wkbType()) + + if gtype == QgsWkbTypes.PointGeometry: + if is_multi: + return list(geom.asMultiPoint()) + else: + return [geom.asPoint()] + + elif gtype == QgsWkbTypes.LineGeometry: + pts = [] + if is_multi: for line in geom.asMultiPolyline(): - points.extend(line) - # points = geom.asMultiPolyline()[0] - else: - if geom.type() == QgsWkbTypes.PointGeometry: - points = [geom.asPoint()] - elif geom.type() == QgsWkbTypes.LineGeometry: - points = geom.asPolyline() - - for p in points: - data['X'].append(p.x()) - data['Y'].append(p.y()) - if dtm is not None: - # Replace with your coordinates - - # Extract the value at the point - z_value = dtm.dataProvider().identify(p, QgsRaster.IdentifyFormatValue) - if z_value.isValid(): - z_value = z_value.results()[1] - else: - z_value = -9999 - data['Z'].append(z_value) - if dtm is None: - data['Z'].append(0) - for field in fields: - data[field.name()].append(feature[field.name()]) - return pd.DataFrame(data) + pts.extend(line) + else: + pts.extend(geom.asPolyline()) + return pts + + elif gtype == QgsWkbTypes.PolygonGeometry: + pts = [] + if is_multi: + mpoly = geom.asMultiPolygon() + for poly in mpoly: + for ring in poly: # exterior + interior rings + pts.extend(ring) + else: + poly = geom.asPolygon() + for ring in poly: + pts.extend(ring) + return pts + + # Other geometry types not handled + return [] + + # --- Build rows safely (one dict per sampled point) --- + rows = [] + field_names = [f.name() for f in fields] + + for f in feats: + geom = f.geometry() + pts = vertices_from_geometry(geom) + + if not pts: + # If you want to keep attribute rows even when no vertices: uncomment below + # row = {name: f[name] for name in field_names} + # row.update({"X": None, "Y": None, "Z": None}) + # rows.append(row) + continue + + # Cache attributes once per feature and reuse for each sampled point + base_attrs = {name: f[name] for name in field_names} + + for p in pts: + # QgsPoint vs QgsPointXY both have x()/y() + x, y = float(p.x()), float(p.y()) + z = sample_dtm_xy(x, y) + + row = {"X": x, "Y": y, "Z": z} + row.update(base_attrs) + rows.append(row) + + # Create DataFrame; if empty, return with expected columns + if not rows: + cols = ["X", "Y", "Z"] + field_names + return pd.DataFrame(columns=cols) + + return pd.DataFrame.from_records(rows) def GeoDataFrameToQgsLayer(qgs_algorithm, geodataframe, parameters, context, output_key, feedback=None): """ @@ -455,6 +601,98 @@ def dataframeToQgsLayer( feedback.setProgress(100) return sink, sink_id +def matrixToDict(matrix, headers=("minx", "miny", "maxx", "maxy")) -> dict: + """ + Convert a QgsProcessingParameterMatrix value to a dict with float values. + Accepts: [[minx,miny,maxx,maxy]] or [minx,miny,maxx,maxy]. + Raises a clear error if an enum index (int) was passed by mistake. + """ + # Guard: common mistake → using parameterAsEnum + if isinstance(matrix, int): + raise QgsProcessingException( + "Bounding Box was read with parameterAsEnum (got an int). " + "Use parameterAsMatrix for QgsProcessingParameterMatrix." + ) + + if matrix is None: + raise QgsProcessingException("Bounding box matrix is None.") + + # Allow empty string from settings/defaults + if isinstance(matrix, str) and not matrix.strip(): + raise QgsProcessingException("Bounding box matrix is empty.") + + # Accept single-row matrix or flat list + if isinstance(matrix, (list, tuple)): + if matrix and isinstance(matrix[0], (list, tuple)): + row = matrix[0] + else: + row = matrix + else: + # last resort: try comma-separated string "minx,miny,maxx,maxy" + if isinstance(matrix, str) and "," in matrix: + row = [v.strip() for v in matrix.split(",")] + else: + raise QgsProcessingException(f"Unrecognized bounding box value: {type(matrix)}") + + if len(row) < 4: + raise QgsProcessingException(f"Bounding box needs 4 numbers, got {len(row)}: {row}") + + def _to_float(v): + if isinstance(v, str): + v = v.strip() + return float(v) + + vals = list(map(_to_float, row[:4])) + bbox = dict(zip(headers, vals)) + + if not (bbox["minx"] < bbox["maxx"] and bbox["miny"] < bbox["maxy"]): + raise QgsProcessingException(f"Invalid bounding box: {bbox} (expect minx None: defaultValue="Observation projections", # Age-based is safest default ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/sorter_strati_column", "") self.addParameter( QgsProcessingParameterFeatureSource( diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 71f72eb..e10604d 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -25,10 +25,20 @@ QgsProcessingParameterEnum, QgsProcessingParameterNumber, QgsProcessingParameterField, - QgsProcessingParameterMatrix + QgsProcessingParameterMatrix, + QgsSettings, + QgsProcessingParameterRasterLayer, ) # Internal imports -from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame, GeoDataFrameToQgsLayer, qgsLayerToDataFrame, dataframeToQgsLayer +from ...main.vectorLayerWrapper import ( + qgsLayerToGeoDataFrame, + GeoDataFrameToQgsLayer, + qgsLayerToDataFrame, + dataframeToQgsLayer, + qgsRasterToGdalDataset, + matrixToDict, + dataframeToQgsTable + ) from map2loop.thickness_calculator import InterpolatedStructure, StructuralPoint @@ -39,11 +49,13 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_DTM = 'DTM' INPUT_BOUNDING_BOX = 'BOUNDING_BOX' INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' - INPUT_UNITS = 'UNITS' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' INPUT_BASAL_CONTACTS = 'BASAL_CONTACTS' INPUT_STRUCTURE_DATA = 'STRUCTURE_DATA' + INPUT_DIPDIR_FIELD = 'DIPDIR_FIELD' + INPUT_DIP_FIELD = 'DIP_FIELD' INPUT_GEOLOGY = 'GEOLOGY' + INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' OUTPUT = "THICKNESS" @@ -73,21 +85,27 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "Thickness Calculator Type", options=['InterpolatedStructure','StructuralPoint'], allowMultiple=False, + defaultValue='InterpolatedStructure' ) ) self.addParameter( - QgsProcessingParameterFeatureSource( + QgsProcessingParameterRasterLayer( self.INPUT_DTM, - "DTM", + "DTM (InterpolatedStructure)", [QgsProcessing.TypeRaster], + optional=True, ) ) + + bbox_settings = QgsSettings() + last_bbox = bbox_settings.value("m2l/bounding_box", "") self.addParameter( - QgsProcessingParameterEnum( + QgsProcessingParameterMatrix( self.INPUT_BOUNDING_BOX, - "Bounding Box", - options=['minx','miny','maxx','maxy'], - allowMultiple=True, + description="Bounding Box", + headers=['minx','miny','maxx','maxy'], + numberRows=1, + defaultValue=last_bbox ) ) self.addParameter( @@ -98,18 +116,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: defaultValue=1000 ) ) - self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_UNITS, - "Units", - [QgsProcessing.TypeVectorLine], - ) - ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_BASAL_CONTACTS, "Basal Contacts", [QgsProcessing.TypeVectorLine], + defaultValue='Basal Contacts', ) ) self.addParameter( @@ -119,29 +131,60 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: [QgsProcessing.TypeVectorPolygon], ) ) + + self.addParameter( + QgsProcessingParameterField( + 'UNIT_NAME_FIELD', + 'Unit Name Field e.g. Formation', + parentLayerParameterName=self.INPUT_GEOLOGY, + type=QgsProcessingParameterField.String, + defaultValue='Formation' + ) + ) + + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( QgsProcessingParameterMatrix( name=self.INPUT_STRATI_COLUMN, description="Stratigraphic Order", headers=["Unit"], numberRows=0, - defaultValue=[] + defaultValue=last_strati_column ) ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_SAMPLED_CONTACTS, - "SAMPLED_CONTACTS", + "Sampled Contacts", [QgsProcessing.TypeVectorPoint], ) ) self.addParameter( QgsProcessingParameterFeatureSource( self.INPUT_STRUCTURE_DATA, - "STRUCTURE_DATA", + "Orientation Data", [QgsProcessing.TypeVectorPoint], ) ) + self.addParameter( + QgsProcessingParameterField( + self.INPUT_DIPDIR_FIELD, + "Dip Direction Column", + parentLayerParameterName=self.INPUT_STRUCTURE_DATA, + type=QgsProcessingParameterField.Numeric, + defaultValue='DIPDIR' + ) + ) + self.addParameter( + QgsProcessingParameterField( + self.INPUT_DIP_FIELD, + "Dip Column", + parentLayerParameterName=self.INPUT_STRUCTURE_DATA, + type=QgsProcessingParameterField.Numeric, + defaultValue='DIP' + ) + ) self.addParameter( QgsProcessingParameterFeatureSink( self.OUTPUT, @@ -157,32 +200,63 @@ def processAlgorithm( ) -> dict[str, Any]: feedback.pushInfo("Initialising Thickness Calculation Algorithm...") - thickness_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) - dtm_data = self.parameterAsSource(parameters, self.INPUT_DTM, context) - bounding_box = self.parameterAsEnum(parameters, self.INPUT_BOUNDING_BOX, context) - max_line_length = self.parameterAsNumber(parameters, self.INPUT_MAX_LINE_LENGTH, context) - units = self.parameterAsSource(parameters, self.INPUT_UNITS, context) + thickness_type_index = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) + thickness_type = ['InterpolatedStructure', 'StructuralPoint'][thickness_type_index] + dtm_data = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) + bounding_box = self.parameterAsMatrix(parameters, self.INPUT_BOUNDING_BOX, context) + max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) + structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) + structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) + unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context) + bbox_settings = QgsSettings() + bbox_settings.setValue("m2l/bounding_box", bounding_box) + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', stratigraphic_order) # convert layers to dataframe or geodataframe + units = qgsLayerToDataFrame(geology_data) geology_data = qgsLayerToGeoDataFrame(geology_data) - units = qgsLayerToDataFrame(units) basal_contacts = qgsLayerToGeoDataFrame(basal_contacts) structure_data = qgsLayerToDataFrame(structure_data) + rename_map = {} + missing_fields = [] + if unit_name_field != 'UNITNAME' and unit_name_field in geology_data.columns: + geology_data = geology_data.rename(columns={unit_name_field: 'UNITNAME'}) + units = units.rename(columns={unit_name_field: 'UNITNAME'}) + units = units.drop_duplicates(subset=['UNITNAME']).reset_index(drop=True) + units = units.rename(columns={'UNITNAME': 'name'}) + if structure_data is not None: + if structure_dipdir_field: + if structure_dipdir_field in structure_data.columns: + rename_map[structure_dipdir_field] = 'DIPDIR' + else: + missing_fields.append(structure_dipdir_field) + if structure_dip_field: + if structure_dip_field in structure_data.columns: + rename_map[structure_dip_field] = 'DIP' + else: + missing_fields.append(structure_dip_field) + if missing_fields: + raise QgsProcessingException( + f"Orientation data missing required field(s): {', '.join(missing_fields)}" + ) + if rename_map: + structure_data = structure_data.rename(columns=rename_map) sampled_contacts = qgsLayerToDataFrame(sampled_contacts) - + dtm_data = qgsRasterToGdalDataset(dtm_data) + bounding_box = matrixToDict(bounding_box) feedback.pushInfo("Calculating unit thicknesses...") - if thickness_type == "InterpolatedStructure": thickness_calculator = InterpolatedStructure( dtm_data=dtm_data, bounding_box=bounding_box, ) - thickness_calculator.compute( + thicknesses = thickness_calculator.compute( units, stratigraphic_order, basal_contacts, @@ -197,7 +271,7 @@ def processAlgorithm( bounding_box=bounding_box, max_line_length=max_line_length, ) - thickness_calculator.compute( + thicknesses =thickness_calculator.compute( units, stratigraphic_order, basal_contacts, @@ -206,17 +280,22 @@ def processAlgorithm( sampled_contacts ) - #TODO: convert thicknesses dataframe to qgs layer - thicknesses = dataframeToQgsLayer( - self, - # contact_extractor.basal_contacts, + thicknesses = thicknesses[ + ["name","ThicknessMean","ThicknessMedian", "ThicknessStdDev"] + ].copy() + + feedback.pushInfo("Exporting Thickness Table...") + thicknesses = dataframeToQgsTable( + self, + thicknesses, parameters=parameters, context=context, feedback=feedback, - ) - + param_name=self.OUTPUT + ) + return {self.OUTPUT: thicknesses[1]} def createInstance(self) -> QgsProcessingAlgorithm: """Create a new instance of the algorithm.""" - return self.__class__() # BasalContactsAlgorithm() \ No newline at end of file + return self.__class__() # ThicknessCalculatorAlgorithm() diff --git a/tests/qgis/test_sampler_decimator.py b/tests/qgis/test_sampler_decimator.py index b12f56a..bb1864c 100644 --- a/tests/qgis/test_sampler_decimator.py +++ b/tests/qgis/test_sampler_decimator.py @@ -34,22 +34,27 @@ def test_decimator_1_with_structure(self): self.assertTrue(geology_layer.isValid(), "geology layer should be valid") self.assertTrue(structure_layer.isValid(), "structure layer should be valid") + self.assertTrue(dtm_layer.isValid(), "dtm layer should be valid") self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features") self.assertGreater(structure_layer.featureCount(), 0, "structure layer should have features") QgsMessageLog.logMessage(f"geology layer valid: {geology_layer.isValid()}", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"structure layer valid: {structure_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm layer valid: {dtm_layer.isValid()}", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm source: {dtm_layer.source()}", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"structure layer: {structure_layer.featureCount()} features", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"spatial data- structure layer", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"sampler type: Decimator", "TestDecimator", Qgis.Critical) QgsMessageLog.logMessage(f"decimation: 1", "TestDecimator", Qgis.Critical) + QgsMessageLog.logMessage(f"dtm: {self.dtm_file.name}", "TestDecimator", Qgis.Critical) algorithm = SamplerAlgorithm() algorithm.initAlgorithm() parameters = { + 'DTM': dtm_layer, 'GEOLOGY': geology_layer, 'SPATIAL_DATA': structure_layer, 'SAMPLER_TYPE': 0, @@ -87,7 +92,11 @@ def test_decimator_1_with_structure(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() diff --git a/tests/qgis/test_sampler_spacing.py b/tests/qgis/test_sampler_spacing.py index a542b85..a49042f 100644 --- a/tests/qgis/test_sampler_spacing.py +++ b/tests/qgis/test_sampler_spacing.py @@ -74,7 +74,11 @@ def test_spacing_50_with_geology(self): @classmethod def tearDownClass(cls): - QgsApplication.processingRegistry().removeProvider(cls.provider) + try: + registry = QgsApplication.processingRegistry() + registry.removeProvider(cls.provider) + except Exception: + pass if __name__ == '__main__': unittest.main() From 17cd45b4efdca18df0da25048b43b64ff1ad8d64 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:46:30 +0930 Subject: [PATCH 095/116] feat: add UserDefinedStratigraphyAlgorithm to provider --- m2l/processing/provider.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index 318e944..9616d9b 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -19,6 +19,7 @@ from .algorithms import ( BasalContactsAlgorithm, StratigraphySorterAlgorithm, + UserDefinedStratigraphyAlgorithm, ThicknessCalculatorAlgorithm, SamplerAlgorithm ) @@ -35,6 +36,7 @@ def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) self.addAlgorithm(StratigraphySorterAlgorithm()) + self.addAlgorithm(UserDefinedStratigraphyAlgorithm()) self.addAlgorithm(ThicknessCalculatorAlgorithm()) self.addAlgorithm(SamplerAlgorithm()) From 439588045b7e7e3fd62769bb8b4d8102d69108a2 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:46:46 +0930 Subject: [PATCH 096/116] feat: import UserDefinedStratigraphyAlgorithm --- m2l/processing/algorithms/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index f0aaedb..08d76a0 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1,4 +1,5 @@ from .extract_basal_contacts import BasalContactsAlgorithm from .sorter import StratigraphySorterAlgorithm +from .user_defined_sorter import UserDefinedStratigraphyAlgorithm from .thickness_calculator import ThicknessCalculatorAlgorithm from .sampler import SamplerAlgorithm From 2d4eff65b83c200ccd7cb8ebf9a345086a9e524f Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:47:09 +0930 Subject: [PATCH 097/116] refactor: update BasalContactsAlgorithm --- .../algorithms/extract_basal_contacts.py | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 5903d82..4d3ba5f 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,9 +22,11 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterMapLayer, QgsProcessingParameterString, QgsProcessingParameterField, QgsProcessingParameterMatrix, + QgsVectorLayer, QgsSettings ) # Internal imports @@ -49,15 +51,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Basal Contacts" + return "Basal Contacts" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Contact Extractors" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Contact_Extractors" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -98,15 +100,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "Stratigraphic Order", + [QgsProcessing.TypeVector], + defaultValue='formation', ) ) ignore_settings = QgsSettings() @@ -145,34 +144,33 @@ def processAlgorithm( feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) - if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): - raise QgsProcessingException("no stratigraphic column found") + + if isinstance(strati_column, QgsProcessingParameterMapLayer) : + raise QgsProcessingException("Invalid stratigraphic column layer") + + elif strati_column is not None: + # extract unit names from strati_column + field_name = "unit_name" + strati_order = [f[field_name] for f in strati_column.getFeatures()] if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): feedback.pushInfo("no units to ignore specified") - - # if strati_column and strati_column.strip(): - # strati_column = [unit.strip() for unit in strati_column.split(',')] - # Save stratigraphic column settings - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', strati_column) ignore_settings = QgsSettings() ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) - formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - if formation_field and formation_field in geology.columns: - mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + if unit_name_field and unit_name_field in geology.columns: + mask = ~geology[unit_name_field].astype(str).str.strip().isin(ignore_units) geology = geology[mask].reset_index(drop=True) - feedback.pushInfo(f"filtered by formation field: {formation_field}") + feedback.pushInfo(f"filtered by unit name field: {unit_name_field}") else: - feedback.pushInfo(f"no formation field found: {formation_field}") + feedback.pushInfo(f"no unit name field found: {unit_name_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -181,7 +179,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) all_contacts = contact_extractor.extract_all_contacts() - basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + basal_contacts = contact_extractor.extract_basal_contacts(strati_order) feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( From 09034f1d47fdd596eced678e3c4e0632eff75a31 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:47:21 +0930 Subject: [PATCH 098/116] refactor: update SamplerAlgorithm --- m2l/processing/algorithms/sampler.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 726d44a..c2b0e35 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -10,7 +10,7 @@ """ # Python imports from typing import Any, Optional -from qgis.PyQt.QtCore import QMetaType +from qgis.PyQt.QtCore import QMetaType, QVariant from osgeo import gdal import pandas as pd @@ -59,15 +59,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Sampler" + return "Spacing-Decimator Samplers" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Samplers" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Samplers" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -176,11 +176,11 @@ def processAlgorithm( samples = sampler.sample(spatial_data_gdf) fields = QgsFields() - fields.append(QgsField("ID", QMetaType.Type.QString)) - fields.append(QgsField("X", QMetaType.Type.Float)) - fields.append(QgsField("Y", QMetaType.Type.Float)) - fields.append(QgsField("Z", QMetaType.Type.Float)) - fields.append(QgsField("featureId", QMetaType.Type.QString)) + fields.append(QgsField("ID", QVariant.String)) + fields.append(QgsField("X", QVariant.Double)) + fields.append(QgsField("Y", QVariant.Double)) + fields.append(QgsField("Z", QVariant.Double)) + fields.append(QgsField("featureId", QVariant.String)) crs = None if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: From 932b95cf2654b02a44adc383960ac027253747a8 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:48:04 +0930 Subject: [PATCH 099/116] refactor: update sorter --- m2l/processing/algorithms/sorter.py | 92 +++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index f80cfe0..e3772f9 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -24,6 +24,7 @@ QgsProcessingParameterField, QgsProcessingParameterRasterLayer, QgsProcessingParameterMatrix, + QgsCoordinateReferenceSystem, QgsVectorLayer, QgsWkbTypes, QgsSettings @@ -73,13 +74,13 @@ def name(self) -> str: return "loop_sorter" def displayName(self) -> str: - return "Loop3d: Stratigraphic sorter" + return "Automatic Stratigraphic Column" def group(self) -> str: - return "Loop3d" + return "Stratigraphy" def groupId(self) -> str: - return "Loop3d" + return "Stratigraphy_Column" def updateParameters(self, parameters): selected_method = parameters.get(self.METHOD, 0) @@ -341,6 +342,89 @@ def createInstance(self) -> QgsProcessingAlgorithm: return StratigraphySorterAlgorithm() +class UserDefinedStratigraphyAlgorithm(QgsProcessingAlgorithm): + """ + Creates a one-column ‘stratigraphic column’ table ordered + by the selected map2loop sorter. + """ + INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" + OUTPUT = "OUTPUT" + + # ---------------------------------------------------------- + # Metadata + # ---------------------------------------------------------- + def name(self) -> str: + return "loop_sorter" + + def displayName(self) -> str: + return "Stratigraphy: User-Defined Stratigraphic Column" + + def group(self) -> str: + return "Stratigraphy" + + def groupId(self) -> str: + return "Stratigraphy_Column" + + # ---------------------------------------------------------- + # Parameters + # ---------------------------------------------------------- + def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: + + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Stratigraphic column", + ) + ) + + # ---------------------------------------------------------- + # Core + # ---------------------------------------------------------- + def processAlgorithm( + self, + parameters: dict[str, Any], + context: QgsProcessingContext, + feedback: QgsProcessingFeedback, + ) -> dict[str, Any]: + + strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + + # 4 ► write an in-memory table with the result + sink_fields = QgsFields() + sink_fields.append(QgsField("order", QVariant.Int)) + sink_fields.append(QgsField("unit_name", QVariant.String)) + crs = context.project().crs() if context and context.project() else QgsCoordinateReferenceSystem() + + (sink, dest_id) = self.parameterAsSink( + parameters, + self.OUTPUT, + context, + sink_fields, + QgsWkbTypes.NoGeometry, + crs, + ) + + + return {self.OUTPUT: dest_id} + + # ---------------------------------------------------------- + def createInstance(self) -> QgsProcessingAlgorithm: + return __class__() + # ------------------------------------------------------------------------- # Helper stub – you must replace with *your* conversion logic # ------------------------------------------------------------------------- @@ -415,4 +499,4 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee else: relationships_df = pd.DataFrame() - return units_df, relationships_df, contacts_df \ No newline at end of file + return units_df, relationships_df, contacts_df From 476eadba13a1a8c03cb97e3a98696a8f745e037b Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:48:18 +0930 Subject: [PATCH 100/116] refactor: update ThicknessCalculatorAlgorithm --- .../algorithms/thickness_calculator.py | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index e10604d..53d7624 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -28,6 +28,7 @@ QgsProcessingParameterMatrix, QgsSettings, QgsProcessingParameterRasterLayer, + QgsProcessingParameterMapLayer ) # Internal imports from ...main.vectorLayerWrapper import ( @@ -66,15 +67,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Thickness Calculator" + return "Thickness Calculator" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Thickness Calculators" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Thickness_Calculators" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -142,15 +143,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "Stratigraphic Order", + [QgsProcessing.TypeVector], + defaultValue='formation', ) ) self.addParameter( @@ -207,7 +205,7 @@ def processAlgorithm( max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) - stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + stratigraphic_order = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) @@ -216,8 +214,14 @@ def processAlgorithm( bbox_settings = QgsSettings() bbox_settings.setValue("m2l/bounding_box", bounding_box) - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', stratigraphic_order) + + if isinstance(stratigraphic_order, QgsProcessingParameterMapLayer) : + raise QgsProcessingException("Invalid stratigraphic column layer") + + if stratigraphic_order is not None: + # extract unit names from stratigraphic_order + field_name = "unit_name" + stratigraphic_order = [f[field_name] for f in stratigraphic_order.getFeatures()] # convert layers to dataframe or geodataframe units = qgsLayerToDataFrame(geology_data) geology_data = qgsLayerToGeoDataFrame(geology_data) From 9e890d57d758232b0375fe2055291eba7119db89 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:48:29 +0930 Subject: [PATCH 101/116] feat: implement UserDefinedStratigraphyAlgorithm --- .../algorithms/user_defined_sorter.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 m2l/processing/algorithms/user_defined_sorter.py diff --git a/m2l/processing/algorithms/user_defined_sorter.py b/m2l/processing/algorithms/user_defined_sorter.py new file mode 100644 index 0000000..23857a3 --- /dev/null +++ b/m2l/processing/algorithms/user_defined_sorter.py @@ -0,0 +1,112 @@ +from typing import Any, Optional +from osgeo import gdal +import numpy as np +import json + +from PyQt5.QtCore import QVariant +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsFields, + QgsField, + QgsFeature, + QgsGeometry, + QgsRasterLayer, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterEnum, + QgsProcessingParameterFileDestination, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterField, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterMatrix, + QgsCoordinateReferenceSystem, + QgsVectorLayer, + QgsWkbTypes, + QgsSettings +) + + +from qgis.core import ( + QgsFields, QgsField, QgsFeature, QgsFeatureSink, QgsWkbTypes, + QgsCoordinateReferenceSystem, QgsProcessingAlgorithm, QgsProcessingContext, + QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterMatrix, + QgsSettings +) +from PyQt5.QtCore import QVariant +import numpy as np + +class UserDefinedStratigraphyAlgorithm(QgsProcessingAlgorithm): + INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" + OUTPUT = "OUTPUT" + + def name(self): return "loop_sorter_2" + def displayName(self): return "User-Defined Stratigraphic Column" + def group(self): return "Stratigraphy" + def groupId(self): return "Stratigraphy_Column" + + def initAlgorithm(self, config=None): + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Stratigraphic column", + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + # 1) Read the matrix; it may be a list of lists (rows) or a flat list depending on input source. + matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + + # Normalize to a list of unit strings (one column: "Unit") + units = [] + for row in matrix: + if isinstance(row, (list, tuple)): + unit = row[0] if row else "" + else: + unit = row + unit = (unit or "").strip() + if unit: # skip empty rows to avoid writing "" into fields + units.append(unit) + + # 2) Build sequential order (1-based), cast to native int + order_vals = [int(i) for i in (np.arange(len(units)) + 1)] + + # 3) Prepare sink + sink_fields = QgsFields() + sink_fields.append(QgsField("order", QVariant.Int)) # or QVariant.LongLong + sink_fields.append(QgsField("unit_name", QVariant.String)) + + crs = context.project().crs() if context and context.project() else QgsCoordinateReferenceSystem() + sink, dest_id = self.parameterAsSink( + parameters, self.OUTPUT, context, + sink_fields, QgsWkbTypes.NoGeometry, crs + ) + + # 4) Insert features + for pos, unit_name in zip(order_vals, units): + f = QgsFeature(sink_fields) + # Ensure correct types: int for "order", str for "unit_name" + f.setAttributes([int(pos), str(unit_name)]) + ok = sink.addFeature(f, QgsFeatureSink.FastInsert) + if not ok: + feedback.reportError(f"Failed to add feature for unit '{unit_name}' (order={pos}).") + + return {self.OUTPUT: dest_id} + + def createInstance(self): + return __class__() From 3b61ed6f071af71091fea5d6a7b4024d4570b1b4 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:51:56 +0930 Subject: [PATCH 102/116] refactor: remove redundant algo --- m2l/processing/algorithms/sorter.py | 84 ----------------------------- 1 file changed, 84 deletions(-) diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index e3772f9..55a179c 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -341,90 +341,6 @@ def processAlgorithm( def createInstance(self) -> QgsProcessingAlgorithm: return StratigraphySorterAlgorithm() - -class UserDefinedStratigraphyAlgorithm(QgsProcessingAlgorithm): - """ - Creates a one-column ‘stratigraphic column’ table ordered - by the selected map2loop sorter. - """ - INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" - OUTPUT = "OUTPUT" - - # ---------------------------------------------------------- - # Metadata - # ---------------------------------------------------------- - def name(self) -> str: - return "loop_sorter" - - def displayName(self) -> str: - return "Stratigraphy: User-Defined Stratigraphic Column" - - def group(self) -> str: - return "Stratigraphy" - - def groupId(self) -> str: - return "Stratigraphy_Column" - - # ---------------------------------------------------------- - # Parameters - # ---------------------------------------------------------- - def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: - - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") - self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column - ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - self.OUTPUT, - "Stratigraphic column", - ) - ) - - # ---------------------------------------------------------- - # Core - # ---------------------------------------------------------- - def processAlgorithm( - self, - parameters: dict[str, Any], - context: QgsProcessingContext, - feedback: QgsProcessingFeedback, - ) -> dict[str, Any]: - - strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") - - # 4 ► write an in-memory table with the result - sink_fields = QgsFields() - sink_fields.append(QgsField("order", QVariant.Int)) - sink_fields.append(QgsField("unit_name", QVariant.String)) - crs = context.project().crs() if context and context.project() else QgsCoordinateReferenceSystem() - - (sink, dest_id) = self.parameterAsSink( - parameters, - self.OUTPUT, - context, - sink_fields, - QgsWkbTypes.NoGeometry, - crs, - ) - - - return {self.OUTPUT: dest_id} - - # ---------------------------------------------------------- - def createInstance(self) -> QgsProcessingAlgorithm: - return __class__() - # ------------------------------------------------------------------------- # Helper stub – you must replace with *your* conversion logic # ------------------------------------------------------------------------- From 248040d22a76436b0ee0a4ffd173296ca718e425 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:06:21 +0930 Subject: [PATCH 103/116] fix: use correct strati table --- tests/qgis/test_basal_contacts.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index c6f9256..f7565cf 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -1,6 +1,7 @@ import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication, QgsFeature, QgsField +from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider @@ -61,7 +62,23 @@ def test_basal_contacts_extraction(self): "Rocklea Inlier greenstones", "Rocklea Inlier metagranitic unit" ] + strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") + # define the single field + provider = strati_table.dataProvider() + provider.addAttributes([QgsField("unit_name", vtype)]) + strati_table.updateFields() + vtype=QVariant.String + # add features (one row per value) + feats = [] + fields = strati_table.fields() + for val in strati_column: + f = QgsFeature(fields) + f.setAttributes([val]) + feats.append(f) + if feats: + provider.addFeatures(feats) + strati_table.updateExtents() algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() @@ -70,7 +87,7 @@ def test_basal_contacts_extraction(self): 'UNIT_NAME_FIELD': 'unitname', 'FORMATION_FIELD': 'formation', 'FAULTS': faults_layer, - 'STRATIGRAPHIC_COLUMN': strati_column, + 'STRATIGRAPHIC_COLUMN': strati_table, 'IGNORE_UNITS': [], 'BASAL_CONTACTS': 'memory:basal_contacts', 'ALL_CONTACTS': 'memory:all_contacts' From 9cacb9bca59df8254304eb696fc0603c7ed5cad9 Mon Sep 17 00:00:00 2001 From: Rabii Chaarani <50892556+rabii-chaarani@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:43:13 +0930 Subject: [PATCH 104/116] fix: correct variable assignment --- tests/qgis/test_basal_contacts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index f7565cf..e49ba85 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -65,9 +65,10 @@ def test_basal_contacts_extraction(self): strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") # define the single field provider = strati_table.dataProvider() + vtype=QVariant.String provider.addAttributes([QgsField("unit_name", vtype)]) strati_table.updateFields() - vtype=QVariant.String + # add features (one row per value) feats = [] fields = strati_table.fields() From f59f4f8d19fadc506de5a6e23de01dbba1813fe1 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 12:42:28 +0800 Subject: [PATCH 105/116] fix units in ThicknessCalculatorAlgorithm --- m2l/processing/algorithms/thickness_calculator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 0210f68..7bab4f4 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -10,6 +10,7 @@ """ # Python imports from typing import Any, Optional +import pandas as pd # QGIS imports from qgis import processing @@ -260,9 +261,8 @@ def processAlgorithm( missing_fields = [] if unit_name_field != 'UNITNAME' and unit_name_field in geology_data.columns: geology_data = geology_data.rename(columns={unit_name_field: 'UNITNAME'}) - units = units.rename(columns={unit_name_field: 'UNITNAME'}) - units = units.drop_duplicates(subset=['UNITNAME']).reset_index(drop=True) - units = units.rename(columns={'UNITNAME': 'name'}) + units_unique = units.drop_duplicates(subset=[unit_name_field]).reset_index(drop=True) + units = pd.DataFrame({'name': units_unique[unit_name_field]}) if structure_data is not None: if structure_dipdir_field: if structure_dipdir_field in structure_data.columns: From b1bb23fd3c2e8e966ba8160cb3b07ac280c92d32 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 13:13:38 +0800 Subject: [PATCH 106/116] update extract_basal_contacts and test_basal_contacts --- .../algorithms/extract_basal_contacts.py | 50 ++++++++++--------- tests/qgis/test_basal_contacts.py | 22 +------- 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 4d3ba5f..5903d82 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,11 +22,9 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, - QgsProcessingParameterMapLayer, QgsProcessingParameterString, QgsProcessingParameterField, QgsProcessingParameterMatrix, - QgsVectorLayer, QgsSettings ) # Internal imports @@ -51,15 +49,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Basal Contacts" + return "Loop3d: Basal Contacts" def group(self) -> str: """Return the algorithm group name.""" - return "Contact Extractors" + return "Loop3d" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Contact_Extractors" + return "Loop3d" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -100,12 +98,15 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterFeatureSource( - self.INPUT_STRATI_COLUMN, - "Stratigraphic Order", - [QgsProcessing.TypeVector], - defaultValue='formation', + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column ) ) ignore_settings = QgsSettings() @@ -144,33 +145,34 @@ def processAlgorithm( feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) - - if isinstance(strati_column, QgsProcessingParameterMapLayer) : - raise QgsProcessingException("Invalid stratigraphic column layer") - - elif strati_column is not None: - # extract unit names from strati_column - field_name = "unit_name" - strati_order = [f[field_name] for f in strati_column.getFeatures()] + if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): + raise QgsProcessingException("no stratigraphic column found") if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): feedback.pushInfo("no units to ignore specified") + + # if strati_column and strati_column.strip(): + # strati_column = [unit.strip() for unit in strati_column.split(',')] + # Save stratigraphic column settings + strati_column_settings = QgsSettings() + strati_column_settings.setValue('m2l/strati_column', strati_column) ignore_settings = QgsSettings() ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) + formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - if unit_name_field and unit_name_field in geology.columns: - mask = ~geology[unit_name_field].astype(str).str.strip().isin(ignore_units) + if formation_field and formation_field in geology.columns: + mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) geology = geology[mask].reset_index(drop=True) - feedback.pushInfo(f"filtered by unit name field: {unit_name_field}") + feedback.pushInfo(f"filtered by formation field: {formation_field}") else: - feedback.pushInfo(f"no unit name field found: {unit_name_field}") + feedback.pushInfo(f"no formation field found: {formation_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -179,7 +181,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) all_contacts = contact_extractor.extract_all_contacts() - basal_contacts = contact_extractor.extract_basal_contacts(strati_order) + basal_contacts = contact_extractor.extract_basal_contacts(strati_column) feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index e49ba85..c6f9256 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -1,7 +1,6 @@ import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication, QgsFeature, QgsField -from qgis.PyQt.QtCore import QVariant +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication from qgis.testing import start_app from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider @@ -62,24 +61,7 @@ def test_basal_contacts_extraction(self): "Rocklea Inlier greenstones", "Rocklea Inlier metagranitic unit" ] - strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") - # define the single field - provider = strati_table.dataProvider() - vtype=QVariant.String - provider.addAttributes([QgsField("unit_name", vtype)]) - strati_table.updateFields() - - # add features (one row per value) - feats = [] - fields = strati_table.fields() - for val in strati_column: - f = QgsFeature(fields) - f.setAttributes([val]) - feats.append(f) - if feats: - provider.addFeatures(feats) - strati_table.updateExtents() algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() @@ -88,7 +70,7 @@ def test_basal_contacts_extraction(self): 'UNIT_NAME_FIELD': 'unitname', 'FORMATION_FIELD': 'formation', 'FAULTS': faults_layer, - 'STRATIGRAPHIC_COLUMN': strati_table, + 'STRATIGRAPHIC_COLUMN': strati_column, 'IGNORE_UNITS': [], 'BASAL_CONTACTS': 'memory:basal_contacts', 'ALL_CONTACTS': 'memory:all_contacts' From ec389b37b34ea8bbc5490be52d4145e2152919e9 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 14:21:21 +0800 Subject: [PATCH 107/116] Revert "update extract_basal_contacts and test_basal_contacts" This reverts commit b1bb23fd3c2e8e966ba8160cb3b07ac280c92d32. --- .../algorithms/extract_basal_contacts.py | 50 +++++++++---------- tests/qgis/test_basal_contacts.py | 22 +++++++- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 5903d82..4d3ba5f 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,9 +22,11 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterMapLayer, QgsProcessingParameterString, QgsProcessingParameterField, QgsProcessingParameterMatrix, + QgsVectorLayer, QgsSettings ) # Internal imports @@ -49,15 +51,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Basal Contacts" + return "Basal Contacts" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Contact Extractors" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Contact_Extractors" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -98,15 +100,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "Stratigraphic Order", + [QgsProcessing.TypeVector], + defaultValue='formation', ) ) ignore_settings = QgsSettings() @@ -145,34 +144,33 @@ def processAlgorithm( feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) - if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): - raise QgsProcessingException("no stratigraphic column found") + + if isinstance(strati_column, QgsProcessingParameterMapLayer) : + raise QgsProcessingException("Invalid stratigraphic column layer") + + elif strati_column is not None: + # extract unit names from strati_column + field_name = "unit_name" + strati_order = [f[field_name] for f in strati_column.getFeatures()] if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): feedback.pushInfo("no units to ignore specified") - - # if strati_column and strati_column.strip(): - # strati_column = [unit.strip() for unit in strati_column.split(',')] - # Save stratigraphic column settings - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', strati_column) ignore_settings = QgsSettings() ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) - formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - if formation_field and formation_field in geology.columns: - mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + if unit_name_field and unit_name_field in geology.columns: + mask = ~geology[unit_name_field].astype(str).str.strip().isin(ignore_units) geology = geology[mask].reset_index(drop=True) - feedback.pushInfo(f"filtered by formation field: {formation_field}") + feedback.pushInfo(f"filtered by unit name field: {unit_name_field}") else: - feedback.pushInfo(f"no formation field found: {formation_field}") + feedback.pushInfo(f"no unit name field found: {unit_name_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -181,7 +179,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) all_contacts = contact_extractor.extract_all_contacts() - basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + basal_contacts = contact_extractor.extract_basal_contacts(strati_order) feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index c6f9256..e49ba85 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -1,6 +1,7 @@ import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication, QgsFeature, QgsField +from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider @@ -61,7 +62,24 @@ def test_basal_contacts_extraction(self): "Rocklea Inlier greenstones", "Rocklea Inlier metagranitic unit" ] + strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") + # define the single field + provider = strati_table.dataProvider() + vtype=QVariant.String + provider.addAttributes([QgsField("unit_name", vtype)]) + strati_table.updateFields() + + # add features (one row per value) + feats = [] + fields = strati_table.fields() + for val in strati_column: + f = QgsFeature(fields) + f.setAttributes([val]) + feats.append(f) + if feats: + provider.addFeatures(feats) + strati_table.updateExtents() algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() @@ -70,7 +88,7 @@ def test_basal_contacts_extraction(self): 'UNIT_NAME_FIELD': 'unitname', 'FORMATION_FIELD': 'formation', 'FAULTS': faults_layer, - 'STRATIGRAPHIC_COLUMN': strati_column, + 'STRATIGRAPHIC_COLUMN': strati_table, 'IGNORE_UNITS': [], 'BASAL_CONTACTS': 'memory:basal_contacts', 'ALL_CONTACTS': 'memory:all_contacts' From e6fa63c9b66d8363bf02496a61d80d9123fbde65 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 15:14:59 +0800 Subject: [PATCH 108/116] fix test_basal_contacts --- .../input/stratigraphic_column_testing.gpkg | Bin 0 -> 73728 bytes tests/qgis/test_basal_contacts.py | 41 ++---------------- 2 files changed, 3 insertions(+), 38 deletions(-) create mode 100644 tests/qgis/input/stratigraphic_column_testing.gpkg diff --git a/tests/qgis/input/stratigraphic_column_testing.gpkg b/tests/qgis/input/stratigraphic_column_testing.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..bb782ef5b2f52fba0d364aa506a7e5bdf571a79c GIT binary patch literal 73728 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 b00bZa0SG_<0uVS00+X)(fXnvO#pM41;`-|; literal 0 HcmV?d00001 diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index e49ba85..2155444 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -21,9 +21,10 @@ def setUp(self): self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" self.faults_file = self.input_dir / "faults_clip.shp" + self.strati_file = self.input_dir / "stratigraphic_column.gpkg" self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") - + self.assertTrue(self.strati_file.exists(), f"strati not found: {self.strati_file}") if not self.faults_file.exists(): QgsMessageLog.logMessage(f"faults not found: {self.faults_file}, will run test without faults", "TestBasalContacts", Qgis.Warning) @@ -43,43 +44,7 @@ def test_basal_contacts_extraction(self): QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) - strati_column = [ - "Turee Creek Group", - "Boolgeeda Iron Formation", - "Woongarra Rhyolite", - "Weeli Wolli Formation", - "Brockman Iron Formation", - "Mount McRae Shale and Mount Sylvia Formation", - "Wittenoom Formation", - "Marra Mamba Iron Formation", - "Jeerinah Formation", - "Bunjinah Formation", - "Pyradie Formation", - "Fortescue Group", - "Hardey Formation", - "Boongal Formation", - "Mount Roe Basalt", - "Rocklea Inlier greenstones", - "Rocklea Inlier metagranitic unit" - ] - strati_table = QgsVectorLayer(faults_layer.crs().authid(), "strati_column", "memory") - # define the single field - provider = strati_table.dataProvider() - vtype=QVariant.String - provider.addAttributes([QgsField("unit_name", vtype)]) - strati_table.updateFields() - - # add features (one row per value) - feats = [] - fields = strati_table.fields() - for val in strati_column: - f = QgsFeature(fields) - f.setAttributes([val]) - feats.append(f) - - if feats: - provider.addFeatures(feats) - strati_table.updateExtents() + strati_table = QgsVectorLayer(str(self.strati_file), "strati", "ogr") algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() From d4d6d8358c6f416ec6f9e085f31134ef6d102ee7 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 24 Sep 2025 15:23:36 +0800 Subject: [PATCH 109/116] fix input file name in test_basal_contacts --- tests/qgis/test_basal_contacts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index 2155444..bb90de8 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -21,7 +21,7 @@ def setUp(self): self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" self.faults_file = self.input_dir / "faults_clip.shp" - self.strati_file = self.input_dir / "stratigraphic_column.gpkg" + self.strati_file = self.input_dir / "stratigraphic_column_testing.gpkg" self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") self.assertTrue(self.strati_file.exists(), f"strati not found: {self.strati_file}") From 2cd4c536f392f8bb6cb988e465ff53f39fa616bc Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 6 Oct 2025 14:06:04 +0800 Subject: [PATCH 110/116] feat dynamic field handling in Sampler --- m2l/processing/algorithms/sampler.py | 40 +++++++++++++++++++--------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 726d44a..cafd4b2 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -176,11 +176,19 @@ def processAlgorithm( samples = sampler.sample(spatial_data_gdf) fields = QgsFields() - fields.append(QgsField("ID", QMetaType.Type.QString)) - fields.append(QgsField("X", QMetaType.Type.Float)) - fields.append(QgsField("Y", QMetaType.Type.Float)) - fields.append(QgsField("Z", QMetaType.Type.Float)) - fields.append(QgsField("featureId", QMetaType.Type.QString)) + if samples is not None and not samples.empty: + for column_name in samples.columns: + dtype = samples[column_name].dtype + dtype_str = str(dtype) + + if dtype_str in ['float16', 'float32', 'float64']: + field_type = QMetaType.Type.Double + elif dtype_str in ['int8', 'int16', 'int32', 'int64']: + field_type = QMetaType.Type.Int + else: + field_type = QMetaType.Type.QString + + fields.append(QgsField(column_name, field_type)) crs = None if spatial_data_gdf is not None and spatial_data_gdf.crs is not None: @@ -207,13 +215,21 @@ def processAlgorithm( #spacing has no z values feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y']))) - feature.setAttributes([ - str(row.get('ID', '')), - float(row.get('X', 0)), - float(row.get('Y', 0)), - float(row.get('Z', 0)) if pd.notna(row.get('Z')) else 0.0, - str(row.get('featureId', '')) - ]) + attributes = [] + for column_name in samples.columns: + value = row.get(column_name) + dtype = samples[column_name].dtype + + if pd.isna(value): + attributes.append(None) + elif dtype in ['float16', 'float32', 'float64']: + attributes.append(float(value)) + elif dtype in ['int8', 'int16', 'int32', 'int64']: + attributes.append(int(value)) + else: + attributes.append(str(value)) + + feature.setAttributes(attributes) sink.addFeature(feature) From 97c527446c1813599fa7873de4748b93d6eac539 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Mon, 6 Oct 2025 15:17:12 +0800 Subject: [PATCH 111/116] fix strat column data in ThicknessCalculator --- .../algorithms/thickness_calculator.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 7bab4f4..51d6f91 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -233,19 +233,22 @@ def processAlgorithm( 'maxx': extent.xMaximum(), 'maxy': extent.yMaximum() } - stratigraphic_column_layer = self.parameterAsVectorLayer(parameters, self.INPUT_STRATIGRAPHIC_COLUMN_LAYER, context) - stratigraphic_order = None - if stratigraphic_column_layer is not None and stratigraphic_column_layer.isValid(): - stratigraphic_order=[] - stratigraphic_order_df = qgsLayerToDataFrame(stratigraphic_column_layer) - stratigraphic_order_df = stratigraphic_order_df.sort_values('order') - for _, row in stratigraphic_order_df.iterrows(): - stratigraphic_order.append(row['unit_name']) + stratigraphic_column_source = self.parameterAsSource(parameters, self.INPUT_STRATIGRAPHIC_COLUMN_LAYER, context) + stratigraphic_order = [] + if stratigraphic_column_source is not None: + ordered_pairs =[] + for feature in stratigraphic_column_source.getFeatures(): + order = feature.attribute('order') + unit_name = feature.attribute('unit_name') + ordered_pairs.append((order, unit_name)) + ordered_pairs.sort(key=lambda x: x[0]) + stratigraphic_order = [pair[1] for pair in ordered_pairs] + feedback.pushInfo(f"DEBUG: parameterAsVectorLayer Stratigraphic order: {stratigraphic_order}") else: matrix_stratigraphic_order = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) if matrix_stratigraphic_order: - stratigraphic_order = [row[0] for row in matrix_stratigraphic_order if row and len(row) > 0] + stratigraphic_order = [str(row) for row in matrix_stratigraphic_order if row] else: raise QgsProcessingException("Stratigraphic column layer is required") if stratigraphic_order: From fe429df9cde758877301b8308afbf737204feafc Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Tue, 7 Oct 2025 11:08:39 +0800 Subject: [PATCH 112/116] merge with processing/processing_tools --- m2l/processing/algorithms/__init__.py | 1 + .../algorithms/extract_basal_contacts.py | 50 ++++---- m2l/processing/algorithms/sorter.py | 10 +- .../algorithms/user_defined_sorter.py | 112 ++++++++++++++++++ m2l/processing/provider.py | 2 + .../input/stratigraphic_column_testing.gpkg | Bin 0 -> 73728 bytes tests/qgis/test_basal_contacts.py | 29 +---- 7 files changed, 150 insertions(+), 54 deletions(-) create mode 100644 m2l/processing/algorithms/user_defined_sorter.py create mode 100644 tests/qgis/input/stratigraphic_column_testing.gpkg diff --git a/m2l/processing/algorithms/__init__.py b/m2l/processing/algorithms/__init__.py index f0aaedb..08d76a0 100644 --- a/m2l/processing/algorithms/__init__.py +++ b/m2l/processing/algorithms/__init__.py @@ -1,4 +1,5 @@ from .extract_basal_contacts import BasalContactsAlgorithm from .sorter import StratigraphySorterAlgorithm +from .user_defined_sorter import UserDefinedStratigraphyAlgorithm from .thickness_calculator import ThicknessCalculatorAlgorithm from .sampler import SamplerAlgorithm diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index 5903d82..4d3ba5f 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -22,9 +22,11 @@ QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterFeatureSource, + QgsProcessingParameterMapLayer, QgsProcessingParameterString, QgsProcessingParameterField, QgsProcessingParameterMatrix, + QgsVectorLayer, QgsSettings ) # Internal imports @@ -49,15 +51,15 @@ def name(self) -> str: def displayName(self) -> str: """Return the algorithm display name.""" - return "Loop3d: Basal Contacts" + return "Basal Contacts" def group(self) -> str: """Return the algorithm group name.""" - return "Loop3d" + return "Contact Extractors" def groupId(self) -> str: """Return the algorithm group ID.""" - return "Loop3d" + return "Contact_Extractors" def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: """Initialize the algorithm parameters.""" @@ -98,15 +100,12 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True, ) ) - strati_settings = QgsSettings() - last_strati_column = strati_settings.value("m2l/strati_column", "") self.addParameter( - QgsProcessingParameterMatrix( - name=self.INPUT_STRATI_COLUMN, - description="Stratigraphic Order", - headers=["Unit"], - numberRows=0, - defaultValue=last_strati_column + QgsProcessingParameterFeatureSource( + self.INPUT_STRATI_COLUMN, + "Stratigraphic Order", + [QgsProcessing.TypeVector], + defaultValue='formation', ) ) ignore_settings = QgsSettings() @@ -145,34 +144,33 @@ def processAlgorithm( feedback.pushInfo("Loading data...") geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context) - strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + strati_column = self.parameterAsSource(parameters, self.INPUT_STRATI_COLUMN, context) ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context) - if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column): - raise QgsProcessingException("no stratigraphic column found") + + if isinstance(strati_column, QgsProcessingParameterMapLayer) : + raise QgsProcessingException("Invalid stratigraphic column layer") + + elif strati_column is not None: + # extract unit names from strati_column + field_name = "unit_name" + strati_order = [f[field_name] for f in strati_column.getFeatures()] if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units): feedback.pushInfo("no units to ignore specified") - - # if strati_column and strati_column.strip(): - # strati_column = [unit.strip() for unit in strati_column.split(',')] - # Save stratigraphic column settings - strati_column_settings = QgsSettings() - strati_column_settings.setValue('m2l/strati_column', strati_column) ignore_settings = QgsSettings() ignore_settings.setValue("m2l/ignore_units", ignore_units) unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context) - formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context) geology = qgsLayerToGeoDataFrame(geology) - if formation_field and formation_field in geology.columns: - mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units) + if unit_name_field and unit_name_field in geology.columns: + mask = ~geology[unit_name_field].astype(str).str.strip().isin(ignore_units) geology = geology[mask].reset_index(drop=True) - feedback.pushInfo(f"filtered by formation field: {formation_field}") + feedback.pushInfo(f"filtered by unit name field: {unit_name_field}") else: - feedback.pushInfo(f"no formation field found: {formation_field}") + feedback.pushInfo(f"no unit name field found: {unit_name_field}") faults = qgsLayerToGeoDataFrame(faults) if faults else None if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns: @@ -181,7 +179,7 @@ def processAlgorithm( feedback.pushInfo("Extracting Basal Contacts...") contact_extractor = ContactExtractor(geology, faults) all_contacts = contact_extractor.extract_all_contacts() - basal_contacts = contact_extractor.extract_basal_contacts(strati_column) + basal_contacts = contact_extractor.extract_basal_contacts(strati_order) feedback.pushInfo("Exporting Basal Contacts Layer...") basal_contacts = GeoDataFrameToQgsLayer( diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index f80cfe0..55a179c 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -24,6 +24,7 @@ QgsProcessingParameterField, QgsProcessingParameterRasterLayer, QgsProcessingParameterMatrix, + QgsCoordinateReferenceSystem, QgsVectorLayer, QgsWkbTypes, QgsSettings @@ -73,13 +74,13 @@ def name(self) -> str: return "loop_sorter" def displayName(self) -> str: - return "Loop3d: Stratigraphic sorter" + return "Automatic Stratigraphic Column" def group(self) -> str: - return "Loop3d" + return "Stratigraphy" def groupId(self) -> str: - return "Loop3d" + return "Stratigraphy_Column" def updateParameters(self, parameters): selected_method = parameters.get(self.METHOD, 0) @@ -340,7 +341,6 @@ def processAlgorithm( def createInstance(self) -> QgsProcessingAlgorithm: return StratigraphySorterAlgorithm() - # ------------------------------------------------------------------------- # Helper stub – you must replace with *your* conversion logic # ------------------------------------------------------------------------- @@ -415,4 +415,4 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee else: relationships_df = pd.DataFrame() - return units_df, relationships_df, contacts_df \ No newline at end of file + return units_df, relationships_df, contacts_df diff --git a/m2l/processing/algorithms/user_defined_sorter.py b/m2l/processing/algorithms/user_defined_sorter.py new file mode 100644 index 0000000..23857a3 --- /dev/null +++ b/m2l/processing/algorithms/user_defined_sorter.py @@ -0,0 +1,112 @@ +from typing import Any, Optional +from osgeo import gdal +import numpy as np +import json + +from PyQt5.QtCore import QVariant +from qgis import processing +from qgis.core import ( + QgsFeatureSink, + QgsFields, + QgsField, + QgsFeature, + QgsGeometry, + QgsRasterLayer, + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingException, + QgsProcessingFeedback, + QgsProcessingParameterEnum, + QgsProcessingParameterFileDestination, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterField, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterMatrix, + QgsCoordinateReferenceSystem, + QgsVectorLayer, + QgsWkbTypes, + QgsSettings +) + + +from qgis.core import ( + QgsFields, QgsField, QgsFeature, QgsFeatureSink, QgsWkbTypes, + QgsCoordinateReferenceSystem, QgsProcessingAlgorithm, QgsProcessingContext, + QgsProcessingFeedback, QgsProcessingParameterFeatureSink, QgsProcessingParameterMatrix, + QgsSettings +) +from PyQt5.QtCore import QVariant +import numpy as np + +class UserDefinedStratigraphyAlgorithm(QgsProcessingAlgorithm): + INPUT_STRATI_COLUMN = "INPUT_STRATI_COLUMN" + OUTPUT = "OUTPUT" + + def name(self): return "loop_sorter_2" + def displayName(self): return "User-Defined Stratigraphic Column" + def group(self): return "Stratigraphy" + def groupId(self): return "Stratigraphy_Column" + + def initAlgorithm(self, config=None): + strati_settings = QgsSettings() + last_strati_column = strati_settings.value("m2l/strati_column", "") + self.addParameter( + QgsProcessingParameterMatrix( + name=self.INPUT_STRATI_COLUMN, + description="Stratigraphic Order", + headers=["Unit"], + numberRows=0, + defaultValue=last_strati_column + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + "Stratigraphic column", + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + # 1) Read the matrix; it may be a list of lists (rows) or a flat list depending on input source. + matrix = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context) + + # Normalize to a list of unit strings (one column: "Unit") + units = [] + for row in matrix: + if isinstance(row, (list, tuple)): + unit = row[0] if row else "" + else: + unit = row + unit = (unit or "").strip() + if unit: # skip empty rows to avoid writing "" into fields + units.append(unit) + + # 2) Build sequential order (1-based), cast to native int + order_vals = [int(i) for i in (np.arange(len(units)) + 1)] + + # 3) Prepare sink + sink_fields = QgsFields() + sink_fields.append(QgsField("order", QVariant.Int)) # or QVariant.LongLong + sink_fields.append(QgsField("unit_name", QVariant.String)) + + crs = context.project().crs() if context and context.project() else QgsCoordinateReferenceSystem() + sink, dest_id = self.parameterAsSink( + parameters, self.OUTPUT, context, + sink_fields, QgsWkbTypes.NoGeometry, crs + ) + + # 4) Insert features + for pos, unit_name in zip(order_vals, units): + f = QgsFeature(sink_fields) + # Ensure correct types: int for "order", str for "unit_name" + f.setAttributes([int(pos), str(unit_name)]) + ok = sink.addFeature(f, QgsFeatureSink.FastInsert) + if not ok: + feedback.reportError(f"Failed to add feature for unit '{unit_name}' (order={pos}).") + + return {self.OUTPUT: dest_id} + + def createInstance(self): + return __class__() diff --git a/m2l/processing/provider.py b/m2l/processing/provider.py index 318e944..9616d9b 100644 --- a/m2l/processing/provider.py +++ b/m2l/processing/provider.py @@ -19,6 +19,7 @@ from .algorithms import ( BasalContactsAlgorithm, StratigraphySorterAlgorithm, + UserDefinedStratigraphyAlgorithm, ThicknessCalculatorAlgorithm, SamplerAlgorithm ) @@ -35,6 +36,7 @@ def loadAlgorithms(self): """Loads all algorithms belonging to this provider.""" self.addAlgorithm(BasalContactsAlgorithm()) self.addAlgorithm(StratigraphySorterAlgorithm()) + self.addAlgorithm(UserDefinedStratigraphyAlgorithm()) self.addAlgorithm(ThicknessCalculatorAlgorithm()) self.addAlgorithm(SamplerAlgorithm()) diff --git a/tests/qgis/input/stratigraphic_column_testing.gpkg b/tests/qgis/input/stratigraphic_column_testing.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..bb782ef5b2f52fba0d364aa506a7e5bdf571a79c GIT binary patch literal 73728 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 b00bZa0SG_<0uVS00+X)(fXnvO#pM41;`-|; literal 0 HcmV?d00001 diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py index c6f9256..bb90de8 100644 --- a/tests/qgis/test_basal_contacts.py +++ b/tests/qgis/test_basal_contacts.py @@ -1,6 +1,7 @@ import unittest from pathlib import Path -from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication +from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication, QgsFeature, QgsField +from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm from m2l.processing.provider import Map2LoopProvider @@ -20,9 +21,10 @@ def setUp(self): self.geology_file = self.input_dir / "geol_clip_no_gaps.shp" self.faults_file = self.input_dir / "faults_clip.shp" + self.strati_file = self.input_dir / "stratigraphic_column_testing.gpkg" self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}") - + self.assertTrue(self.strati_file.exists(), f"strati not found: {self.strati_file}") if not self.faults_file.exists(): QgsMessageLog.logMessage(f"faults not found: {self.faults_file}, will run test without faults", "TestBasalContacts", Qgis.Warning) @@ -42,26 +44,7 @@ def test_basal_contacts_extraction(self): QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical) - strati_column = [ - "Turee Creek Group", - "Boolgeeda Iron Formation", - "Woongarra Rhyolite", - "Weeli Wolli Formation", - "Brockman Iron Formation", - "Mount McRae Shale and Mount Sylvia Formation", - "Wittenoom Formation", - "Marra Mamba Iron Formation", - "Jeerinah Formation", - "Bunjinah Formation", - "Pyradie Formation", - "Fortescue Group", - "Hardey Formation", - "Boongal Formation", - "Mount Roe Basalt", - "Rocklea Inlier greenstones", - "Rocklea Inlier metagranitic unit" - ] - + strati_table = QgsVectorLayer(str(self.strati_file), "strati", "ogr") algorithm = BasalContactsAlgorithm() algorithm.initAlgorithm() @@ -70,7 +53,7 @@ def test_basal_contacts_extraction(self): 'UNIT_NAME_FIELD': 'unitname', 'FORMATION_FIELD': 'formation', 'FAULTS': faults_layer, - 'STRATIGRAPHIC_COLUMN': strati_column, + 'STRATIGRAPHIC_COLUMN': strati_table, 'IGNORE_UNITS': [], 'BASAL_CONTACTS': 'memory:basal_contacts', 'ALL_CONTACTS': 'memory:all_contacts' From 53917e3a0e47807e5d4d344f12c529d9304b373d Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 15 Oct 2025 13:17:34 +0800 Subject: [PATCH 113/116] add user defined boundingbox in ThicknessCalculatorAlgorithm --- .../algorithms/thickness_calculator.py | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index 51d6f91..a88f387 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -48,6 +48,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_THICKNESS_CALCULATOR_TYPE = 'THICKNESS_CALCULATOR_TYPE' INPUT_DTM = 'DTM' + INPUT_BOUNDING_BOX_TYPE = 'BOUNDING_BOX_TYPE' INPUT_BOUNDING_BOX = 'BOUNDING_BOX' INPUT_MAX_LINE_LENGTH = 'MAX_LINE_LENGTH' INPUT_STRATI_COLUMN = 'STRATIGRAPHIC_COLUMN' @@ -100,6 +101,29 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) ) + self.addParameter( + QgsProcessingParameterEnum( + self.INPUT_BOUNDING_BOX_TYPE, + "Bounding Box Type", + options=['Extract from geology layer', 'User defined'], + allowMultiple=False, + defaultValue=1 + ) + ) + + bbox_settings = QgsSettings() + last_bbox = bbox_settings.value("m2l/bounding_box", "") + self.addParameter( + QgsProcessingParameterMatrix( + self.INPUT_BOUNDING_BOX, + description="Static Bounding Box", + headers=['minx','miny','maxx','maxy'], + numberRows=1, + defaultValue=last_bbox, + optional=True + ) + ) + self.addParameter( QgsProcessingParameterNumber( self.INPUT_MAX_LINE_LENGTH, @@ -213,7 +237,7 @@ def processAlgorithm( thickness_type_index = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_CALCULATOR_TYPE, context) thickness_type = ['InterpolatedStructure', 'StructuralPoint'][thickness_type_index] dtm_data = self.parameterAsRasterLayer(parameters, self.INPUT_DTM, context) - bounding_box = self.parameterAsMatrix(parameters, self.INPUT_BOUNDING_BOX, context) + bounding_box_type = self.parameterAsEnum(parameters, self.INPUT_BOUNDING_BOX_TYPE, context) max_line_length = self.parameterAsSource(parameters, self.INPUT_MAX_LINE_LENGTH, context) basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) @@ -225,14 +249,26 @@ def processAlgorithm( sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context) - geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) - extent = geology_layer.extent() - bounding_box = { - 'minx': extent.xMinimum(), - 'miny': extent.yMinimum(), - 'maxx': extent.xMaximum(), - 'maxy': extent.yMaximum() - } + if bounding_box_type == 0: + geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) + extent = geology_layer.extent() + bounding_box = { + 'minx': extent.xMinimum(), + 'miny': extent.yMinimum(), + 'maxx': extent.xMaximum(), + 'maxy': extent.yMaximum() + } + feedback.pushInfo("Using bounding box from geology layer") + else: + static_bbox_matrix = self.parameterAsMatrix(parameters, self.INPUT_BOUNDING_BOX, context) + if not static_bbox_matrix or len(static_bbox_matrix) == 0: + raise QgsProcessingException("Bounding box is required") + + bounding_box = matrixToDict(static_bbox_matrix) + + bbox_settings = QgsSettings() + bbox_settings.setValue("m2l/bounding_box", static_bbox_matrix) + feedback.pushInfo("Using bounding box from user input") stratigraphic_column_source = self.parameterAsSource(parameters, self.INPUT_STRATIGRAPHIC_COLUMN_LAYER, context) stratigraphic_order = [] From c94b40cb706a5144f032ff8fd4869fcff25dea3e Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 15 Oct 2025 13:48:43 +0800 Subject: [PATCH 114/116] replace QMetaType with QVariant in sampler --- m2l/processing/algorithms/sampler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index cafd4b2..a3f384e 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -10,7 +10,7 @@ """ # Python imports from typing import Any, Optional -from qgis.PyQt.QtCore import QMetaType +from qgis.PyQt.QtCore import QVariant from osgeo import gdal import pandas as pd @@ -182,11 +182,11 @@ def processAlgorithm( dtype_str = str(dtype) if dtype_str in ['float16', 'float32', 'float64']: - field_type = QMetaType.Type.Double + field_type = QVariant.Double elif dtype_str in ['int8', 'int16', 'int32', 'int64']: - field_type = QMetaType.Type.Int + field_type = QVariant.Int else: - field_type = QMetaType.Type.QString + field_type = QVariant.String fields.append(QgsField(column_name, field_type)) From b32baec3fbd2695729aa22fe827543517a421677 Mon Sep 17 00:00:00 2001 From: Noelle Cheng Date: Wed, 15 Oct 2025 13:56:35 +0800 Subject: [PATCH 115/116] change orientation type variable name in ThicknessCalculatorAlgorithm --- m2l/processing/algorithms/thickness_calculator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/m2l/processing/algorithms/thickness_calculator.py b/m2l/processing/algorithms/thickness_calculator.py index a88f387..824fd34 100644 --- a/m2l/processing/algorithms/thickness_calculator.py +++ b/m2l/processing/algorithms/thickness_calculator.py @@ -57,7 +57,7 @@ class ThicknessCalculatorAlgorithm(QgsProcessingAlgorithm): INPUT_DIPDIR_FIELD = 'DIPDIR_FIELD' INPUT_DIP_FIELD = 'DIP_FIELD' INPUT_GEOLOGY = 'GEOLOGY' - INPUT_THICKNESS_ORIENTATION_TYPE = 'THICKNESS_ORIENTATION_TYPE' + INPUT_ORIENTATION_TYPE = 'ORIENTATION_TYPE' INPUT_UNIT_NAME_FIELD = 'UNIT_NAME_FIELD' INPUT_SAMPLED_CONTACTS = 'SAMPLED_CONTACTS' INPUT_STRATIGRAPHIC_COLUMN_LAYER = 'STRATIGRAPHIC_COLUMN_LAYER' @@ -195,8 +195,8 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: ) self.addParameter( QgsProcessingParameterEnum( - 'THICKNESS_ORIENTATION_TYPE', - 'Thickness Orientation Type', + self.INPUT_ORIENTATION_TYPE, + 'Orientation Type', options=['Dip Direction', 'Strike'], defaultValue=0 # Default to Dip Direction ) @@ -242,8 +242,8 @@ def processAlgorithm( basal_contacts = self.parameterAsSource(parameters, self.INPUT_BASAL_CONTACTS, context) geology_data = self.parameterAsSource(parameters, self.INPUT_GEOLOGY, context) structure_data = self.parameterAsSource(parameters, self.INPUT_STRUCTURE_DATA, context) - thickness_orientation_type = self.parameterAsEnum(parameters, self.INPUT_THICKNESS_ORIENTATION_TYPE, context) - is_strike = (thickness_orientation_type == 1) + orientation_type = self.parameterAsEnum(parameters, self.INPUT_ORIENTATION_TYPE, context) + is_strike = (orientation_type == 1) structure_dipdir_field = self.parameterAsString(parameters, self.INPUT_DIPDIR_FIELD, context) structure_dip_field = self.parameterAsString(parameters, self.INPUT_DIP_FIELD, context) sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context) From fc24ac33cb205d010e87854a88ce93b0a613279c Mon Sep 17 00:00:00 2001 From: noellehmcheng <143368485+noellehmcheng@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:49:40 +0800 Subject: [PATCH 116/116] Fix: processing tools/sorter 2 (#30) * fix replace contact layer input with ContactExtractor and remove duplicated units in Sorter * revert to using contacts layer instead of contact extractor class * add validation for contacts layer based on sorter * merge with noelle/thickness_calculator --- m2l/processing/algorithms/sampler.py | 2 +- m2l/processing/algorithms/sorter.py | 33 ++++++++++++++++++---------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/m2l/processing/algorithms/sampler.py b/m2l/processing/algorithms/sampler.py index 3e134d4..fdc9856 100644 --- a/m2l/processing/algorithms/sampler.py +++ b/m2l/processing/algorithms/sampler.py @@ -10,7 +10,7 @@ """ # Python imports from typing import Any, Optional -from qgis.PyQt.QtCore import QMetaType, QVariant +from qgis.PyQt.QtCore import QVariant from osgeo import gdal import pandas as pd diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 55a179c..ec7d086 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -125,7 +125,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: self.INPUT_GEOLOGY, "Geology polygons", [QgsProcessing.TypeVectorPolygon], - optional=True + optional=False ) ) @@ -161,7 +161,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: optional=True ) ) - + self.addParameter( QgsProcessingParameterField( 'GROUP_FIELD', @@ -225,7 +225,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None: "CONTACTS_LAYER", "Contacts Layer", [QgsProcessing.TypeVectorLine], - optional=False, + optional=True, ) ) @@ -256,9 +256,14 @@ def processAlgorithm( algo_index: int = self.parameterAsEnum(parameters, self.SORTING_ALGORITHM, context) sorter_cls = list(SORTER_LIST.values())[algo_index] + sorter_name = list(SORTER_LIST.keys())[algo_index] + requires_contacts = sorter_cls in [SorterAlpha, SorterMaximiseContacts, SorterObservationProjections] contacts_layer = self.parameterAsVectorLayer(parameters, self.CONTACTS_LAYER, context) in_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context) output_file = self.parameterAsFileOutput(parameters, 'JSON_OUTPUT', context) + + if requires_contacts and not contacts_layer or not contacts_layer.isValid(): + raise QgsProcessingException(f"{sorter_name} requires a contacts layer") units_df, relationships_df, contacts_df= build_input_frames(in_layer,contacts_layer, feedback,parameters) @@ -384,17 +389,21 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee if not group_field: raise QgsProcessingException("Group Field is required") + unique_units = {} units_records = [] for f in layer.getFeatures(): - units_records.append( - dict( - layerId=f.id(), - name=f[unit_name_field], - minAge=qvariantToFloat(f, min_age_field), - maxAge=qvariantToFloat(f, max_age_field), - group=f[group_field], + unit_name = f[unit_name_field] + if unit_name not in unique_units: + unique_units[unit_name] = len(unique_units) + units_records.append( + dict( + layerId=len(units_records), + name=f[unit_name_field], + minAge=qvariantToFloat(f, min_age_field), + maxAge=qvariantToFloat(f, max_age_field), + group=f[group_field], + ) ) - ) units_df = pd.DataFrame.from_records(units_records) feedback.pushInfo(f"Units → {len(units_df)} records") @@ -402,8 +411,8 @@ def build_input_frames(layer: QgsVectorLayer,contacts_layer: QgsVectorLayer, fee if not contacts_layer or not contacts_layer.isValid(): raise QgsProcessingException("No contacts layer provided") - contacts_df = qgsLayerToGeoDataFrame(contacts_layer) if contacts_layer else pd.DataFrame() + if not contacts_df.empty: relationships_df = contacts_df.copy() if 'length' in contacts_df.columns: