SecInterp - Detailed Project ArchitectureΒΆ
Complete Technical Documentation for the SecInterp QGIS Plugin Version 2.2 | Last update: 2025-12-21
π Table of ContentsΒΆ
π― OverviewΒΆ
SecInterp (Section Interpreter) is a QGIS plugin designed for the extraction and visualization of geological data in cross-sections. The plugin allows geologists to generate topographic profiles, project geological outcrops, and analyze structural data in a unified 2D view.
Main FeaturesΒΆ
β Interactive Preview System with real-time rendering
β Parallel Processing for complex geological intersections
β Adaptive LOD (Level of Detail) based on zoom
β Measurement Tools with automatic snapping
β Drillhole Support with 3Dβ2D projection
β Multi-format Export (SHP, CSV, PDF, SVG, PNG)
π Directory StructureΒΆ
The project organization follows a clear modular structure to separate the interface, business logic, and utilities.
sec_interp/
βββ __init__.py # Plugin entry point
βββ sec_interp_plugin.py # Root class (SecInterp)
βββ metadata.txt # QGIS metadata
βββ Makefile # Automation (deploy, docs)
β
βββ core/ # βοΈ Business Logic (Core Layer)
β βββ controller.py # Orchestrator (ProfileController)
β βββ algorithms.py # Pure intersection logic
β βββ services/ # Specialized services
β β βββ profile_service.py # Topography and sampling
β β βββ geology_service.py # Geological intersections
β β βββ structure_service.py# Structural projection
β β βββ drillhole_service.py# Desurvey and 3D intervals
β β βββ preview_service.py # Preview orchestrator
β βββ validation/ # Modular validation package
β βββ utils/ # Utilities (Geometry, Spatial, etc.)
β
βββ gui/ # π₯οΈ User Interface (GUI Layer)
β βββ main_dialog.py # Main dialog (Simplified)
β βββ preview_renderer.py # Native PyQGIS rendering
β βββ parallel_geology.py # Worker for parallel processing
β βββ main_dialog_preview.py # Preview manager
β βββ ui/ # Components and Pages (Layouts)
β βββ tools/ # Map tools (Measure Tool)
β
βββ exporters/ # π€ Export Layer
β βββ base_exporter.py # Export interface
β βββ shp_exporter.py # Generic Shapefile exporter
β βββ profile_exporters.py # Specific profile exporters
β βββ drillhole_exporters.py # Drillhole exporters
β
βββ docs/ # π Technical documentation and manuals
βββ tests/ # π§ͺ Unit test suite
βββ resources/ # π¨ Icons and Qt resources
ποΈ System ArchitectureΒΆ
Full Architecture DiagramΒΆ
graph TB
%% ========== ENTRY POINT ==========
QGIS[QGIS Application]
INIT[__init__.py<br/>Entry Point]
PLUGIN[sec_interp_plugin.py<br/>SecInterp Class<br/>Plugin Root]
%% ========== GUI LAYER ==========
subgraph GUI["π₯οΈ GUI Layer - User Interface"]
direction TB
MAIN[main_dialog.py<br/>SecInterpDialog<br/>~340 lines]
subgraph MANAGERS["Managers"]
SIGNALS_MGR[main_dialog_signals.py<br/>SignalManager]
DATA_MGR[main_dialog_data.py<br/>DataAggregator]
PREVIEW_MGR[main_dialog_preview.py<br/>PreviewManager]
EXPORT_MGR[main_dialog_export.py<br/>ExportManager]
VALIDATION_MGR[main_dialog_validation.py<br/>DialogValidator]
CONFIG_MGR[main_dialog_config.py<br/>DialogDefaults]
end
RENDERER[preview_renderer.py<br/>PreviewRenderer<br/>1190 lines<br/>20 methods]
LEGEND[legend_widget.py<br/>LegendWidget<br/>1.6k lines]
subgraph TOOLS["π οΈ Tools"]
MEASURE[measure_tool.py<br/>ProfileMeasureTool<br/>Snapping + Measurement]
end
subgraph UI_WIDGETS["π¦ UI Components"]
UI_MAIN[main_window.py<br/>SecInterpMainWindow]
UI_PAGES[Page Classes:<br/>DemPage, SectionPage,<br/>GeologyPage, StructPage,<br/>DrillholePage]
UI_PREVIEW[PreviewWidget]
UI_OUTPUT[OutputWidget]
end
end
%% ========== CORE LAYER ==========
subgraph CORE["βοΈ Core Layer - Business Logic"]
direction TB
CONTROLLER[controller.py<br/>ProfileController<br/>192 lines]
subgraph SERVICES["π§ Services"]
PROFILE_SVC[profile_service.py<br/>ProfileService<br/>2.8k lines]
GEOLOGY_SVC[geology_service.py<br/>GeologyService<br/>244 lines<br/>8 methods]
STRUCTURE_SVC[structure_service.py<br/>StructureService<br/>216 lines<br/>7 methods]
DRILLHOLE_SVC[drillhole_service.py<br/>DrillholeService<br/>319 lines<br/>4 methods]
PARALLEL_GEO[parallel_geology.py<br/>ParallelGeologyService<br/>QThread Worker]
end
ALGORITHMS[core/algorithms.py<br/>Pure Business Logic<br/>~20 lines]
subgraph VALIDATION_PKG["π‘οΈ Validation Package"]
VALIDATION_INIT[core/validation/__init__.py<br/>Facade]
FIELD_VAL[core/validation/field_validator.py<br/>Fields and Inputs]
LAYER_VAL[core/validation/layer_validator.py<br/>QGIS Layers]
PATH_VAL[core/validation/path_validator.py<br/>File Paths]
PROJ_VAL[core/validation/project_validator.py<br/>Orchestrator]
end
CACHE[data_cache.py<br/>DataCache<br/>7.8k lines]
METRICS[performance_metrics.py<br/>PerformanceMetrics<br/>7.8k lines]
TYPES[types.py<br/>Type Definitions<br/>1.8k lines]
subgraph UTILS["π¨ Utilities"]
GEOM_UTILS[geometry.py<br/>345 lines<br/>Geometric operations]
DRILL_UTILS[drillhole.py<br/>7.2k lines<br/>Desurvey + Projection]
GEOLOGY_UTILS[geology.py<br/>1.4k lines]
SPATIAL_UTILS[spatial.py<br/>3.2k lines]
SAMPLING_UTILS[sampling.py<br/>3.7k lines]
PARSING_UTILS[parsing.py<br/>2.7k lines]
RENDERING_UTILS[rendering.py<br/>2.9k lines]
IO_UTILS[io.py<br/>2.6k lines]
end
end
%% ========== EXPORTERS LAYER ==========
subgraph EXPORTERS["π€ Exporters Layer - Export"]
direction TB
ORCHESTRATOR[orchestrator.py<br/>DataExportOrchestrator<br/>148 lines]
BASE_EXP[base_exporter.py<br/>BaseExporter<br/>Abstract Class]
subgraph EXPORT_FORMATS["Export Formats"]
SHP_EXP[shp_exporter.py<br/>ShapefileExporter<br/>3.3k lines]
CSV_EXP[csv_exporter.py<br/>CSVExporter<br/>1.3k lines]
PDF_EXP[pdf_exporter.py<br/>PDFExporter<br/>2.5k lines]
SVG_EXP[svg_exporter.py<br/>SVGExporter<br/>2.3k lines]
IMG_EXP[image_exporter.py<br/>ImageExporter<br/>2.1k lines]
PROFILE_EXP[profile_exporters.py<br/>ProfileLineShpExporter<br/>GeologyShpExporter<br/>StructureShpExporter<br/>AxesShpExporter<br/>8.3k lines]
DRILL_EXP[drillhole_exporters.py<br/>DrillholeTraceShpExporter<br/>DrillholeIntervalShpExporter<br/>4.2k lines]
end
end
%% ========== EXTERNAL DEPENDENCIES ==========
subgraph EXTERNAL["π External Dependencies"]
QGIS_CORE[qgis.core<br/>QgsVectorLayer<br/>QgsRasterLayer<br/>QgsGeometry<br/>QgsProcessing<br/>QgsSpatialIndex]
QGIS_GUI[qgis.gui<br/>QgsMapCanvas<br/>QgsMapTool<br/>QgsMapLayer]
PYQT5[PyQt5<br/>QtWidgets<br/>QtCore<br/>QtGui<br/>Signals/Slots]
end
%% ========== CONNECTIONS ==========
QGIS -->|loads| INIT
INIT -->|delegates| PLUGIN
PLUGIN -->|initializes| MAIN
MAIN -->|delegates signals| SIGNALS_MGR
MAIN -->|uses data from| DATA_MGR
MAIN -->|manages| PREVIEW_MGR
MAIN -->|manages| EXPORT_MGR
MAIN -->|manages| VALIDATION_MGR
MAIN -->|manages| CONFIG_MGR
MAIN -->|manages| CACHE_HANDLER
MAIN -->|uses| UI_MAIN
PREVIEW_MGR -->|renders with| RENDERER
PREVIEW_MGR -->|updates| LEGEND
PREVIEW_MGR -->|activates| MEASURE
PREVIEW_MGR -->|requests data| CONTROLLER
EXPORT_MGR -->|delegates to| ORCHESTRATOR
VALIDATION_MGR -->|validates with| PROJ_VAL
CONTROLLER -->|orchestrates| PROFILE_SVC
CONTROLLER -->|orchestrates| GEOLOGY_SVC
CONTROLLER -->|orchestrates| STRUCTURE_SVC
CONTROLLER -->|orchestrates| DRILLHOLE_SVC
CONTROLLER -->|uses| CACHE
CONTROLLER -->|tracks with| METRICS
GEOLOGY_SVC -->|offloads to| PARALLEL_GEO
GEOLOGY_SVC -->|uses| ALGORITHMS
STRUCTURE_SVC -->|uses| ALGORITHMS
DRILLHOLE_SVC -->|uses| DRILL_UTILS
PROFILE_SVC -->|uses| SAMPLING_UTILS
ALGORITHMS -->|uses| GEOM_UTILS
ALGORITHMS -->|uses| SPATIAL_UTILS
ORCHESTRATOR -->|delegates to| SHP_EXP
ORCHESTRATOR -->|delegates to| CSV_EXP
ORCHESTRATOR -->|delegates to| PROFILE_EXP
ORCHESTRATOR -->|delegates to| DRILL_EXP
RENDERER -->|uses| QGIS_GUI
CONTROLLER -->|uses| QGIS_CORE
MAIN -->|uses| PYQT5
classDef entryPoint fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff
classDef guiLayer fill:#4ecdc4,stroke:#0a9396,stroke-width:2px,color:#000
classDef coreLayer fill:#95e1d3,stroke:#38a169,stroke-width:2px,color:#000
classDef exportLayer fill:#ffd93d,stroke:#f59e0b,stroke-width:2px,color:#000
classDef externalLayer fill:#a8dadc,stroke:#457b9d,stroke-width:2px,color:#000
class QGIS,PLUGIN entryPoint
class MAIN,PREVIEW_MGR,EXPORT_MGR,VALIDATION_MGR,CONFIG_MGR,RENDERER,LEGEND,MEASURE guiLayer
class CONTROLLER,ALGORITHMS,PROJ_VAL,CACHE,METRICS,TYPES coreLayer
class PROFILE_SVC,GEOLOGY_SVC,STRUCTURE_SVC,DRILLHOLE_SVC,PARALLEL_GEO coreLayer
class ORCHESTRATOR,BASE_EXP,SHP_EXP,CSV_EXP,PDF_EXP,SVG_EXP,IMG_EXP,PROFILE_EXP,DRILL_EXP exportLayer
class QGIS_CORE,QGIS_GUI,PYQT5 externalLayer
π§© Visualizing Mermaid Diagrams in VS CodeΒΆ
You can preview Mermaid diagrams in VS Code with the Mermaid Editor extension (installed). Quick steps:
Open this file
docs/ARCHITECTURE.md.Place the cursor inside a
mermaidblock and open the command palette (Ctrl+Shift+P) β βOpen Mermaid Editorβ or βPreview Mermaidβ.Alternatively, use the Markdown preview (Ctrl+Shift+V) if you have
Markdown Preview Mermaid Support(already installed).
Quick Example (edit this block and save to see the preview):
graph LR
A[User] --> B[SecInterpPlugin]
B --> C[Generate Profile]
C --> D[Export SVG/PNG]
π₯οΈ GUI Layer - User InterfaceΒΆ
1. SecInterpDialog (main_dialog.py)ΒΆ
Main Class: SecInterpDialog
Inherits from: SecInterpMainWindow
Lines of code: ~340 (Reduced from 1,057)
Responsibility: Simplified main dialog that coordinates components via Managers
Key ComponentsΒΆ
class SecInterpDialog(SecInterpMainWindow):
"""Dialog for the SecInterp QGIS plugin."""
def __init__(self, iface=None, plugin_instance=None, parent=None):
# Logic Managers
self.signal_manager = DialogSignalManager(self)
self.data_aggregator = DialogDataAggregator(self)
# Operation Managers
self.validator = DialogValidator(self)
self.preview_manager = PreviewManager(self)
self.export_manager = ExportManager(self)
self.status_manager = DialogStatusManager(self)
self.settings_manager = DialogSettingsManager(self)
# Widgets
self.legend_widget = LegendWidget(self.preview_widget.canvas)
self.pan_tool = QgsMapToolPan(self.preview_widget.canvas)
self.measure_tool = ProfileMeasureTool(self.preview_widget.canvas)
Main MethodsΒΆ
Method |
Description |
Location |
|---|---|---|
|
Initializes dedicated managers |
|
|
Facade for the DataAggregator |
|
|
Actual data aggregation from pages |
|
|
Bulk signal connection |
|
|
Delegated to PreviewManager |
|
|
Delegated to ExportManager |
|
|
Delegated to StatusManager |
|
Signals and SlotsΒΆ
# Button connections
self.preview_widget.btn_preview.clicked.connect(self.preview_profile_handler)
self.preview_widget.btn_export.clicked.connect(self.export_preview)
self.preview_widget.btn_measure.toggled.connect(self.toggle_measure_tool)
# Checkbox connections
self.preview_widget.chk_topo.stateChanged.connect(self.update_preview_from_checkboxes)
self.preview_widget.chk_geol.stateChanged.connect(self.update_preview_from_checkboxes)
self.preview_widget.chk_struct.stateChanged.connect(self.update_preview_from_checkboxes)
self.preview_widget.chk_drillholes.stateChanged.connect(self.update_preview_from_checkboxes)
# Layer connections
self.page_dem.raster_combo.layerChanged.connect(self.update_button_state)
self.page_section.line_combo.layerChanged.connect(self.update_button_state)
2. PreviewManager (main_dialog_preview.py)ΒΆ
Class: PreviewManager
Lines of code: ~31,000
Responsibility: Manages preview generation and updates
Main MethodsΒΆ
class PreviewManager:
def generate_preview(self) -> Tuple[bool, str]:
"""Generates preview with validation and error handling."""
def update_from_checkboxes(self):
"""Updates preview when visualization options change."""
def _get_validated_inputs(self) -> Optional[Dict]:
"""Obtains and validates inputs from the dialog."""
def _process_data(self, inputs: Dict) -> Tuple:
"""Processes data using the controller."""
3. PreviewRenderer (preview_renderer.py)ΒΆ
Class: PreviewRenderer
Lines of code: 1,190
Methods: 20
Responsibility: Renders the preview canvas using native PyQGIS
Renderer ArchitectureΒΆ
graph LR
A[render] --> B[_create_topo_layer]
A --> C[_create_geol_layer]
A --> D[_create_struct_layer]
A --> E[_create_drillhole_layers]
A --> F[_create_axes_layer]
A --> G[_create_axes_labels_layer]
B --> H[_decimate_line_data]
B --> I[_adaptive_sample]
C --> H
D --> J[_interpolate_elevation]
H --> K[QgsVectorLayer]
I --> K
J --> K
LOD Optimization MethodsΒΆ
Method |
Purpose |
Algorithm |
|---|---|---|
|
Line simplification |
Douglas-Peucker |
|
Local curvature calculation |
Angle between segments |
|
Adaptive sampling |
Curvature-based |
Usage ExampleΒΆ
renderer = PreviewRenderer(canvas)
canvas, layers = renderer.render(
topo_data=[(0, 100), (10, 105), ...],
geol_data=[GeologySegment(...), ...],
struct_data=[StructureMeasurement(...), ...],
vert_exag=2.0,
dip_line_length=50.0,
max_points=1000,
preserve_extent=False
)
4. ProfileMeasureTool (measure_tool.py)ΒΆ
Class: ProfileMeasureTool
Inherits from: QgsMapTool
Responsibility: Measurement tool with snapping
FeaturesΒΆ
β Snapping to vertices of visible layers
β Distance calculation (Euclidean)
β Slope calculation (slope in degrees)
β Real-time visualization with rubber band
SignalsΒΆ
measurementChanged = pyqtSignal(float, float, float, float) # dx, dy, dist, slope
measurementCleared = pyqtSignal()
βοΈ Core Layer - Business LogicΒΆ
1. ProfileController (controller.py)ΒΆ
Class: ProfileController
Lines of code: 192
Responsibility: Orchestrates the data generation services
ArchitectureΒΆ
class ProfileController:
def __init__(self):
self.profile_service = ProfileService()
self.geology_service = GeologyService()
self.structure_service = StructureService()
self.drillhole_service = DrillholeService()
self.data_cache = DataCache()
Main MethodΒΆ
def generate_profile_data(self, values: Dict[str, Any]) -> Tuple[List, Any, Any, Any, List[str]]:
"""Unified method to generate all profile components.
Returns:
tuple: (profile_data, geol_data, struct_data, drillhole_data, messages)
"""
# 1. Topography
profile_data = self.profile_service.generate_topographic_profile(...)
# 2. Geology (if layer exists)
if outcrop_layer:
geol_data = self.geology_service.generate_geological_profile(...)
# 3. Structures (if layer exists)
if structural_layer:
struct_data = self.structure_service.project_structures(...)
# 4. Drillholes (if layer exists)
if collar_layer:
collars = self.drillhole_service.project_collars(...)
drillhole_data = self.drillhole_service.process_intervals(...)
return profile_data, geol_data, struct_data, drillhole_data, messages
2. GeologyService (geology_service.py)ΒΆ
Class: GeologyService
Lines of code: 244
Methods: 8
Responsibility: Generates geological profiles by intersecting polygons
Processing FlowΒΆ
sequenceDiagram
participant Client
participant GeoService as GeologyService
participant Utils as Utils
Client->>GeoService: generate_geological_profile()
GeoService->>GeoService: _generate_master_profile_data()
%% Optimized PyQGIS Intersection
GeoService->>GeoService: QgsFeatureRequest.setFilterRect()
loop For each candidate feature
GeoService->>GeoService: QgsGeometry.intersection()
GeoService->>GeoService: _process_intersection_geometry()
GeoService->>Utils: interpolate_elevation()
Utils-->>GeoService: elevation_points
end
GeoService-->>Client: List[GeologySegment]
Key MethodsΒΆ
Method |
Description |
|---|---|
|
Main method that orchestrates the process |
|
Generates grid of points and elevations |
|
Executes QGIS intersection algorithm |
|
Processes each intersection feature |
|
Creates GeologySegment from geometry |
Return TypeΒΆ
@dataclass
class GeologySegment:
unit_name: str
points: List[Tuple[float, float]] # (distance, elevation)
geometry: QgsGeometry
attributes: Dict[str, Any]
3. StructureService (structure_service.py)ΒΆ
Class: StructureService
Lines of code: 216
Methods: 7
Responsibility: Projects structural measurements (dip/strike)
Projection AlgorithmΒΆ
graph TD
A[Structural Measurement] --> B[Create Buffer]
B --> C[Filter Structures]
C --> D[For each structure]
D --> E[Project point to line]
E --> F[Interpolate elevation]
F --> G[Calculate apparent dip]
G --> H[StructureMeasurement]
Apparent Dip CalculationΒΆ
The formula used is:
apparent_dip = arctan(tan(true_dip) Γ |cos(strike - section_azimuth)|)
Implemented in utils.calculate_apparent_dip().
Return TypeΒΆ
@dataclass
class StructureMeasurement:
distance: float
elevation: float
apparent_dip: float
original_dip: float
original_strike: float
attributes: Dict[str, Any]
4. DrillholeService (drillhole_service.py)ΒΆ
Class: DrillholeService
Lines of code: 319
Methods: 4
Responsibility: Processes and projects drillhole data
Processing FlowΒΆ
graph TB
A[Collar Layer] --> B[project_collars]
C[Survey Layer] --> D[process_intervals]
E[Interval Layer] --> D
B --> F[Collar Points]
F --> D
D --> G[Desurvey Drillhole]
G --> H[Project to Section]
H --> I[Create Segments]
I --> J[Drillhole Data]
Main MethodsΒΆ
1. project_collars()
Projects collar points to the section line.
def project_collars(
self,
collar_layer: QgsVectorLayer,
line_geom: QgsGeometry,
line_start: QgsPointXY,
distance_area: QgsDistanceArea,
buffer_width: float,
collar_id_field: str,
use_geometry: bool,
collar_x_field: str,
collar_y_field: str,
collar_z_field: str,
collar_depth_field: str,
dem_layer: Optional[QgsRasterLayer],
) -> List[Dict]:
"""Returns list of dictionaries with collar_id, distance, elevation, depth."""
2. process_intervals()
Processes intervals and generates 2D traces.
def process_intervals(
self,
collar_points: List[Dict],
collar_layer: QgsVectorLayer,
survey_layer: QgsVectorLayer,
interval_layer: QgsVectorLayer,
# ... more parameters
) -> Tuple[List[GeologySegment], List[Dict]]:
"""Returns (geology_segments, drillhole_traces)."""
5. Utilities (core/utils/)ΒΆ
geometry.py (345 lines)ΒΆ
Geometric Operations with QGIS Core API
Function |
Description |
|---|---|
|
Creates temporary in-memory layer |
|
Extracts geometry vertices (multipart-safe) |
|
Extracts line vertices |
|
Runs QGIS algorithm with error handling |
|
Creates buffer using |
|
Filters features with spatial index |
|
Densifies line with |
drillhole.py (7,297 lines)ΒΆ
Drillhole Processing
Function |
Description |
|---|---|
|
Calculates 3D trajectory from survey |
|
Projects 3D trace to 2D plane |
|
Interpolates intervals on trace |
sampling.py (3,783 lines)ΒΆ
Sampling and Interpolation
Function |
Description |
|---|---|
|
Interpolates elevation on grid |
|
Samples raster along a line |
6. DataCache (data_cache.py)ΒΆ
Class: DataCache
Lines of code: 7,883
Responsibility: Cache of processed data
Cache StrategyΒΆ
class DataCache:
def get_cache_key(self, inputs: Dict) -> str:
"""Generates a unique key based on relevant inputs."""
# Considers: layers, bands, buffer, vertical exaggeration
def get(self, key: str) -> Optional[Dict]:
"""Retrieves data from cache."""
def set(self, key: str, data: Dict) -> None:
"""Stores data in cache."""
def clear(self) -> None:
"""Clears the entire cache."""
π€ Exporters Layer - Data ExportΒΆ
1. DataExportOrchestrator (orchestrator.py)ΒΆ
Class: DataExportOrchestrator
Lines of code: 148
Responsibility: Coordinates exports to multiple formats
Main MethodΒΆ
def export_data(
self,
output_folder: Path,
values: Dict[str, Any],
profile_data: List[Tuple],
geol_data: Optional[List[Any]],
struct_data: Optional[List[Any]],
drillhole_data: Optional[List[Any]] = None
) -> List[str]:
"""Exports generated data to CSV and Shapefile using lazy imports."""
# Lazy import of exporters
from sec_interp.exporters import (
AxesShpExporter,
CSVExporter,
GeologyShpExporter,
ProfileLineShpExporter,
StructureShpExporter,
DrillholeTraceShpExporter,
DrillholeIntervalShpExporter,
)
# Export topography
csv_exporter.export(output_folder / "topo_profile.csv", ...)
ProfileLineShpExporter({}).export(output_folder / "profile_line.shp", ...)
# Export geology
if geol_data:
csv_exporter.export(output_folder / "geol_profile.csv", ...)
GeologyShpExporter({}).export(output_folder / "geol_profile.shp", ...)
# Export structures
if struct_data:
csv_exporter.export(output_folder / "structural_profile.csv", ...)
StructureShpExporter({}).export(output_folder / "structural_profile.shp", ...)
# Export drillholes
if drillhole_data:
DrillholeTraceShpExporter({}).export(output_folder / "drillhole_traces.shp", ...)
DrillholeIntervalShpExporter({}).export(output_folder / "drillhole_intervals.shp", ...)
return result_msg
2. Exporter HierarchyΒΆ
classDiagram
class BaseExporter {
<<abstract>>
+export(path, data)
}
class CSVExporter {
+export(path, data)
}
class ShapefileExporter {
+export(path, data)
}
class ImageExporter {
+export(path, map_settings)
}
class PDFExporter {
+export(path, map_settings)
}
class SVGExporter {
+export(path, map_settings)
}
BaseExporter <|-- CSVExporter
BaseExporter <|-- ShapefileExporter
BaseExporter <|-- ImageExporter
BaseExporter <|-- PDFExporter
BaseExporter <|-- SVGExporter
ShapefileExporter <|-- ProfileLineShpExporter
ShapefileExporter <|-- GeologyShpExporter
ShapefileExporter <|-- StructureShpExporter
ShapefileExporter <|-- AxesShpExporter
ShapefileExporter <|-- DrillholeTraceShpExporter
ShapefileExporter <|-- DrillholeIntervalShpExporter
π Main Data FlowsΒΆ
Flow 1: Preview GenerationΒΆ
sequenceDiagram
participant User
participant Dialog as SecInterpDialog
participant PreviewMgr as PreviewManager
participant Controller
participant Services
participant Renderer
participant Canvas
User->>Dialog: Click "Preview Profile"
Dialog->>PreviewMgr: generate_preview()
PreviewMgr->>PreviewMgr: _get_validated_inputs()
PreviewMgr->>Controller: generate_profile_data(inputs)
par Parallel Processing
Controller->>Services: ProfileService.generate_topographic_profile()
Services-->>Controller: profile_data
Controller->>Services: GeologyService.generate_geological_profile()
Services-->>Controller: geol_data
Controller->>Services: StructureService.project_structures()
Services-->>Controller: struct_data
Controller->>Services: DrillholeService.project_collars()
Services->>Services: DrillholeService.process_intervals()
Services-->>Controller: drillhole_data
end
Controller-->>PreviewMgr: (profile, geol, struct, drill, msgs)
PreviewMgr->>Renderer: render(profile, geol, struct, drill, vert_exag, ...)
Renderer->>Renderer: _create_topo_layer()
Renderer->>Renderer: _create_geol_layer()
Renderer->>Renderer: _create_struct_layer()
Renderer->>Renderer: _create_drillhole_layers()
Renderer->>Renderer: _create_axes_layer()
Renderer->>Canvas: setLayers(layers)
Renderer->>Canvas: zoomToFullExtent()
Renderer-->>PreviewMgr: (canvas, layers)
PreviewMgr-->>Dialog: success
Dialog-->>User: Display Preview
Flow 2: Data ExportΒΆ
sequenceDiagram
participant User
participant Dialog
participant ExportMgr as ExportManager
participant Controller
participant Orchestrator
participant Exporters
User->>Dialog: Click "Save"
Dialog->>ExportMgr: export_data()
ExportMgr->>Controller: generate_profile_data(inputs)
Controller-->>ExportMgr: (profile, geol, struct, drill, msgs)
ExportMgr->>Orchestrator: export_data(folder, values, data...)
Orchestrator->>Exporters: CSVExporter.export("topo_profile.csv")
Exporters-->>Orchestrator: success
Orchestrator->>Exporters: ProfileLineShpExporter.export("profile_line.shp")
Exporters-->>Orchestrator: success
Orchestrator->>Exporters: GeologyShpExporter.export("geol_profile.shp")
Exporters-->>Orchestrator: success
Orchestrator->>Exporters: StructureShpExporter.export("structural_profile.shp")
Exporters-->>Orchestrator: success
Orchestrator->>Exporters: DrillholeTraceShpExporter.export("drillhole_traces.shp")
Exporters-->>Orchestrator: success
Orchestrator-->>ExportMgr: result_messages
ExportMgr-->>Dialog: success
Dialog-->>User: "All files saved to: {folder}"
Flow 3: Parallel Geological ProcessingΒΆ
sequenceDiagram
participant Main as Main Thread
participant GeoService as GeologyService
participant ParallelGeo as ParallelGeologyService
participant Worker as GeologyProcessingThread
Main->>GeoService: generate_geological_profile()
GeoService->>ParallelGeo: process_async(line, raster, outcrop, field, band)
ParallelGeo->>Worker: start()
Note over Worker: QThread Worker
Worker->>Worker: run()
Worker->>Worker: _generate_master_profile_data()
Worker->>Worker: _perform_intersection()
loop For each feature
Worker->>Worker: _process_intersection_feature()
end
Worker->>ParallelGeo: finished.emit(results)
ParallelGeo->>GeoService: return results
GeoService-->>Main: List[GeologySegment]
π¨ Design PatternsΒΆ
1. MVC (Model-View-Controller)ΒΆ
Model: Services + Algorithms + Types
View: GUI Widgets + Renderer
Controller: ProfileController
2. Strategy PatternΒΆ
Different exporters implement the same BaseExporter interface:
class BaseExporter(ABC):
@abstractmethod
def export(self, path: Path, data: Dict) -> bool:
pass
class CSVExporter(BaseExporter):
def export(self, path: Path, data: Dict) -> bool:
# CSV Specific implementation
class ShapefileExporter(BaseExporter):
def export(self, path: Path, data: Dict) -> bool:
# Shapefile Specific implementation
3. Observer PatternΒΆ
PyQt5 Signals/Slots for communication between components:
# Signal
measurementChanged = pyqtSignal(float, float, float, float)
# Slot
def update_measurement_display(self, dx, dy, dist, slope):
msg = f"Distance: {dist:.2f} m..."
self.results_text.setHtml(msg)
# Connection
self.measure_tool.measurementChanged.connect(self.update_measurement_display)
4. Facade PatternΒΆ
ProfileController acts as a facade for the services:
class ProfileController:
def generate_profile_data(self, values):
# Orchestrates multiple services
profile = self.profile_service.generate_topographic_profile(...)
geol = self.geology_service.generate_geological_profile(...)
struct = self.structure_service.project_structures(...)
drill = self.drillhole_service.process_intervals(...)
return profile, geol, struct, drill, msgs
5. Factory PatternΒΆ
Exporter Factory:
def get_exporter(ext: str, settings: Dict) -> BaseExporter:
exporters = {
'.png': ImageExporter,
'.jpg': ImageExporter,
'.pdf': PDFExporter,
'.svg': SVGExporter,
}
exporter_class = exporters.get(ext)
return exporter_class(settings)
6. Singleton Pattern (Implicit)ΒΆ
DataCache is instantiated once in the controller.
7. Template Method PatternΒΆ
BaseExporter defines the template, subclasses implement details:
class BaseExporter(ABC):
def export(self, path, data):
self._validate(data)
self._prepare(data)
self._write(path, data)
self._finalize()
@abstractmethod
def _write(self, path, data):
pass
π External DependenciesΒΆ
QGIS Core APIΒΆ
from qgis.core import (
QgsVectorLayer, # Vector layers
QgsRasterLayer, # Raster layers
QgsGeometry, # Geometric operations
QgsProcessing, # Processing algorithms
QgsSpatialIndex, # Spatial indices
QgsCoordinateTransform,# Coordinate transformations
QgsDistanceArea, # Distance calculations
QgsProject, # QGIS Project
QgsFeature, # Features
QgsField, # Fields
QgsWkbTypes, # Geometry types
)
Main use: All geometric operations, spatial processing, and layer management.
QGIS GUI APIΒΆ
from qgis.gui import (
QgsMapCanvas, # Map canvas
QgsMapTool, # Map tools
QgsMapLayer, # Map layers
QgsMapLayerComboBox, # Layer combo boxes
QgsFileWidget, # File widget
)
Main use: User interface, interactive tools, specialized widgets.
PyQt5ΒΆ
from PyQt5.QtCore import (
Qt, # Qt constants
QVariant, # Data types
pyqtSignal, # Signals
pyqtSlot, # Slots
)
from PyQt5.QtWidgets import (
QDialog, # Dialogs
QWidget, # Base widgets
QPushButton, # Buttons
QCheckBox, # Checkboxes
QSpinBox, # Spin boxes
QComboBox, # Combo boxes
QLabel, # Labels
QGroupBox, # Group boxes
QVBoxLayout, # Vertical layouts
QHBoxLayout, # Horizontal layouts
)
from PyQt5.QtGui import (
QColor, # Colors
QFont, # Fonts
QPen, # Drawing pens
QBrush, # Fill brushes
)
Main use: Complete UI framework, signals/slots, layouts, widgets.
β‘ Performance OptimizationsΒΆ
1. Adaptive Level of Detail (LOD)ΒΆ
Implemented in: PreviewRenderer
def _decimate_line_data(self, data, tolerance=None, max_points=1000):
"""Simplifies lines using Douglas-Peucker."""
if len(data) <= max_points:
return data
# Calculate automatic tolerance
if tolerance is None:
x_range = max(p[0] for p in data) - min(p[0] for p in data)
tolerance = x_range / (max_points * 2)
# Apply Douglas-Peucker
simplified = self._douglas_peucker(data, tolerance)
return simplified
Benefit: Reduces 10,000+ points to ~1,000 without significant visual loss.
2. Curvature-based Adaptive SamplingΒΆ
def _adaptive_sample(self, data, min_tolerance=0.1, max_tolerance=10.0, max_points=1000):
"""Samples more densely in high-curvature areas."""
curvatures = self._calculate_curvature(data)
# Normalize curvatures
max_curv = max(curvatures)
normalized = [c / max_curv for c in curvatures]
# Tolerance inversely proportional to curvature
tolerances = [
max_tolerance - (max_tolerance - min_tolerance) * n
for n in normalized
]
# Apply Douglas-Peucker with variable tolerance
return self._douglas_peucker_adaptive(data, tolerances)
Benefit: Preserves important details (tight curves) while simplifying straight areas.
3. Parallel Geological ProcessingΒΆ
Implemented in: ParallelGeologyService
class ParallelGeologyService(QObject):
finished = pyqtSignal(list)
progress = pyqtSignal(int)
error = pyqtSignal(str)
def process_async(self, line_lyr, raster_lyr, outcrop_lyr, field, band):
"""Processes geology in a separate thread."""
self.worker = GeologyProcessingThread(...)
self.worker.finished.connect(self.finished.emit)
self.worker.start()
Benefit: UI remains responsive during heavy processing.
4. Processed Data CacheΒΆ
Implemented in: DataCache
def get_cache_key(self, inputs: Dict) -> str:
"""Generates a unique key based on relevant inputs."""
key_parts = [
inputs.get("raster_layer"),
inputs.get("selected_band"),
inputs.get("crossline_layer"),
inputs.get("buffer_distance"),
# DOES NOT include: vertexag, dip_scale_factor (only for display)
]
return hashlib.md5(str(key_parts).encode()).hexdigest()
Benefit: Avoids re-processing when only display parameters change.
5. Spatial Index for FilteringΒΆ
Implemented in: geometry.filter_features_by_buffer()
def filter_features_by_buffer(features_layer, buffer_geometry):
"""Filters features using a spatial index."""
# 1. Build spatial index
index = QgsSpatialIndex(features_layer.getFeatures())
# 2. Fast search by bounding box
candidate_ids = index.intersects(buffer_geometry.boundingBox())
# 3. Precise filtering of candidates only
filtered = []
for fid in candidate_ids:
feature = features_layer.getFeature(fid)
if feature.geometry().intersects(buffer_geometry):
filtered.append(feature)
return filtered
Benefit: O(log n) instead of O(n) for spatial filtering.
π Project MetricsΒΆ
Code StatisticsΒΆ
Metric |
Value |
|---|---|
Python Modules |
~60 files |
Total Lines of Code |
~15,000 LOC |
Core Lines of Code |
~8,000 LOC |
GUI Lines of Code |
~5,000 LOC |
Exporters Lines of Code |
~2,000 LOC |
Main Classes |
25+ |
Functions/Methods |
200+ |
Distribution by LayerΒΆ
pie title Code Distribution by Layer
"Core (53%)" : 8000
"GUI (33%)" : 5000
"Exporters (13%)" : 2000
Complexity by ModuleΒΆ
Module |
Lines |
Classes |
Methods |
Complexity |
|---|---|---|---|---|
|
~600 |
1 |
15 |
Medium |
|
~340 |
1 |
12 |
Low/Medium |
|
~200 |
1 |
10 |
Medium |
|
~150 |
1 |
8 |
Medium |
|
1,190 |
1 |
20 |
High |
|
192 |
1 |
4 |
Low |
|
~800 |
0 |
25 |
Medium |
|
244 |
1 |
8 |
Medium |
|
216 |
1 |
7 |
Medium |
|
319 |
1 |
4 |
Medium |
|
345 |
0 |
10 |
Medium |
|
148 |
1 |
1 |
Low |
DependenciesΒΆ
graph LR
A[SecInterp Plugin] --> B[QGIS Core API]
A --> C[QGIS GUI API]
A --> D[PyQt5]
B --> E[Python 3.x]
C --> E
D --> E
style A fill:#ff6b6b
style B fill:#4ecdc4
style C fill:#4ecdc4
style D fill:#95e1d3
style E fill:#ffd93d
Feature CoverageΒΆ
Feature |
Status |
Coverage |
|---|---|---|
Topographic Profile |
β Complete |
100% |
Geological Projection |
β Complete |
100% |
Structural Projection |
β Complete |
100% |
Drillhole Projection |
β Complete |
100% |
Interactive Preview |
β Complete |
100% |
Measurement Tools |
β Complete |
100% |
CSV Export |
β Complete |
100% |
Shapefile Export |
β Complete |
100% |
PDF Export |
β Complete |
100% |
SVG Export |
β Complete |
100% |
PNG/JPG Export |
β Complete |
100% |
Adaptive LOD |
β Complete |
100% |
Parallel Processing |
β Complete |
100% |
Data Cache |
β Complete |
100% |
π ReferencesΒΆ
[Source Code](file:///home/jmbernales/qgispluginsdev/sec_interp)
[Main README](file:///home/jmbernales/qgispluginsdev/sec_interp/README.md)
[User Guide](file:///home/jmbernales/qgispluginsdev/sec_interp/docs/USER_GUIDE.md)
[Architecture Graph](file:///home/jmbernales/qgispluginsdev/sec_interp/docs/sec_interp_architecture_graph.md)
π¨ Design PrinciplesΒΆ
The SecInterp plugin has been designed following robust software engineering principles to ensure quality and maintainability.
SOLID PrinciplesΒΆ
SRP (Single Responsibility Principle): Each service (Profile, Geology, Structure, Drillhole) has a single, clear responsibility.
OCP (Open/Closed Principle): Exporters are easily extensible through the abstract base class without modifying the core logic.
LSP (Liskov Substitution Principle): All concrete exporters can subtitute the
BaseExporterclass.ISP (Interface Segregation Principle): Service interfaces are focused on their specific domain.
DIP (Dependency Inversion Principle): The controller depends on abstractions and injected services, not on heavy concrete implementations.
Other Patterns and PrinciplesΒΆ
DRY (Donβt Repeat Yourself): Intensive use of
utilsmodules to centralize mathematical and spatial calculations.Separation of Concerns: Clear distinction between the GUI Layer (Managers), Core Layer (Services), and Data Layer (DataCache).
π ExtensibilityΒΆ
Quick guide for developers wishing to expand the plugin.
Adding a New ServiceΒΆ
Create the new file in
core/services/(e.g.,seismic_service.py).Implement the service logic following the pattern of other services.
Register the service in
controller.pywithin theProfileControllerconstructor.Add the orchestrator method in the controller and connect it to
PreviewManager.
Adding a New Export FormatΒΆ
Create a class in
exporters/that inherits fromBaseExporter.Implement the required
export()method.Register the new exporter in the factory in
orchestrator.pyor the specific export modules.
π¦ DeploymentΒΆ
The plugin uses a Makefile-based system to facilitate local deployment and packaging.
Main command:
make deploy(Copies files to the QGIS plugins directory).Process:
Temporary files (
.pyc, etc.) are cleaned.Resources and translations are copied.
Synchronized with the local QGIS directory for immediate testing.
π Final NotesΒΆ
This document provides a detailed view of the SecInterp plugin architecture. For development information, see [README_DEV.md](file:///home/jmbernales/qgispluginsdev/sec_interp/README_DEV.md).
Last update: 2025-12-21 Plugin Version: 2.2 Author: Juan M. Bernales