Skip to content

Commit d901fe3

Browse files
feat: add layer guessing and filters to conversion widget
1 parent 33f12aa commit d901fe3

1 file changed

Lines changed: 156 additions & 2 deletions

File tree

loopstructural/gui/data_conversion/data_conversion_widget.py

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
from __future__ import annotations
44

5+
import os
6+
import re
57
from dataclasses import dataclass
68
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple
79

8-
from PyQt5.QtCore import Qt
10+
from PyQt5.QtCore import Qt, QTimer
911
from PyQt5.QtWidgets import (
1012
QComboBox,
1113
QDialog,
@@ -21,6 +23,7 @@
2123
from qgis.core import QgsMapLayerProxyModel, QgsProject, QgsVectorLayer
2224
from qgis.gui import QgsMapLayerComboBox
2325

26+
from ...main.helpers import ColumnMatcher
2427
from ...main.vectorLayerWrapper import QgsLayerFromDataFrame, QgsLayerFromGeoDataFrame
2528
from LoopDataConverter import Datatype, InputData, LoopConverter, SurveyName
2629

@@ -214,6 +217,11 @@ def __init__(
214217
self.sources_layout.setLabelAlignment(Qt.AlignLeft | Qt.AlignTop)
215218
layout.addWidget(self.sources_widget)
216219
self._build_data_source_inputs()
220+
if self.project is not None:
221+
try:
222+
self.project.layersAdded.connect(self._guess_layers)
223+
except Exception:
224+
pass
217225

218226
actions_widget = QWidget()
219227
actions_layout = QHBoxLayout(actions_widget)
@@ -276,11 +284,157 @@ def _build_data_source_inputs(self) -> None:
276284

277285
for data_type in self._data_types:
278286
combo = QgsMapLayerComboBox()
279-
combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
287+
if self.project is not None:
288+
try:
289+
combo.setProject(self.project)
290+
except Exception:
291+
pass
292+
combo.setFilters(self._layer_filter_for_data_type(data_type))
280293
combo.setAllowEmptyLayer(True)
281294
combo.setObjectName(f"automaticSource_{self._format_identifier_label(data_type)}")
282295
self.sources_layout.addRow(self._format_identifier_label(data_type), combo)
283296
self.layer_selectors[data_type] = combo
297+
self._schedule_guess_layers()
298+
299+
def _schedule_guess_layers(self) -> None:
300+
QTimer.singleShot(0, self._guess_layers)
301+
302+
def _guess_layers(self) -> None:
303+
"""Attempt to auto-select layers based on common naming conventions."""
304+
if self.project is None and QgsProject.instance() is None:
305+
return
306+
307+
for data_type, combo in self.layer_selectors.items():
308+
if combo.currentLayer() is not None:
309+
continue
310+
candidates = self._build_layer_candidate_map(combo)
311+
if not candidates:
312+
continue
313+
matcher = ColumnMatcher(list(candidates.keys()))
314+
match = None
315+
for target in self._target_keys_for_data_type(data_type):
316+
match = matcher.find_match(target)
317+
if match is not None:
318+
break
319+
if match is None:
320+
match = matcher.find_match(self._format_identifier_label(data_type))
321+
if match is None and isinstance(data_type, Datatype):
322+
value = getattr(data_type, "value", None)
323+
if isinstance(value, str) and value.strip():
324+
match = matcher.find_match(value)
325+
if match:
326+
layer = candidates.get(match)
327+
if layer is not None:
328+
combo.setLayer(layer)
329+
330+
def _build_layer_candidate_map(
331+
self, combo: QgsMapLayerComboBox
332+
) -> Dict[str, QgsVectorLayer]:
333+
candidates: Dict[str, QgsVectorLayer] = {}
334+
for layer in self._layers_for_combo(combo):
335+
if not isinstance(layer, QgsVectorLayer) or not layer.isValid():
336+
continue
337+
for candidate in self._layer_candidates(layer):
338+
if candidate and candidate not in candidates:
339+
candidates[candidate] = layer
340+
upper_candidate = candidate.upper() if isinstance(candidate, str) else None
341+
if upper_candidate and upper_candidate not in candidates:
342+
candidates[upper_candidate] = layer
343+
return candidates
344+
345+
def _layers_for_combo(self, combo: QgsMapLayerComboBox) -> List[QgsVectorLayer]:
346+
layers: List[QgsVectorLayer] = []
347+
for index in range(combo.count()):
348+
layer = combo.layer(index)
349+
if isinstance(layer, QgsVectorLayer):
350+
layers.append(layer)
351+
if layers:
352+
return layers
353+
project = self.project or QgsProject.instance()
354+
if project is None:
355+
return layers
356+
for layer in project.mapLayers().values():
357+
if isinstance(layer, QgsVectorLayer) and layer.isValid():
358+
layers.append(layer)
359+
return layers
360+
361+
def _layer_candidates(self, layer: QgsVectorLayer) -> List[str]:
362+
names: List[str] = []
363+
if layer is None:
364+
return names
365+
layer_name = layer.name()
366+
if layer_name:
367+
names.append(layer_name)
368+
try:
369+
source = layer.source()
370+
except Exception:
371+
source = ""
372+
names.extend(self._names_from_source(source))
373+
return names
374+
375+
def _names_from_source(self, source: str) -> List[str]:
376+
if not source:
377+
return []
378+
379+
names: List[str] = []
380+
parts = source.split("|")
381+
base = parts[0].strip()
382+
if base:
383+
names.append(base)
384+
basename = os.path.basename(base)
385+
if basename:
386+
names.append(basename)
387+
stem, _ = os.path.splitext(basename)
388+
if stem:
389+
names.append(stem)
390+
391+
for part in parts[1:]:
392+
key, _, value = part.partition("=")
393+
if not value:
394+
continue
395+
key = key.strip().lower()
396+
if key in ("layername", "table"):
397+
cleaned = value.strip().strip("\"'").strip()
398+
if cleaned:
399+
names.append(cleaned)
400+
401+
for pattern in (r"\blayername\s*=\s*([^|;]+)", r"\btable\s*=\s*([^|;]+)"):
402+
match = re.search(pattern, source, re.IGNORECASE)
403+
if match:
404+
cleaned = match.group(1).strip().strip("\"'").strip()
405+
if cleaned:
406+
names.append(cleaned)
407+
408+
return names
409+
410+
def _target_key_for_data_type(self, data_type: Datatype | str) -> str:
411+
if isinstance(data_type, Datatype):
412+
return data_type.name
413+
return str(data_type)
414+
415+
def _target_keys_for_data_type(self, data_type: Datatype | str) -> List[str]:
416+
key = self._target_key_for_data_type(data_type).upper()
417+
if key == "GEOLOGY":
418+
return ["GEOLOGY", "LITH", "LITHOLOGY", "OUTCROP"]
419+
if key == "FOLD":
420+
return ["FOLD", "FOLDS"]
421+
if key == "FAULT":
422+
return ["FAULT", "FAULTS"]
423+
if key == "STRUCTURE":
424+
return ["STRUCTURE", "STRUCTURES"]
425+
return [key]
426+
427+
def _layer_filter_for_data_type(self, data_type: Datatype | str) -> int:
428+
key = self._target_key_for_data_type(data_type).upper()
429+
if key == "GEOLOGY":
430+
return QgsMapLayerProxyModel.PolygonLayer
431+
if key == "FAULT":
432+
return QgsMapLayerProxyModel.LineLayer
433+
if key == "STRUCTURE":
434+
return QgsMapLayerProxyModel.PointLayer
435+
if key == "FOLD":
436+
return QgsMapLayerProxyModel.LineLayer
437+
return QgsMapLayerProxyModel.VectorLayer
284438

285439
def _collect_data_sources(self) -> Dict[Datatype | str, str]:
286440
data_sources: Dict[Datatype | str, str] = {}

0 commit comments

Comments
 (0)