Architecture¶
This document describes the architecture and design of Species Explorer.
Overview¶
Species Explorer is a QGIS Python plugin that interfaces with the GBIF REST API to fetch and visualize species occurrence data. The plugin uses asynchronous background tasks for non-blocking data fetching.
graph TB
subgraph QGIS
A[Plugin Entry<br/>__init__.py] --> B[Main Plugin<br/>species_explorer.py]
B --> C[Dialog<br/>species_explorer_dialog.py]
C --> D[GBIF Utils<br/>gbifutils.py]
C --> E[Async Fetcher<br/>gbif_fetcher.py]
E --> F[Layer Creation]
F --> G[QGIS Map Canvas]
end
subgraph Branding
H[Kartoza Header]
I[Kartoza Footer]
J[Status Label]
end
subgraph External
K[GBIF API<br/>api.gbif.org/v1]
end
C --> H
C --> I
C --> J
D <--> K
E <--> K
Directory Structure¶
SpeciesExplorer/
├── species_explorer/ # Main plugin package (deployed to QGIS)
│ ├── __init__.py # Plugin initialization & classFactory
│ ├── species_explorer.py # Main plugin class
│ ├── species_explorer_dialog.py # Dialog UI logic
│ ├── species_explorer_dialog_base.ui # Qt Designer UI file
│ ├── gbifutils.py # GBIF API utilities (sync)
│ ├── gbif_fetcher.py # Async GBIF fetcher (QgsTask)
│ ├── resources.py # Compiled Qt resources
│ ├── resources.qrc # Qt resource definitions
│ ├── icon.png # Plugin icon
│ ├── metadata.txt # QGIS plugin metadata
│ ├── gui/
│ │ └── kartoza_branding.py # Kartoza branding components
│ └── resources/
│ └── styles/kartoza.qss # Custom QSS stylesheet
├── test/ # Test suite
├── docs/ # MkDocs documentation
├── .github/workflows/ # CI/CD workflows
├── flake.nix # Nix development environment
└── mkdocs.yml # Documentation configuration
Important: Only the species_explorer/ folder is deployed to QGIS. All other files are for development and documentation.
Core Components¶
Plugin Entry Point¶
File: species_explorer/__init__.py
The entry point for QGIS plugin loading:
def classFactory(iface):
from .species_explorer import SpeciesExplorer
return SpeciesExplorer(iface)
Main Plugin Class¶
File: species_explorer/species_explorer.py
Handles plugin lifecycle:
| Method | Purpose |
|---|---|
__init__(iface) |
Initialize with QGIS interface |
initGui() |
Create toolbar and menu items |
run() |
Show the Species Explorer dialog |
unload() |
Clean up on plugin unload |
add_action() |
Helper for toolbar actions |
tr() |
Translation support |
Dialog Class¶
File: species_explorer/species_explorer_dialog.py
Main user interface and business logic:
| Method | Purpose |
|---|---|
find() |
Search GBIF for species |
select(item) |
Display taxonomic hierarchy |
fetch() |
Start async background fetch |
_on_fetch_finished(task) |
Handle fetch completion |
_set_status() |
Update status display |
_set_fetching_state() |
Toggle UI during fetch |
closeEvent() |
Cancel tasks on close |
UI Elements:
search_text- Species name inputsearch_button- Trigger searchresults_list- Matching species listtaxonomy_list- Taxonomic hierarchy displayfetch_button- Download occurrencesstatus_label- Progress/error display
Async GBIF Fetcher¶
File: species_explorer/gbif_fetcher.py
Non-blocking background task for fetching occurrence data:
Key Features:
- Extends
QgsTaskfor QGIS task management - Runs in background thread without blocking UI
- Handles paginated GBIF API responses
- Progress reporting capability
- User-cancellable
- Creates
QgsVectorLayerfrom fetched data
Configuration Constants:
Methods:
| Method | Purpose |
|---|---|
run() |
Main execution in background |
_fetch_occurrences() |
Orchestrate pagination |
_make_request(offset) |
Single HTTP request |
_process_records() |
Parse occurrence data |
_create_layer() |
Create QgsVectorLayer |
finished(result) |
Callback on completion |
Usage:
from species_explorer.gbif_fetcher import fetch_species_async
def on_complete(task):
if task.layer:
QgsProject.instance().addMapLayer(task.layer)
fetch_species_async("Panthera leo", on_complete)
GBIF Utilities¶
File: species_explorer/gbifutils.py
Synchronous API interaction layer (adapted from pygbif):
| Function | Purpose |
|---|---|
gbif_GET(url, args) |
HTTP GET with QGIS networking |
name_parser(name) |
Parse scientific names |
name_usage(key, ...) |
Look up taxon details |
Exception Classes:
class NoResultException(Exception):
"""Raised when GBIF returns no results."""
class GBIFNetworkError(Exception):
"""Raised for network failures."""
Networking:
- Uses
QgsFileDownloaderfor QGIS proxy/SSL support - Synchronous wrapper with
QEventLoop - No external HTTP libraries required
Kartoza Branding¶
File: species_explorer/gui/kartoza_branding.py
Consistent branding components:
Brand Colors:
Components:
| Class | Purpose |
|---|---|
KartozaHeader |
Branded header with logo |
KartozaFooter |
Footer with links |
StatusLabel |
Color-coded status display |
Data Flow¶
Complete Workflow Sequence¶
sequenceDiagram
participant User
participant Dialog
participant GBIFUtils
participant GBIFFetchTask
participant GBIF API
participant QGIS
User->>Dialog: Enter species name
User->>Dialog: Click Find
Dialog->>GBIFUtils: name_parser(name)
GBIFUtils->>GBIF API: GET /parser/name
GBIF API-->>GBIFUtils: Parsed name
Dialog->>GBIFUtils: species/search
GBIFUtils->>GBIF API: GET /species/search
GBIF API-->>GBIFUtils: Matching taxa
GBIFUtils-->>Dialog: Results list
Dialog-->>User: Display results
User->>Dialog: Select species
Dialog->>GBIFUtils: name_usage(key)
GBIFUtils->>GBIF API: GET /species/{key}
GBIF API-->>GBIFUtils: Taxonomy data
GBIFUtils-->>Dialog: Species details
Dialog-->>User: Display taxonomy
User->>Dialog: Click Fetch
Dialog->>GBIFFetchTask: Start background task
activate GBIFFetchTask
loop Paginated fetching
GBIFFetchTask->>GBIF API: GET /occurrence/search
GBIF API-->>GBIFFetchTask: Occurrence page
GBIFFetchTask->>Dialog: Progress update
end
GBIFFetchTask->>GBIFFetchTask: Create layer
GBIFFetchTask-->>Dialog: Task complete
deactivate GBIFFetchTask
Dialog->>QGIS: Add layer to project
QGIS-->>User: Display points
API Integration¶
GBIF Endpoints Used¶
| Endpoint | Method | Purpose |
|---|---|---|
/parser/name |
GET | Parse scientific names |
/species/search |
GET | Search for species |
/species/{key} |
GET | Get species details |
/occurrence/search |
GET | Search occurrences |
Query Parameters¶
Species Search:
Occurrence Search:
Response Handling¶
- JSON responses parsed to Python dicts
- Records filtered for valid coordinates
- Field names sanitized for QGIS compatibility
Layer Creation¶
When data is fetched:
- Create memory layer with Point geometry (EPSG:4326)
- Define attribute fields dynamically from response
- Add features for each occurrence with coordinates
- Add layer to QGIS project
Field Schema¶
Fields are created dynamically from GBIF response. Common fields:
| Field | Type | Description |
|---|---|---|
gbifID |
String | Unique identifier |
scientificName |
String | Full scientific name |
decimalLatitude |
String | Latitude |
decimalLongitude |
String | Longitude |
eventDate |
String | Observation date |
basisOfRecord |
String | Record type |
countryCode |
String | ISO country code |
institutionCode |
String | Data provider |
Field Name Sanitization:
- Alphanumeric characters and underscores only
- Maximum 60 characters
- All stored as String(255) for compatibility
Error Handling¶
Errors are handled at multiple levels:
graph TD
A[Network Error] --> B[GBIFNetworkError]
C[No Results] --> D[NoResultException]
E[Invalid Data] --> F[Filtered/Skipped]
B --> G[Status Label - Red]
D --> G
F --> H[Warning in Log]
G --> I[User Notification]
Error Types:
- Network errors - Connection timeouts, HTTP errors
- API errors - Invalid responses, rate limiting
- Data errors - Missing coordinates, invalid values
Display:
StatusLabelshows errors in red- Successful operations shown in green
- QGIS log contains detailed messages
Async Architecture¶
Why QgsTask?¶
- Non-blocking UI - User can interact during downloads
- Progress reporting - Real-time status updates
- Cancellation support - User can cancel long operations
- Thread safety - Proper handling of Qt signals/slots
- QGIS integration - Task manager visibility
Task Lifecycle¶
stateDiagram-v2
[*] --> Pending: Created
Pending --> Running: Started
Running --> Running: Progress update
Running --> Completed: Success
Running --> Failed: Error
Running --> Cancelled: User cancel
Completed --> [*]
Failed --> [*]
Cancelled --> [*]
Testing¶
The test suite covers:
- Plugin initialization
- GBIF API integration
- Dialog functionality
- Layer creation
- Error handling
- Async task behavior
Run tests with:
Extension Points¶
Adding New Data Sources¶
To add additional data sources:
- Create new utility module (similar to
gbifutils.py) - Create async fetcher (similar to
gbif_fetcher.py) - Add UI elements to dialog
- Update layer creation logic
Custom Styling¶
Modify the default layer styling by:
- Creating a QML style file
- Applying style after layer creation
- Or letting users apply their own styles