Skip to content

Commit 773ef48

Browse files
feature: Implement StratigraphySorterAlgorithm
1 parent 043ea5c commit 773ef48

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
from typing import Any, Optional
2+
3+
from qgis import processing
4+
from qgis.core import (
5+
QgsFeatureSink,
6+
QgsFields, QgsField, QgsFeature, QgsGeometry,
7+
QgsProcessing,
8+
QgsProcessingAlgorithm,
9+
QgsProcessingContext,
10+
QgsProcessingException,
11+
QgsProcessingFeedback,
12+
QgsProcessingParameterEnum,
13+
QgsProcessingParameterFeatureSink,
14+
QgsProcessingParameterFeatureSource,
15+
QgsVectorLayer,
16+
)
17+
18+
# ────────────────────────────────────────────────
19+
# map2loop sorters
20+
# ────────────────────────────────────────────────
21+
from map2loop.map2loop.sorter import (
22+
SorterAlpha,
23+
SorterAgeBased,
24+
SorterMaximiseContacts,
25+
SorterObservationProjections,
26+
SorterUseNetworkX,
27+
SorterUseHint, # kept for backwards compatibility
28+
)
29+
30+
# a lookup so we don’t need a giant if/else block
31+
SORTER_LIST = {
32+
"Age‐based": SorterAgeBased,
33+
"NetworkX topological": SorterUseNetworkX,
34+
"Hint (deprecated)": SorterUseHint,
35+
"Adjacency α": SorterAlpha,
36+
"Maximise contacts": SorterMaximiseContacts,
37+
"Observation projections": SorterObservationProjections,
38+
}
39+
40+
class StratigraphySorterAlgorithm(QgsProcessingAlgorithm):
41+
"""
42+
Creates a one-column ‘stratigraphic column’ table ordered
43+
by the selected map2loop sorter.
44+
"""
45+
46+
INPUT = "INPUT"
47+
ALGO = "SORT_ALGO"
48+
OUTPUT = "OUTPUT"
49+
50+
# ----------------------------------------------------------
51+
# Metadata
52+
# ----------------------------------------------------------
53+
def name(self) -> str:
54+
return "loop_sorter"
55+
56+
def displayName(self) -> str:
57+
return "loop: Stratigraphic sorter"
58+
59+
def group(self) -> str:
60+
return "Loop3d"
61+
62+
def groupId(self) -> str:
63+
return "loop3d"
64+
65+
# ----------------------------------------------------------
66+
# Parameters
67+
# ----------------------------------------------------------
68+
def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None:
69+
70+
self.addParameter(
71+
QgsProcessingParameterFeatureSource(
72+
self.INPUT,
73+
self.tr("Geology polygons"),
74+
[QgsProcessing.TypeVectorPolygon],
75+
)
76+
)
77+
78+
# enum so the user can pick the strategy from a dropdown
79+
self.addParameter(
80+
QgsProcessingParameterEnum(
81+
self.ALGO,
82+
self.tr("Sorting strategy"),
83+
options=list(SORTER_LIST.keys()),
84+
defaultValue=0, # Age-based is safest default
85+
)
86+
) #:contentReference[oaicite:0]{index=0}
87+
88+
self.addParameter(
89+
QgsProcessingParameterFeatureSink(
90+
self.OUTPUT,
91+
self.tr("Stratigraphic column"),
92+
)
93+
)
94+
95+
# ----------------------------------------------------------
96+
# Core
97+
# ----------------------------------------------------------
98+
def processAlgorithm(
99+
self,
100+
parameters: dict[str, Any],
101+
context: QgsProcessingContext,
102+
feedback: QgsProcessingFeedback,
103+
) -> dict[str, Any]:
104+
105+
# 1 ► fetch user selections
106+
in_layer: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.INPUT, context)
107+
algo_index: int = self.parameterAsEnum(parameters, self.ALGO, context)
108+
sorter_cls = list(SORTER_LIST.values())[algo_index]
109+
110+
feedback.pushInfo(f"Using sorter: {sorter_cls.__name__}")
111+
112+
# 2 ► convert QGIS layers / tables to pandas
113+
# --------------------------------------------------
114+
# You must supply these three DataFrames:
115+
# units_df — required (layerId, name, minAge, maxAge, group)
116+
# relationships_df — required (Index1 / Unitname1, Index2 / Unitname2 …)
117+
# contacts_df — required for all but Age‐based
118+
#
119+
# Typical workflow:
120+
# • iterate over in_layer.getFeatures()
121+
# • build dicts/lists
122+
# • pd.DataFrame(…)
123+
#
124+
# NB: map2loop does *not* need geometries – only attribute values.
125+
# --------------------------------------------------
126+
units_df, relationships_df, contacts_df, map_data = build_input_frames(in_layer, feedback)
127+
128+
# 3 ► run the sorter
129+
sorter = sorter_cls() # instantiation is always zero-argument
130+
order = sorter.sort(
131+
units_df,
132+
relationships_df,
133+
contacts_df,
134+
map_data,
135+
)
136+
137+
# 4 ► write an in-memory table with the result
138+
sink_fields = QgsFields()
139+
sink_fields.append(QgsField("strat_pos", int))
140+
sink_fields.append(QgsField("unit_name", str))
141+
142+
(sink, dest_id) = self.parameterAsSink(
143+
parameters,
144+
self.OUTPUT,
145+
context,
146+
sink_fields,
147+
QgsWkbTypes.NoGeometry,
148+
in_layer.sourceCrs(),
149+
)
150+
151+
for pos, name in enumerate(order, start=1):
152+
f = QgsFeature(sink_fields)
153+
f.setAttributes([pos, name])
154+
sink.addFeature(f, QgsFeatureSink.FastInsert)
155+
156+
return {self.OUTPUT: dest_id}
157+
158+
# ----------------------------------------------------------
159+
def createInstance(self) -> QgsProcessingAlgorithm:
160+
return StratigraphySorterAlgorithm()
161+
162+
163+
# -------------------------------------------------------------------------
164+
# Helper stub – you must replace with *your* conversion logic
165+
# -------------------------------------------------------------------------
166+
def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple:
167+
"""
168+
Placeholder that turns the geology layer (and any other project
169+
layers) into the four objects required by the sorter.
170+
171+
Returns
172+
-------
173+
(units_df, relationships_df, contacts_df, map_data)
174+
"""
175+
import pandas as pd
176+
from map2loop.map2loop.mapdata import MapData # adjust import path if needed
177+
178+
# Example: convert the geology layer to a very small units_df
179+
units_records = []
180+
for f in layer.getFeatures():
181+
units_records.append(
182+
dict(
183+
layerId=f.id(),
184+
name=f["UNITNAME"], # attribute names → your schema
185+
minAge=f.attribute("MIN_AGE"),
186+
maxAge=f.attribute("MAX_AGE"),
187+
group=f["GROUP"],
188+
)
189+
)
190+
units_df = pd.DataFrame.from_records(units_records)
191+
192+
# relationships_df and contacts_df are domain-specific ─ fill them here
193+
relationships_df = pd.DataFrame(columns=["Index1", "UNITNAME_1", "Index2", "UNITNAME_2"])
194+
contacts_df = pd.DataFrame(columns=["UNITNAME_1", "UNITNAME_2", "length"])
195+
196+
# map_data can be mocked if you only use Age-based sorter
197+
map_data = MapData() # or MapData.from_project(…) / MapData.from_files(…)
198+
199+
feedback.pushInfo(f"Units → {len(units_df)} records")
200+
201+
return units_df, relationships_df, contacts_df, map_data

0 commit comments

Comments
 (0)