|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import os |
| 6 | +import re |
5 | 7 | from dataclasses import dataclass |
6 | 8 | from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple |
7 | 9 |
|
8 | | -from PyQt5.QtCore import Qt |
| 10 | +from PyQt5.QtCore import Qt, QTimer |
9 | 11 | from PyQt5.QtWidgets import ( |
10 | 12 | QComboBox, |
11 | 13 | QDialog, |
|
21 | 23 | from qgis.core import QgsMapLayerProxyModel, QgsProject, QgsVectorLayer |
22 | 24 | from qgis.gui import QgsMapLayerComboBox |
23 | 25 |
|
| 26 | +from ...main.helpers import ColumnMatcher |
24 | 27 | from ...main.vectorLayerWrapper import QgsLayerFromDataFrame, QgsLayerFromGeoDataFrame |
25 | 28 | from LoopDataConverter import Datatype, InputData, LoopConverter, SurveyName |
26 | 29 |
|
@@ -214,6 +217,11 @@ def __init__( |
214 | 217 | self.sources_layout.setLabelAlignment(Qt.AlignLeft | Qt.AlignTop) |
215 | 218 | layout.addWidget(self.sources_widget) |
216 | 219 | 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 |
217 | 225 |
|
218 | 226 | actions_widget = QWidget() |
219 | 227 | actions_layout = QHBoxLayout(actions_widget) |
@@ -276,11 +284,157 @@ def _build_data_source_inputs(self) -> None: |
276 | 284 |
|
277 | 285 | for data_type in self._data_types: |
278 | 286 | 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)) |
280 | 293 | combo.setAllowEmptyLayer(True) |
281 | 294 | combo.setObjectName(f"automaticSource_{self._format_identifier_label(data_type)}") |
282 | 295 | self.sources_layout.addRow(self._format_identifier_label(data_type), combo) |
283 | 296 | 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 |
284 | 438 |
|
285 | 439 | def _collect_data_sources(self) -> Dict[Datatype | str, str]: |
286 | 440 | data_sources: Dict[Datatype | str, str] = {} |
|
0 commit comments