SecInterp - Detailed Project Architectureο
Complete Technical Documentation for the SecInterp QGIS Plugin Version 2.9.0 | Last update: 2026-02-01
β οΈ Important Note: See the Core Components Distinction Guide (EN) or GuΓa de DistinciΓ³n de Core (ES) to differentiate between the
SecInterp Coreand theqgis.coreAPI.
π 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
β βββ models/ # Dataclasses and settings models
β βββ 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/ # π‘οΈ 3-Level Validation Architecture
β β βββ validators.py # Level 1: Reusable Type/Range validators
β β βββ validation_helpers.py# Level 2: Business field constraints
β β βββ project_validator.py# Level 3: Cross-layer domain logic
β βββ domain/ # [NEW] Domain Layer (Entities & DTOs)
β β βββ entities.py # Business objects with identity
β β βββ dtos.py # Data Transfer Objects
β β βββ task_inputs.py # Async Task Inputs
β β βββ enums.py # Domain-pure enumerations
β βββ utils/ # Utilities (Geometry, Spatial, etc.)
β
βββ gui/ # π₯οΈ User Interface (GUI Layer)
β βββ main_dialog.py # Main dialog (Simplified)
β βββ preview_renderer.py # Native PyQGIS rendering
β βββ tasks/ # π Asynchronous QgsTasks
β β βββ geology_task.py # Background geology intersection
β β βββ drillhole_task.py # Background drillhole projection
β βββ managers/ # π§© UI Logic Managers
β β βββ interpretation_mgr.py # Interpretation persistence
β β βββ message_mgr.py # Feedback and alerts
β βββ 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/>~350 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]
INTERP_MGR[interpretation_manager.py<br/>InterpretationManager]
MSG_MGR[message_manager.py<br/>MessageManager]
CONFIG_MGR[main_dialog_config.py<br/>DialogDefaults]
end
RENDERER[preview_renderer.py<br/>PreviewRenderer<br/>~280 lines]
LEGEND[legend_widget.py<br/>LegendWidget]
subgraph TOOLS["π οΈ Tools"]
MEASURE[measure_tool.py<br/>ProfileMeasureTool]
INTERP_TOOL[interpretation_tool.py<br/>InterpretationTool]
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]
end
end
%% ========== CORE LAYER ==========
subgraph CORE["βοΈ Core Layer - Business Logic"]
direction TB
CONTROLLER[controller.py<br/>ProfileController<br/>~280 lines]
subgraph SERVICES["π§ Services"]
PROFILE_SVC[profile_service.py<br/>ProfileService]
GEOLOGY_SVC[geology_service.py<br/>GeologyService]
STRUCTURE_SVC[structure_service.py<br/>StructureService]
STRUCTURE_SVC[structure_service.py<br/>StructureService]
subgraph DRILLHOLE_PKG["Drillhole Sub-system"]
DRILLHOLE_FACADE[drillhole_service.py<br/>DrillholeService (Facade)]
COLLAR_PROC[collar_processor.py]
SURVEY_PROC[survey_processor.py]
INTERVAL_PROC[interval_processor.py]
PROJ_ENGINE[projection_engine.py<br/>Pure Math]
end
end
subgraph TASKS["π QGS TASKS (Extract-then-Compute)"]
GEO_TASK[geology_task.py<br/>GeologyGenerationTask]
DRILL_TASK[drillhole_task.py<br/>DrillholeGenerationTask]
end
subgraph VALIDATION_PKG["π‘οΈ Validation Architecture"]
LEVEL1[validators.py<br/>Type/Range Checks]
LEVEL2[validation_helpers.py<br/>Business Logic]
LEVEL3[project_validator.py<br/>Domain Interaction]
end
METRICS[performance_metrics.py<br/>Performance Profiling]
DOMAIN[core/domain/<br/>Domain Layer Package]
subgraph UTILS["π¨ Utilities"]
GEOM_UTILS[geometry.py]
DRILL_UTILS[drillhole.py]
GEOLOGY_UTILS[geology.py]
SPATIAL_UTILS[spatial.py]
SAMPLING_UTILS[sampling.py]
end
end
%% ========== EXPORTERS LAYER ==========
subgraph EXPORTERS["π€ Exporters Layer - Export"]
direction TB
ORCHESTRATOR[orchestrator.py<br/>DataExportOrchestrator]
BASE_EXP[base_exporter.py<br/>BaseExporter]
subgraph EXPORT_FORMATS["Export Formats"]
SHP_EXP[shp_exporter.py<br/>ShapefileExporter]
CSV_EXP[csv_exporter.py<br/>CSVExporter]
PDF_EXP[pdf_exporter.py<br/>PDFExporter]
SVG_EXP[svg_exporter.py<br/>SVGExporter]
IMG_EXP[image_exporter.py<br/>ImageExporter]
PROFILE_EXP[profile_exporters.py<br/>Profile/Geology/Struct]
DRILL_EXP[drillhole_exporters.py<br/>2D Traces/Intervals]
DRILL_3D[drillhole_3d_exporter.py<br/>3D Traces/Intervals]
INTERP_3D[interpretation_3d_exporter.py<br/>3D Polygons]
end
end
%% ========== CONNECTIONS ==========
QGIS -->|loads| INIT
INIT -->|delegates| PLUGIN
PLUGIN -->|initializes| MAIN
MAIN -->|delegates| MANAGERS
MAIN -->|uses| UI_MAIN
PREVIEW_MGR -->|renders with| RENDERER
PREVIEW_MGR -->|updates| LEGEND
PREVIEW_MGR -->|activates| TOOLS
PREVIEW_MGR -->|launches| TASKS
TASKS -->|uses| SERVICES
EXPORT_MGR -->|delegates to| ORCHESTRATOR
VALIDATION_MGR -->|validates with| VALIDATION_PKG
CONTROLLER -->|orchestrates| SERVICES
SERVICES -->|uses| UTILS
SERVICES -->|uses| ALGORITHMS
ORCHESTRATOR -->|delegates to| EXPORT_FORMATS
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,DOMAIN 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: ~350 (Unified logic handled by Managers)
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)
self.interpretation_manager = InterpretationManager(self)
self.message_manager = MessageManager(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: ~250
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: ~280
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: ~280
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: ~540
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]
π‘οΈ 3-Level Validation Architectureο
The project implements a robust, tiered validation system to ensure data integrity across all layers without external dependencies like Pydantic.
Level 1: Type & Range Validation (Dataclass Layer)ο
Location:
core/models/settings_model.pyusingcore/validation/validators.py.Purpose: Immediate feedback during object instantiation.
Mechanism:
__post_init__hooks use reusable validators (e.g.,validate_and_clamp).Example: Ensuring vertical exaggeration is within
(0.1, 10.0).
Level 2: Business Field Validation (Form Layer)ο
Location:
core/validation/field_validator.py.Purpose: Validates individual fields based on plugin-specific business rules.
Mechanism: Checks for required fields, valid data types in QGIS layers, and mandatory combinations.
Level 3: Domain & Cross-Layer Validation (Project Layer)ο
Location:
core/validation/project_validator.py.Purpose: Validates the relationship between different layers and the project state.
Mechanism: Checks if the section line intersects the DEM extent, or if drillholes fall within the buffer zone.
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: 2026-01-18 Plugin Version: 2.7.0 Author: Juan M. Bernales