Compare commits

...

20 Commits

Author SHA1 Message Date
VALLONGOL
ebb2546d84 fatto refactoring con submodulo map-manager 2025-12-02 09:50:29 +01:00
VALLONGOL
e2f5fdd1d7 Feat: Add submodule 'external/python-map-manager' tracking branch 'master' 2025-12-02 09:10:52 +01:00
VALLONGOL
8288a01890 add 2025-11-12 13:17:43 +01:00
VALLONGOL
b0144d5597 test 2025-09-01 15:30:18 +02:00
61e0da302f Merge branch 'master' of http://192.168.100.10:3000/vallongol/GeoElevation 2025-09-01 15:28:00 +02:00
4d497f8abd update pyvbista 2025-09-01 15:27:39 +02:00
VALLONGOL
1646b655bd update example pyvista 2025-09-01 15:24:28 +02:00
VALLONGOL
044f72d20b add c++ source code for use hgt file into controlpanel 2025-08-08 13:06:00 +02:00
VALLONGOL
e438d0a1c8 add zoom in zoom out and pan function into map viewer 2025-05-14 14:07:13 +02:00
VALLONGOL
ba689010ea fix call tool like external module 2025-05-14 10:05:48 +02:00
VALLONGOL
46202a2829 fix rectangular view for dem files 2025-05-14 09:31:53 +02:00
VALLONGOL
3a5a3db6ad fix visualization map for point and area 2025-05-14 08:47:38 +02:00
VALLONGOL
aa7fb18626 fix view point on map 2025-05-14 07:55:57 +02:00
VALLONGOL
6925e8e323 ok view point on map 2025-05-13 16:01:44 +02:00
VALLONGOL
e7ca4855ab import corrected 2025-05-13 15:11:07 +02:00
VALLONGOL
d5561c6f8b add map viewer 2025-05-13 14:18:48 +02:00
VALLONGOL
f18a23df41 add command line and use in module function 2025-05-13 08:26:12 +02:00
VALLONGOL
8b5f620c66 update readme 2025-05-06 15:26:49 +02:00
VALLONGOL
0cf34d5664 add readme and requirements 2025-05-06 15:25:23 +02:00
VALLONGOL
aff8df7701 add _version 2025-05-06 09:32:29 +02:00
35 changed files with 3929 additions and 689 deletions

21
.gitignore vendored
View File

@ -1,10 +1,19 @@
.svn
map_elevation/
map_elevation/*
.hgt
.jpg
.png
elevation_cache/
__pycache__/
_version.py
_build/
_dist/
elevation_cache/*
__pycache__/*
_build/*
_dist/*
_req_packages/*
map_tile_cache/*
map_tile_cache_ge/*
geoelevation_dem_cache/*
build/*
elevation_data_cache_gui_fallback_critical/*
elevation_data_cache/*
debug_map_cache/*
test_cache_dir/*
.pytest_cache/*

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "external/python-map-manager"]
path = external/python-map-manager
url = http://192.168.100.10:3000/VALLONGOL/python-map-manager
branch = master

21
.vscode/launch.json vendored
View File

@ -1,7 +1,4 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
@ -9,6 +6,22 @@
"type": "debugpy",
"request": "launch",
"module": "geoelevation"
}
},
{
"name": "(Windows) Launch C++ CLI",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/build/elevation_cli.exe",
"args": [
// Argomenti a riga di comando per il test
"${workspaceFolder}/geoelevation_dem_cache/hgt_tiles",
"46.1705234",
"9.403258"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"console": "integratedTerminal" // Mostra l'output nel terminale integrato
},
]
}

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"cmake.sourceDirectory": "C:/src/____GitProjects/GeoElevation/source_c",
"files.associations": {
"string": "cpp"
}
}

25
GEMINI.md Normal file
View File

@ -0,0 +1,25 @@
Sono un ingegnere informatico che sviluppa principalmente in python e c++.
Quando mi proponi del codice ricordati di indicarmi le modifiche con il codice precedente, perchè le hai fatte, dove e come sono state fatte.
Utilizzando tutte le regole per scrivere in maniera migliore possibile codice in python.
Non dare nulla per scontato e spiega tutti i passi che segui nei tuoi ragionamenti.
Con me parla in italiano.
Come deve essere scritto il codice:
1) come standard di scrittura del codice Python, lo standard PEP8.
2) una istruzione per ogni riga di codice
3) nomi di funzioni, variabili, commenti, doc_string devono essere in inglese
4) codice più chiaro, ordinato, riutilizzabile
5) i commenti nel codice devono essere essenziali, stringati e chiari, non devono essere prolissi. dobbiamo cercare di mantenere il codice più pulito possibile, senza troppi fronzoli
6) Non indicare le modifche che fai come commento al codice, ma solo in linea generale in chat. il codice lascialo più pulito possibile
Per semplificare l'operazione di aggiornamento del codice:
1) se le modifiche che proponi interessano solo poche funzioni del modulo, allora indicami il contenuto di tutte le funzioni dove ci sono le modifiche.
2) se le modifiche impattano la maggior parte delle funzioni dello stesso modulo, allora ripeti per intero il codice del modulo senza omissioni.
3) se le modifiche che proponi interessano meno di 5 righe di una funzione, indicami quali sono le linee che cambiano e come modificarle
4) passami sempre un modulo alla volta e ti dico io quando passarmi il successivo, sempre in maniera completa e senza omissioni
Se vedi che il codice di un singolo modulo è più lungo di 1000 righe, prendi in considerazione il fatto di creare un nuovo modulo spostando quelle funzioni che sono omogenee per argomento in questo nuovo modulo e rendere più leggere il file che sta crescendo troppo.
Quando ti passo del codice da analizzare, cerca sempre di capirne le funzionalità e se hai da proporre dei miglioramenti o delle modifiche prima ne discuti con me e poi decidiamo se applicarlo oppure no.
Se noti che nel codice c'è qualcosa da migliorare, ne parli con me e poi vediamo se applicarlo oppure no, per evitare di mettere mano a funzioni che sono già state ottimizzate e funzionano come io voglio, e concentrarsi sulla risoluzione di problemi o l'introduzione di nuove funzioni.

321
README.md Normal file
View File

@ -0,0 +1,321 @@
## GeoElevation Tool
**(English Presentation Below)**
**IT** | [EN](#english-presentation)
GeoElevation Tool è un'applicazione versatile e una libreria Python progettata per interagire con i dati di elevazione digitale del terreno (DEM), principalmente utilizzando i dataset NASADEM HGT. Offre un'interfaccia grafica (GUI) completa per un facile utilizzo interattivo, un'interfaccia a riga di comando (CLI) per interrogazioni rapide e un'API di libreria per l'integrazione in altri progetti Python.
Permette agli utenti di:
* **Ottenere l'Elevazione Puntuale:** Sia tramite GUI, CLI o API, inserendo coordinate geografiche (latitudine e longitudine) per recuperare l'elevazione di un punto specifico.
* **Scaricare Aree DEM (via GUI):** Definire un'area geografica per scaricare automaticamente tutti i tile NASADEM HGT necessari.
* **Visualizzare Dati DEM (via GUI):**
* Mostrare un'immagine di anteprima del tile DEM.
* Generare e visualizzare una rappresentazione 3D interattiva del tile DEM, con opzioni per lo smoothing e l'interpolazione.
* Creare e mostrare un'immagine composita delle anteprime per un'area definita.
* **Esplorare Mappe e Dati di Elevazione Interattivamente (via GUI):** Utilizzare un visualizzatore mappa dedicato per navigare mappe online, ottenere elevazioni al click e visualizzare overlay informativi sui tile DEM.
Lo strumento gestisce l'autenticazione con NASA Earthdata Login (tramite file `.netrc`), il download dei dati, l'estrazione e la conversione. **Per informazioni dettagliate sull'utilizzo come libreria o da riga di comando, consultare le sezioni "Utilizzo da Riga di Comando" e "Utilizzo come Libreria Python".**
### Caratteristiche Principali
* Interfaccia utente grafica (GUI) intuitiva basata su Tkinter.
* Interfaccia a riga di comando (CLI) per il recupero rapido dell'elevazione puntuale.
* API Python per l'integrazione come libreria in altre applicazioni.
* Recupero dell'elevazione per coordinate specifiche.
* Download automatico e caching dei tile NASADEM HGT e delle immagini browse.
* Visualizzazione 2D di immagini di anteprima singole o composite (GUI).
* Visualizzazione 3D interattiva dei dati di elevazione DEM (GUI).
* **Visualizzatore mappa interattivo integrato per l'esplorazione geografica.**
* **Funzionalità interattive sulla mappa:**
* **Click Sinistro (Standard):** Ottiene e mostra coordinate ed elevazione nel punto cliccato, disegna un marker.
* **Ctrl + Click Sinistro (Pan):** Sposta la mappa centrata sul punto cliccato mantenendo il livello di zoom attuale.
* **Shift + Click Sinistro (Zoom In):** Centra la mappa sul punto cliccato e aumenta il livello di zoom.
* **Click Destro (Zoom Out):** Centra la mappa sul punto cliccato e diminuisce il livello di zoom.
* **Pannello informativo in tempo reale nella GUI** con coordinate del click (Decimali e DMS), elevazione e dettagli sull'area mappa visualizzata (dimensione, zoom).
* **Indicatore visibile di caricamento ("Map Loading" screen)** durante il fetch e lo stitching dei tile mappa.
* Gestione delle dipendenze opzionali (ad es. Map Viewer richiede OpenCV, Mercantile, Pyproj).
### Requisiti
* Python 3.8+
* Tkinter (solitamente incluso con Python)
* Requests
* NumPy
* Matplotlib
* Pillow (PIL Fork)
* Rasterio (per la lettura dei file HGT)
* SciPy (per lo smoothing e l'interpolazione nella visualizzazione 3D)
* **OpenCV-Python** (per il visualizzatore mappa interattivo)
* **Mercantile** (per i calcoli sui tile mappa Web Mercator nel visualizzatore)
* **Pyproj** (per i calcoli geografici nel visualizzatore e utility come deg<->dms)
L'installazione di **OpenCV-Python**, **Mercantile** e **Pyproj** è necessaria per abilitare la funzionalità completa del visualizzatore mappa interattivo. Se non installati, la GUI si avvierà ma i pulsanti e le sezioni correlate alla mappa saranno disabilitati.
È necessario configurare un file `.netrc` nella propria directory home con le credenziali per `urs.earthdata.nasa.gov` per scaricare i dati DEM protetti.
### Installazione
*(Aggiungere istruzioni specifiche per l'installazione del pacchetto se pubblicato, o basate su `requirements.txt` e cloni GitHub.)*
```bash
# Esempio ipotetico se pubblicato su PyPI con tutti gli extra
pip install geoelevation[full]
```
### Utilizzo
#### Avvio dell'Interfaccia Grafica (GUI)
Per avviare l'interfaccia grafica, eseguire il modulo dalla directory principale del progetto (la cartella che contiene `geoelevation/`):
```bash
python -m geoelevation
```
Oppure, se è stato installato come pacchetto:
```bash
geoelevation_gui # (o il nome dell'entry point script se definito in setup.py)
```
L'applicazione si avvierà mostrando la finestra principale.
#### Utilizzo da Riga di Comando (CLI)
È possibile ottenere l'elevazione per un punto specifico direttamente dalla riga di comando:
```bash
python -m geoelevation --lat <latitudine> --lon <longitudine> [--show-progress] [--verbose]
```
Argomenti:
* `--lat LATITUDINE`: Latitudine del punto (es. `45.0`). Richiesto per modalità CLI punto.
* `--lon LONGITUDINE`: Longitudine del punto (es. `7.0`). Richiesto per modalità CLI punto.
* `--gui`: (Opzionale) Forza l'avvio della GUI anche se sono specificati `--lat` e `--lon`.
* `--show-progress`: (Opzionale) Mostra una semplice finestra di dialogo di avanzamento durante il recupero dei dati (solo per CLI punto).
* `--verbose` o `-v`: (Opzionale) Abilita output di logging più dettagliato.
* `--help`: Mostra il messaggio di aiuto con tutti gli argomenti.
Esempio:
```bash
python -m geoelevation --lat 40.7128 --lon -74.0060 --show-progress
```Output atteso:
```
Elevation: <valore> meters
```
oppure `Elevation: NODATA` o `Elevation: UNAVAILABLE`.
#### Utilizzo come Libreria Python
Il package `geoelevation` può essere importato nei tuoi script Python per recuperare programmaticamente i dati di elevazione.
La funzione principale esposta è `get_point_elevation`:
```python
from geoelevation import get_point_elevation
import logging
# Opzionale: configura il logging per vedere i messaggi da geoelevation
# logging.basicConfig(level=logging.INFO)
latitude = 45.0
longitude = 7.0
try:
# Recupera l'elevazione, mostrando opzionalmente una finestra di progresso
elevation = get_point_elevation(
latitude,
longitude,
show_progress_dialog=True,
progress_dialog_custom_message="Sto cercando l'elevazione per te..." # Messaggio personalizzato
)
if elevation is None:
print(f"Impossibile recuperare l'elevazione per ({latitude}, {longitude}).")
elif isinstance(elevation, float) and math.isnan(elevation): # Controllo per NaN (Not a Number)
print(f"Il punto ({latitude}, {longitude}) si trova su un'area NoData.")
else:
print(f"L'elevazione a ({latitude}, {longitude}) è: {elevation:.2f} metri.")
except RuntimeError as e:
print(f"Errore durante l'inizializzazione della libreria GeoElevation: {e}")
except ValueError as e:
print(f"Errore di valore nei parametri: {e}")
except Exception as e:
print(f"Si è verificato un errore imprevisto: {e}")
```
**Dettagli della funzione `get_point_elevation`:**
* `get_point_elevation(latitude: float, longitude: float, show_progress_dialog: bool = False, progress_dialog_custom_message: str = "Retrieving elevation data...") -> Optional[float]`
* `latitude`, `longitude`: Coordinate geografiche del punto.
* `show_progress_dialog`: Se `True`, mostra una finestra di dialogo non bloccante con indicatore di avanzamento durante il recupero dei dati. Utile se la funzione viene chiamata da un'applicazione con una propria GUI per non sembrare bloccata. Richiede Tkinter.
* `progress_dialog_custom_message`: Permette di personalizzare il messaggio visualizzato nella finestra di dialogo di progresso.
* **Restituisce**: L'elevazione in metri come `float`, `float('nan')` se il punto è un'area NoData, o `None` se l'elevazione non può essere determinata, i dati sono non disponibili o si verifica un errore.
* **Solleva**: `RuntimeError` se le dipendenze critiche (es. Rasterio) non sono disponibili o se `ElevationManager` non può essere inizializzato correttamente. `ValueError` per input di coordinate non validi.
*(Aggiungere altre sezioni come Struttura Progetto, Screenshot, Limitazioni, Licenza, Contribuire come suggerito prima)*
---
---
## GeoElevation Tool
**(Italian Presentation Above)**
**EN** | [IT](#geoelevation-tool-1)
GeoElevation Tool is a versatile Python application and library designed for interacting with digital elevation model (DEM) data, primarily utilizing the NASADEM HGT datasets. It offers a comprehensive Graphical User Interface (GUI) for easy interactive use, a Command Line Interface (CLI) for quick queries, and a library API for integration into other Python projects.
It allows users to:
* **Get Point Elevation:** Via GUI, CLI, or API, by inputting geographic coordinates (latitude and longitude) to retrieve the elevation of a specific point.
* **Download DEM Areas (via GUI):** Define a geographical area to automatically download all necessary NASADEM HGT tiles.
* **Visualize DEM Data (via GUI):**
* Display a browse image (preview) of the DEM tile.
* Generate and display an interactive 3D representation of the DEM data, with options for smoothing and interpolation.
* Create and show a composite browse image for a defined area.
* **Explore Maps and Elevation Data Interactively (via GUI):** Use a dedicated map viewer to navigate online maps, get elevations on click, and visualize informative overlays on DEM tiles.
The tool handles authentication with NASA Earthdata Login (via a `.netrc` file), data downloading, extraction, and conversion. **For detailed information on using it as a library or from the command line, please refer to the "Command Line Interface (CLI) Usage" and "Using as a Python Library" sections.**
### Key Features
* Intuitive Tkinter-based Graphical User Interface (GUI).
* Command Line Interface (CLI) for quick point elevation retrieval.
* Python API for integration as a library into other applications.
* Elevation retrieval for specific coordinates.
* Automatic download and caching of NASADEM HGT tiles and browse images.
* 2D visualization of single browse images and composite images (GUI).
* Interactive 3D visualization of DEM elevation data (GUI).
* **Integrated interactive map viewer for geographic exploration.**
* **Interactive map functionalities:**
* **Left Click (Standard):** Gets and displays coordinates and elevation at the clicked point, draws a marker.
* **Ctrl + Left Click (Pan):** Centers the map on the clicked point while maintaining the current zoom level.
* **Shift + Left Click (Zoom In):** Centers the map on the clicked point and increases the zoom level.
* **Right Click (Zoom Out):** Centers the map on the clicked point and decreases the zoom level.
* **Real-time information panel in the GUI** with click coordinates (Decimal and DMS), elevation, and details about the displayed map area (size, zoom).
* **Visible loading indicator ("Map Loading" screen)** during map tile fetching and stitching.
* Handling of optional dependencies (e.g., Map Viewer requires OpenCV, Mercantile, Pyproj).
### Requirements
* Python 3.8+
* Tkinter (usually included with Python)
* Requests
* NumPy
* Matplotlib
* Pillow (PIL Fork)
* Rasterio (for reading HGT files)
* SciPy (for smoothing and interpolation in 3D visualization)
* **OpenCV-Python** (for the interactive map viewer)
* **Mercantile** (for Web Mercator map tile calculations in the viewer)
* **Pyproj** (for geographic calculations in the viewer and utilities like deg<->dms)
Installation of **OpenCV-Python**, **Mercantile**, and **Pyproj** is required to enable the full interactive map viewer functionality. If not installed, the GUI will start but related map buttons and sections will be disabled.
A `.netrc` file in your home directory must be configured with credentials for `urs.earthdata.nasa.gov` to download protected DEM data.
### Installation
*(Add specific instructions for package installation if published, or based on `requirements.txt` and GitHub clones.)*
```bash
# Hypothetical example if published on PyPI with all extras
pip install geoelevation[full]
```
### Usage
#### Launching the Graphical User Interface (GUI)
To start the GUI, run the module from the project's root directory (the folder containing the `geoelevation/` directory):
```bash
python -m geoelevation
```
Alternatively, if installed as a package:
```bash
geoelevation_gui # (or the entry point script name if defined in setup.py)
```
The application window will open.
#### Command Line Interface (CLI) Usage
You can get the elevation for a specific point directly from the command line:
```bash
python -m geoelevation --lat <latitude> --lon <longitude> [--show-progress] [--verbose] [--gui]
```
Arguments:
* `--lat LATITUDE`: Latitude of the point (e.g., `45.0`). Required for point CLI mode.
* `--lon LONGITUDE`: Longitude of the point (e.g., `7.0`). Required for point CLI mode.
* `--gui`: (Optional) Forces the GUI to launch, overriding `--lat` and `--lon` if present.
* `--show-progress`: (Optional) Displays a simple progress dialog window during data retrieval (point CLI only). Requires Tkinter.
* `--verbose` or `-v`: (Optional) Enables more detailed logging output.
* `--help`: Shows the help message with all arguments.
Example:
```bash
python -m geoelevation --lat 40.7128 --lon -74.0060 --show-progress
```
Expected output:
```
Elevation: <value> meters
```
or `Elevation: NODATA` or `Elevation: UNAVAILABLE`.
#### Using as a Python Library
The `geoelevation` package can be imported into your Python scripts to programmatically retrieve elevation data.
The primary function exposed is `get_point_elevation`:
```python
from geoelevation import get_point_elevation
import logging
import math # Needed for math.isnan
# Optional: configure logging to see messages from geoelevation
# logging.basicConfig(level=logging.INFO)
latitude = 45.0
longitude = 7.0
try:
# Retrieve elevation, optionally showing a progress window
elevation = get_point_elevation(
latitude,
longitude,
show_progress_dialog=True,
progress_dialog_custom_message="Fetching elevation data for you..." # Custom message
)
if elevation is None:
print(f"Could not retrieve elevation for ({latitude}, {longitude}).")
elif isinstance(elevation, float) and math.isnan(elevation): # Check for NaN (Not a Number)
print(f"The point ({latitude}, {longitude}) is on a NoData area.")
else:
print(f"The elevation at ({latitude}, {longitude}) is: {elevation:.2f} meters.")
except RuntimeError as e:
print(f"Error initializing GeoElevation library: {e}")
except ValueError as e:
print(f"Value error in coordinates: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
```
**`get_point_elevation` function details:**
* `get_point_elevation(latitude: float, longitude: float, show_progress_dialog: bool = False, progress_dialog_custom_message: str = "Retrieving elevation data...") -> Optional[float]`
* `latitude`, `longitude`: Geographic coordinates of the point.
* `show_progress_dialog`: If `True`, displays a non-blocking dialog window with a progress indicator during data retrieval. Useful if the function is called from an application with its own GUI to prevent it from appearing frozen. Requires Tkinter.
* `progress_dialog_custom_message`: Allows customization of the message displayed in the progress dialog.
* **Returns**: The elevation in meters as a `float`, `float('nan')` if the point is a NoData area, or `None` if the elevation cannot be determined, data is unavailable, or an error occurs.
* **Raises**: `RuntimeError` if critical dependencies (e.g., Rasterio) are unavailable or if the `ElevationManager` cannot be initialized correctly. `ValueError` for invalid coordinate inputs.
*(Add other sections like Project Structure, Screenshots, Limitations, License, Contributing as suggested before)*

View File

@ -1,155 +1,161 @@
**(English Version)**
**User Manual: Elevation Tool**
**User Manual: GeoElevation Tool (English - Updated)**
**1. Introduction**
Elevation Tool is a desktop application designed to retrieve elevation data (altitude) for specific geographic coordinates (latitude and longitude) on the Earth's surface. It utilizes NASADEM DEM (Digital Elevation Model) data, an improved version of SRTM data provided by NASA.
GeoElevation Tool is a desktop application designed to retrieve elevation data (altitude) for specific geographic coordinates (latitude and longitude) on the Earth's surface. It utilizes NASADEM Digital Elevation Model (DEM) data, an improved version of the SRTM data provided by NASA.
The application allows you to:
* Get the elevation for a single point.
* Automatically download the necessary DEM data "tiles" and their corresponding preview ("browse") images from remote servers.
* Automatically download necessary DEM data "tiles" and their corresponding "browse images" from remote servers.
* Maintain a local cache of downloaded data for fast offline access and to reduce repeated downloads.
* Pre-download data for an entire geographic area.
* Visualize browse images of individual tiles or a mosaic of the downloaded area.
* Visualize the elevation data of a single tile as an interactive 3D plot.
* View the browse images of individual tiles or a composite mosaic of the downloaded area (static visualizations).
* View the elevation data of a single tile as an interactive 3D plot (static visualization).
* **Explore elevation data and tiled maps interactively using a dedicated map viewer, with pan and zoom functionalities.**
**2. Prerequisites and Installation**
Before using the application, ensure you have the following:
* **Python:** A recent version of Python 3 installed on your system.
* **Python Libraries:** Install the required libraries using `pip`:
* **Python:** A recent version of Python 3.8 or higher installed on your system.
* **Python Libraries:** Install the required libraries using `pip`. For the full experience with the interactive map viewer and advanced visualizations, we recommend installing all of the following:
```bash
pip install requests rasterio Pillow matplotlib numpy netrc opencv-python
pip install requests rasterio Pillow matplotlib numpy netrc opencv-python mercantile pyproj scipy
```
* `requests`: For downloading data from the internet.
* `rasterio`: For reading DEM data (HGT files).
* `Pillow`: For processing preview images.
* `matplotlib`: For displaying 2D images and 3D plots.
* `numpy`: A fundamental dependency for `rasterio` and `matplotlib`.
* `netrc`: For handling authentication (often already present).
* `opencv-python`: (Currently unused in the final version, but potentially useful for future 2D processing).
* `requests`: For downloading data from the internet (Required).
* `rasterio`: For reading DEM data (HGT files) (Required for elevation functionality).
* `Pillow` (PIL): For image processing (browse, composites) and drawing overlays (Required for image visualizations and map viewer).
* `matplotlib`: For displaying 2D images and 3D plots (Required for static visualizations).
* `numpy`: Fundamental dependency for `rasterio`, `matplotlib`, `scipy`, and `opencv-python` (Required).
* `netrc`: For handling Earthdata authentication (often pre-installed, required for protected NASADEM downloads).
* `opencv-python`: For handling the interactive map viewer window and mouse events (Required for the map viewer).
* `mercantile`: For Web Mercator map tile calculations (Required for the map viewer).
* `pyproj`: For geodesic calculations (bounding boxes, distances) (Required for some map viewer utilities and helpful for deg<->dms).
* `scipy`: For advanced features in 3D plots (smoothing/interpolation) (Optional for 3D visualizations).
* **NASA Earthdata Login Account:** NASADEM data requires a free account. Register at: [https://urs.earthdata.nasa.gov/users/new](https://urs.earthdata.nasa.gov/users/new)
* **`.netrc` File for Authentication:** To allow the application to download protected data, you must create a file named `.netrc` in your system's home directory:
* **`.netrc` File for Authentication:** To allow the application to download protected data, you need to create a file named `.netrc` in your home directory:
* **Location:**
* Windows: `C:\Users\YourUsername\.netrc`
* Linux/macOS: `/home/yourusername/.netrc` (or `/Users/yourusername/.netrc`)
* **Content** (use a plain text editor):
* **Content** (use a simple text editor):
```
machine urs.earthdata.nasa.gov
login YOUR_EARTHDATA_USERNAME
password YOUR_EARTHDATA_PASSWORD
```
Replace `YOUR_EARTHDATA_USERNAME` and `YOUR_EARTHDATA_PASSWORD` with your actual credentials.
* **Permissions (Linux/macOS):** It's crucial to set the correct permissions for security. Open a terminal and type: `chmod 600 ~/.netrc`
* **Permissions (Linux/macOS):** It is crucial to set the correct permissions for security. Open a terminal and type: `chmod 600 ~/.netrc`
**3. Interface Overview**
**3. Graphical User Interface (GUI) Overview**
The application features a single main window divided into sections:
* **Get Elevation for Point:**
* `Latitude`: Field to enter latitude (decimal degrees, e.g., 45.0).
* `Longitude`: Field to enter longitude (decimal degrees, e.g., 7.0).
* `Get Elevation` (Button): Starts the elevation lookup for the specified point. Downloads necessary data (HGT and browse image) if not found in the cache.
* `Get Elevation` (Button): Initiates the elevation lookup for the specified point. Downloads necessary data (HGT and browse image) if not already in cache.
* `Result`: Label displaying the found elevation (in meters), a "nodata" message, or an error.
* `Show Browse Image (2D)` (Button): (Enabled after success) Opens a separate window (using Matplotlib) showing the preview image of the tile containing the point, with the name and source overlaid. Allows zooming and panning.
* `Show DEM Tile (3D)` (Button): (Enabled after success) Opens a separate window (using Matplotlib) showing an interactive 3D plot of the elevation data from the tile containing the point. Allows rotation, zooming, and panning.
* `Show Browse Image (2D)` (Button): (Enabled after successful Get Elevation) Opens a separate Matplotlib window showing the browse image for the tile containing the point.
* `Show DEM Tile (3D)` (Button): (Enabled after successful Get Elevation) Opens a separate Matplotlib window showing an interactive 3D plot of the DEM data for the tile.
* `View Point on Map` (Button): (Enabled after successful Get Elevation, requires Map Viewer libraries) Opens or brings the interactive map viewer window to the foreground, centered on the specified point with an automatic zoom level to show the surrounding DEM tile.
* **Pre-Download Tiles for Area:**
* `Min Lat`, `Max Lat`, `Min Lon`, `Max Lon`: Fields to define the boundaries of a rectangular area to download.
* `Min Lat`, `Max Lat`, `Min Lon`, `Max Lon`: Fields to define the geographic boundaries of a rectangular area to download.
* `Download Area Tiles` (Button): Starts the background download of all HGT data (extracted) and browse images for all tiles intersecting the specified area.
* `Show Area Browse Images (2D)` (Button): (Enabled after successful area download) Opens a separate window (using Matplotlib) showing a mosaic of the preview images for the tiles in the downloaded area, with a red grid and tile names overlaid. Allows zooming and panning.
* `Status`: Label showing the status of the area download (Idle, Downloading, Complete, Error).
* `Show Area Composite (2D)` (Button): (Enabled after successful Download Area Tiles and if browse images are available) Opens a separate Matplotlib window showing a mosaic composite of the downloaded area's browse images, with a grid and tile labels.
* `View Area on Map` (Button): (Enabled after successful Download Area Tiles, requires Map Viewer libraries) Opens or brings the interactive map viewer window to the foreground, displaying the area covered by the downloaded DEM tiles.
* `Status`: Label displaying the status of the area download.
* **Map Display Options:** (Visible only if Map Viewer libraries are available)
* `Map Display Scale`: Combobox to select a scaling factor for the displayed interactive map image. This affects the window size.
* **Map Info:** (Visible only if Map Viewer libraries are available)
* This section displays information related to the point clicked on the interactive map or the currently displayed area.
* `Latitude (Dec)`: Latitude of the clicked point in decimal degrees (copyable).
* `Longitude (Dec)`: Longitude of the clicked point in decimal degrees (copyable).
* `Latitude (DMS)`: Latitude of the clicked point in Degrees, Minutes, Seconds format (copyable).
* `Longitude (DMS)`: Longitude of the clicked point in Degrees, Minutes, Seconds format (copyable).
* `Elevation`: Estimated elevation at the clicked point on the map (in meters, "NoData", "Unavailable", or fetching status) (copyable).
* `Map Area Size`: Approximate geographic dimensions (Width x Height in km) of the area currently displayed in the interactive map window, including the current zoom level (copyable).
* **Interactive Map Viewer Window:**
* Opens when you click "View Point on Map" or "View Area on Map".
* Displays tiled web maps (like OpenStreetMap) with potential overlays (DEM tile borders, click marker).
* **"Map Loading" Screen:** A small progress window automatically appears and disappears above the main GUI during map tile fetching and stitching operations in the map viewer.
**4. Detailed Functionality**
* **Get Elevation:**
1. Enter valid Latitude and Longitude.
2. Click "Get Elevation".
3. The application determines the required 1x1 degree DEM tile.
4. Checks if the extracted HGT file exists in the local cache (`elevation_cache/hgt_tiles/`).
5. **If not exists:**
* Connects to the NASA server using `.netrc` credentials.
* Downloads the `.zip` file containing HGT data.
* Extracts the `.hgt` file into the local cache (`hgt_tiles`).
* Attempts to download the public `.jpg` browse image into the local cache (`browse_images`).
6. **If exists (or after download):**
* Opens the local `.hgt` file.
* Reads the elevation value for the exact coordinates.
* Displays the result in the "Result" label.
* Enables the "Show Browse Image (2D)" and "Show DEM Tile (3D)" buttons.
* **Download Area:**
1. Enter the geographic boundaries of the area.
2. Click "Download Area Tiles".
3. The application calculates which 1x1 degree tiles are needed.
4. For each required tile:
* Checks if the extracted HGT file already exists in the cache.
* If not, performs steps 5a-5c of "Get Elevation".
* Independently of whether the HGT existed, checks if the browse image exists in the cache and attempts to download it if missing.
5. The operation runs in the background to keep the interface responsive. Status is shown in the "Status" label.
6. Upon completion, a message summarizes how many tiles were processed and how many HGT files were actually downloaded/extracted. The "Show Area Browse Images (2D)" button is enabled.
* **2D Visualization (Browse Image - Single Tile):**
1. Successfully get the elevation for a point.
2. Click "Show Browse Image (2D)".
3. The application looks for the corresponding browse image in the cache (`browse_images`).
4. If found, it loads the image and adds the tile name and source as an overlay.
5. Displays the image in a new interactive Matplotlib window.
* **3D Visualization (DEM Tile):**
1. Successfully get the elevation for a point.
2. Click "Show DEM Tile (3D)".
3. The application looks for the corresponding HGT file in the cache (`hgt_tiles`).
4. If found, it reads the elevation data as a NumPy array.
5. Generates a 3D surface plot using Matplotlib (with subsampling for performance).
6. Displays the plot in a new interactive Matplotlib window.
* **2D Visualization (Area - Browse Mosaic):**
1. Successfully download an area using "Download Area Tiles".
2. Click "Show Area Browse Images (2D)".
3. The application looks for all browse images corresponding to the tiles in the requested area within the cache (`browse_images`).
4. Loads the found images.
5. Creates a composite image by "stitching" together the available images (uses gray placeholders if an image is missing).
6. Draws a red grid to separate tiles and adds the name label to each tile.
7. Displays the composite image in a new interactive Matplotlib window.
* **Get Elevation:** (Existing description unchanged - refers to manual input and the "Result" label)
* **Download Area:** (Existing description unchanged - refers to downloading HGT/browse data to cache)
* **Static Visualizations (Show Browse Image, Show DEM Tile, Show Area Composite):** (Existing descriptions unchanged - refer to non-interactive Matplotlib windows with standard Matplotlib pan/zoom tools)
* **Interactive Map Viewer (View on Map buttons):**
* Activates a separate window (based on OpenCV) that displays online map tiles.
* When opened via "View Point on Map", the map is centered on the point and zoomed automatically to show the corresponding DEM tile (with a small buffer). The boundary of the DEM HGT tile is drawn in red.
* When opened via "View Area on Map", the map displays the combined extent of all DEM tiles available (in cache) for the defined area. The boundary of the requested area (based on the input limits) is drawn in blue, and the boundaries of the individual available DEM HGT tiles are drawn in red with their labels.
* The map window responds to mouse events:
* **Left Click (Standard):**
1. Identifies the geographic coordinates of the clicked pixel.
2. Requests the elevation for that point from the ElevationManager.
3. Updates the "Map Info" fields in the main GUI with the coordinates (Decimal and DMS), the found elevation ("NoData", "Unavailable", or meter value), and the displayed area size.
4. Draws a temporary marker on the map at the clicked point.
5. During elevation fetching (which might involve downloads), the "Map Loading" screen may briefly appear.
* **Ctrl + Left Click (Pan):**
1. Identifies the geographic coordinates of the clicked pixel.
2. Updates the map view so the clicked point becomes the new center.
3. **The current zoom level is maintained.**
4. New map tiles are fetched and stitched for the new centered area.
5. Updates the "Map Info" fields (new center, elevation N/A, displayed area size including zoom).
6. During fetching/stitching, the "Map Loading" screen appears.
* **Right Click (Zoom Out + Recenter):**
1. Identifies the geographic coordinates of the clicked pixel.
2. Updates the map view so the clicked point becomes the new center.
3. **Decreases the zoom level by 1** (clamped to a minimum of zoom 0).
4. New map tiles are fetched and stitched for the new area and zoom level.
5. Updates the "Map Info" fields (new center, elevation N/A, displayed area size including zoom).
6. During fetching/stitching, the "Map Loading" screen appears.
* **Shift + Left Click (Zoom In + Recenter):**
1. Identifies the geographic coordinates of the clicked pixel.
2. Updates the map view so the clicked point becomes the new center.
3. **Increases the zoom level by 1** (clamped to the maximum zoom level supported by the map service, e.g., 19 for OSM).
4. New map tiles are fetched and stitched for the new area and zoom level.
5. Updates the "Map Info" fields (new center, elevation N/A, displayed area size including zoom).
6. During fetching/stitching, the "Map Loading" screen appears.
**5. Common Use Cases**
* **Finding the Altitude of a Specific Location:**
1. Launch the application.
2. Enter the location's latitude and longitude into the respective fields.
3. Click "Get Elevation".
4. Read the result in the "Result" label. The first access might take a few seconds for the download.
5. (Optional) Click "Show Browse Image (2D)" to see the general appearance of the area from satellite/radar.
6. (Optional) Click "Show DEM Tile (3D)" to explore the terrain relief in 3D.
1. Start the application.
2. Enter the latitude and longitude of the location in the respective fields.
3. Click "Get Elevation". Read the result in the "Result" label. (This ensures the DEM tile is available and activates buttons).
4. Click "View Point on Map". The interactive map window will open centered on the point.
5. (Optional) Click on any point on the map to get the precise elevation at that spot and see the coordinates and elevation update in the "Map Info" panel. A red marker will appear at the clicked location.
* **Checking Elevation Offline (Data Already Downloaded):**
1. Launch the application.
2. Enter the latitude and longitude of a point within an area whose data has already been downloaded (in the `elevation_cache`).
3. Click "Get Elevation".
4. The result will appear almost instantly as data is read locally.
1. Start the application.
2. Enter latitude and longitude of a point within an area whose DEM data has already been downloaded (into the `geoelevation_dem_cache`).
3. Click "Get Elevation". The result will appear almost instantly as HGT data is read locally.
4. Click "View Point on Map". The map will load using cached OpenStreetMap tiles (if available) or downloading if online, but elevation for a clicked point (step 5 from previous use case) will be fast if the DEM is cached.
* **Preparing Data for an Area Before Going Offline:**
1. Launch the application (with an active internet connection).
2. Identify the boundaries (min/max lat/lon) of the area of interest.
1. Start the application (with active internet connection).
2. Identify the boundaries (min/max lat/lon) of your area of interest.
3. Enter these boundaries in the "Pre-Download Tiles for Area" section.
4. Click "Download Area Tiles".
5. Wait for completion (monitor the "Status" label and console logs). This may take time depending on the area size and connection speed.
6. (Optional) Click "Show Area Browse Images (2D)" to visually confirm which tiles were downloaded.
7. You can now use the application offline to get quick elevations for any point within the downloaded area.
* **Visually Exploring a Single Tile:**
1. Enter the coordinates of any point within the tile you are interested in.
2. Click "Get Elevation" (even if you know the elevation, this ensures the tile is available and enables the buttons).
3. Click "Show Browse Image (2D)" or "Show DEM Tile (3D)".
* **Getting an Overview of a Region:**
4. Click "Download Area Tiles". Wait for completion (monitor the "Status" label and console for logs). This may take time depending on area size and connection speed.
5. (Optional) Click "Show Area Composite (2D)" to visually confirm which tiles were downloaded.
6. You can now use the application offline to get quick elevations for any point within the downloaded DEM area and view the area in "View Area on Map" (the tiled map might not load offline if its tiles aren't cached, but the DEM boundaries will appear).
* **Exploring an Area Visually on the Interactive Map:**
1. Ensure you have already downloaded the desired area using "Download Area Tiles".
2. Click "Show Area Browse Images (2D)".
3. Explore the composite image using Matplotlib's zoom/pan tools to see the tile layout and the general appearance of the region.
2. Click "View Area on Map". The interactive map window will open displaying the area of the downloaded DEM tiles. You will see the boundaries of the requested area (blue) and the boundaries of the individual available DEM tiles (red with labels).
3. Use **Ctrl + Left Click** to move (pan) around the map while keeping the same zoom level.
4. Use **Shift + Left Click** to zoom IN and recenter on a point.
5. Use **Right Click** to zoom OUT and recenter on a point.
6. Standard Left Click on a point to get its elevation and see the info in the "Map Info" panel.
**6. Notes and Troubleshooting**
* **Download Errors:**
* **401/403 (Unauthorized/Forbidden):** Problem with Earthdata Login credentials in your `.netrc` file. Check username, password, and the machine name `urs.earthdata.nasa.gov`.
* **404 (Not Found):** The requested file (HGT zip or browse jpg) does not exist at the specified URL. This could indicate a gap in NASA's data for that area or an issue with the URL construction (check URL constants in `elevation_manager.py`).
* **Network Errors/Timeouts:** Temporary connection issues. The script attempts a short retry, but persistent errors require a stable connection.
* **`.netrc` File:** Ensure it's in the correct directory and has restricted permissions (Linux/macOS).
* **Missing Dependencies:** If libraries (Rasterio, Pillow, Matplotlib) are missing, the application will show warning messages on startup and/or errors when trying to use associated features. Install missing libraries with `pip`.
* **3D Performance:** Rendering a full 3D tile can be intensive. Subsampling helps, but it might still be slow for very complex areas or less powerful machines.
* **Blocking Windows:** The Matplotlib visualization windows (2D and 3D) are run in separate processes to avoid blocking the main GUI, but the Matplotlib window itself will wait for user interaction or closure.
* **Logs:** Always check the output in the console/terminal where you launched the application for detailed log messages, especially if errors occur.
* **Download Errors:** (Existing description unchanged)
* **`.netrc` File:** (Existing description unchanged)
* **Missing Dependencies:** If crucial libraries for the map viewer (Pillow, OpenCV, Mercantile, Pyproj) are missing, the "View on Map" buttons will be disabled and/or error messages will appear on startup. If libraries for static visualizations (Matplotlib, SciPy, Pillow) are missing, the corresponding "Show..." buttons will be disabled. Install missing libraries with `pip`.
* **Performance:**
* Downloading very large areas can take time.
* Rendering a full tile in 3D can be computationally intensive.
* Fetching and stitching a large number of map tiles for the interactive viewer (at low zoom levels or for large view areas) can take time and memory; the "Map Loading" screen will appear in these cases.
* **Separate Windows:** Visualization windows (both static Matplotlib plots and the interactive OpenCV map) run in separate processes to avoid blocking the main GUI. They might appear in unexpected locations on your desktop.
* **"Map Loading" Window:** This window is set to always stay on top to help ensure you see it. Selecting the main GUI window or the map window will not hide it. On some operating systems or window managers, the "always on top" behavior might vary slightly.
* **Logs:** Always check the output in the console/terminal you used to launch the application for detailed log messages, especially in case of errors.

View File

@ -1,9 +1,8 @@
**Manuale Utente: Elevation Tool**
**Manuale Utente: GeoElevation Tool (Italiano - Aggiornato)**
**1. Introduzione**
Elevation Tool è un'applicazione desktop progettata per ottenere dati di elevazione (altitudine) per specifiche coordinate geografiche (latitudine e longitudine) sulla superficie terrestre. Utilizza i dati DEM (Digital Elevation Model) NASADEM, una versione migliorata dei dati SRTM forniti dalla NASA.
GeoElevation Tool è un'applicazione desktop progettata per ottenere dati di elevazione (altitudine) per specifiche coordinate geografiche (latitudine e longitudine) sulla superficie terrestre. Utilizza i dati DEM (Digital Elevation Model) NASADEM, una versione migliorata dei dati SRTM forniti dalla NASA.
L'applicazione permette di:
@ -11,25 +10,29 @@ L'applicazione permette di:
* Scaricare automaticamente i "tile" (mattonelle) di dati DEM necessari e le relative immagini di anteprima ("browse images") da server remoti.
* Mantenere una cache locale dei dati scaricati per un accesso offline veloce e per ridurre download ripetuti.
* Pre-scaricare i dati per un'intera area geografica.
* Visualizzare le immagini di anteprima dei singoli tile o un mosaico dell'area scaricata.
* Visualizzare i dati di elevazione di un singolo tile come plot 3D interattivo.
* Visualizzare le immagini di anteprima dei singoli tile o un mosaico dell'area scaricata (visualizzazioni statiche).
* Visualizzare i dati di elevazione di un singolo tile come plot 3D interattivo (visualizzazione statica).
* **Esplorare dati di elevazione e mappe tiled interattivamente tramite un visualizzatore mappa dedicato, con funzionalità di pan e zoom.**
**2. Prerequisiti e Installazione**
Prima di utilizzare l'applicazione, assicurati di avere quanto segue:
* **Python:** Una versione recente di Python 3 installata sul tuo sistema.
* **Librerie Python:** Installa le librerie necessarie tramite `pip`:
* **Python:** Una versione recente di Python 3.8 o superiore installata sul tuo sistema.
* **Librerie Python:** Installa le librerie necessarie tramite `pip`. Per l'esperienza completa con il visualizzatore mappa e le visualizzazioni avanzate, raccomandiamo di installare tutte le seguenti:
```bash
pip install requests rasterio Pillow matplotlib numpy netrc opencv-python
pip install requests rasterio Pillow matplotlib numpy netrc opencv-python mercantile pyproj scipy
```
* `requests`: Per scaricare dati da internet.
* `rasterio`: Per leggere i dati DEM (file HGT).
* `Pillow`: Per elaborare le immagini di anteprima.
* `matplotlib`: Per visualizzare le immagini 2D e i plot 3D.
* `numpy`: Dipendenza fondamentale per `rasterio` e `matplotlib`.
* `netrc`: Per gestire l'autenticazione (spesso già presente).
* `opencv-python`: (Attualmente non usata nella versione finale, ma potrebbe essere utile per future elaborazioni 2D).
* `requests`: Per scaricare dati da internet (richiesto).
* `rasterio`: Per leggere i dati DEM (file HGT) (richiesto per funzionalità elevazione).
* `Pillow` (PIL): Per elaborare immagini (browse, compositi) e disegnare overlay (richiesto per visualizzazioni immagine e map viewer).
* `matplotlib`: Per visualizzare immagini 2D e plot 3D (richiesto per visualizzazioni statiche).
* `numpy`: Dipendenza fondamentale per `rasterio`, `matplotlib`, `scipy` e `opencv-python` (richiesto).
* `netrc`: Per gestire l'autenticazione Earthdata (spesso già presente, richiesto per download NASADEM protetti).
* `opencv-python`: Per la gestione della finestra e degli eventi mouse del visualizzatore mappa interattivo (richiesto per il map viewer).
* `mercantile`: Per i calcoli sui tile mappa Web Mercator (richiesto per il map viewer).
* `pyproj`: Per calcoli geodetici (bounding boxes, distanze) (richiesto per alcune funzioni map viewer e utili come deg<->dms).
* `scipy`: Per funzionalità avanzate nei plot 3D (smoothing/interpolazione) (opzionale per visualizzazioni 3D).
* **Account NASA Earthdata Login:** I dati NASADEM richiedono un account gratuito. Registrati su: [https://urs.earthdata.nasa.gov/users/new](https://urs.earthdata.nasa.gov/users/new)
* **File `.netrc` per Autenticazione:** Per permettere all'applicazione di scaricare i dati protetti, devi creare un file chiamato `.netrc` nella tua directory home:
* **Posizione:**
@ -44,7 +47,7 @@ Prima di utilizzare l'applicazione, assicurati di avere quanto segue:
Sostituisci `TUA_USERNAME_EARTHDATA` e `TUA_PASSWORD_EARTHDATA` con le tue credenziali reali.
* **Permessi (Linux/macOS):** È cruciale impostare i permessi corretti per sicurezza. Apri un terminale e digita: `chmod 600 ~/.netrc`
**3. Panoramica dell'Interfaccia**
**3. Panoramica dell'Interfaccia Grafica (GUI)**
L'applicazione presenta una singola finestra principale divisa in sezioni:
@ -53,104 +56,107 @@ L'applicazione presenta una singola finestra principale divisa in sezioni:
* `Longitude`: Campo per inserire la longitudine (gradi decimali, es. 7.0).
* `Get Elevation` (Pulsante): Avvia la ricerca dell'elevazione per il punto specificato. Scarica i dati necessari (HGT e browse image) se non presenti in cache.
* `Result`: Etichetta che mostra l'elevazione trovata (in metri), un messaggio di "nodata" o un errore.
* `Show Browse Image (2D)` (Pulsante): (Abilitato dopo successo) Apre una finestra separata (usando Matplotlib) mostrando l'immagine di anteprima del tile contenente il punto, con nome e fonte sovraimpressi. Permette zoom e pan.
* `Show DEM Tile (3D)` (Pulsante): (Abilitato dopo successo) Apre una finestra separata (usando Matplotlib) mostrando un plot 3D interattivo dei dati di elevazione del tile contenente il punto. Permette rotazione, zoom e pan.
* `Show Browse Image (2D)` (Pulsante): (Abilitato dopo successo Get Elevation) Apre una finestra Matplotlib separata con l'immagine di anteprima del tile DEM contenente il punto.
* `Show DEM Tile (3D)` (Pulsante): (Abilitato dopo successo Get Elevation) Apre una finestra Matplotlib separata con un plot 3D interattivo dei dati DEM del tile.
* `View Point on Map` (Pulsante): (Abilitato dopo successo Get Elevation, richiede librerie Map Viewer) Apre o porta in primo piano la finestra del visualizzatore mappa interattivo, centrata sul punto specificato con uno zoom automatico per mostrare il tile DEM circostante.
* **Pre-Download Tiles for Area:**
* `Min Lat`, `Max Lat`, `Min Lon`, `Max Lon`: Campi per definire i limiti di un'area rettangolare da scaricare.
* `Min Lat`, `Max Lat`, `Min Lon`, `Max Lon`: Campi per definire i limiti di un'area rettangolare.
* `Download Area Tiles` (Pulsante): Avvia il download (in background) di tutti i dati HGT (estratti) e delle immagini browse per tutti i tile che intersecano l'area specificata.
* `Show Area Browse Images (2D)` (Pulsante): (Abilitato dopo successo download area) Apre una finestra separata (usando Matplotlib) mostrando un mosaico delle immagini di anteprima dei tile dell'area scaricata, con una griglia rossa e il nome di ogni tile. Permette zoom e pan.
* `Status`: Etichetta che mostra lo stato del download dell'area (Idle, Downloading, Complete, Error).
* `Show Area Composite (2D)` (Pulsante): (Abilitato dopo successo Download Area Tiles e se sono disponibili immagini browse) Apre una finestra Matplotlib separata con un mosaico delle immagini di anteprima dei tile scaricati, con una griglia e etichette.
* `View Area on Map` (Pulsante): (Abilitato dopo successo Download Area Tiles, richiede librerie Map Viewer) Apre o porta in primo piano la finestra del visualizzatore mappa interattivo, mostrando l'area coperta dai tile DEM scaricati.
* `Status`: Etichetta che mostra lo stato del download dell'area.
* **Map Display Options:** (Visibile solo se le librerie del Map Viewer sono disponibili)
* `Map Display Scale`: Combobox per selezionare un fattore di scala per l'immagine della mappa visualizzata nella finestra interattiva. Questo cambia la dimensione della finestra.
* **Map Info:** (Visibile solo se le librerie del Map Viewer sono disponibili)
* Questa sezione mostra informazioni relative al punto cliccato sulla mappa interattiva o all'area visualizzata.
* `Latitude (Dec)`: Latitudine del punto cliccato in gradi decimali (copiabile).
* `Longitude (Dec)`: Longitudine del punto cliccato in gradi decimali (copiabile).
* `Latitude (DMS)`: Latitudine del punto cliccato in formato Gradi, Minuti, Secondi (copiabile).
* `Longitude (DMS)`: Longitudine del punto cliccato in formato Gradi, Minuti, Secondi (copiabile).
* `Elevation`: Elevazione stimata nel punto cliccato sulla mappa (in metri, "NoData", "Unavailable", o stato di fetch) (copiabile).
* `Map Area Size`: Dimensioni approssimative (Larghezza x Altezza in km) dell'area geografica visualizzata nella finestra mappa interattiva, con indicato il livello di zoom corrente (copiabile).
* **Finestra Visualizzatore Mappa Interattivo:**
* Si apre quando si clicca su "View Point on Map" o "View Area on Map".
* Mostra mappe tiled (come OpenStreetMap) con potenziali overlay (bordi tile DEM, marker click).
* **Schermata "Map Loading":** Una piccola finestra di progresso appare e scompare automaticamente sopra la GUI principale durante il caricamento o l'aggiornamento dei tile nella finestra mappa.
**4. Funzionalità Dettagliate**
* **Ottenere Elevazione (Get Elevation):**
1. Inserisci Latitudine e Longitudine valide.
2. Clicca "Get Elevation".
3. L'applicazione determina il tile DEM 1x1 grado necessario.
4. Controlla se il file HGT estratto esiste nella cache locale (`elevation_cache/hgt_tiles/`).
5. **Se non esiste:**
* Si connette al server NASA usando le credenziali `.netrc`.
* Scarica il file `.zip` contenente i dati HGT.
* Estrae il file `.hgt` nella cache locale (`hgt_tiles`).
* Tenta di scaricare l'immagine browse `.jpg` pubblica nella cache locale (`browse_images`).
6. **Se esiste (o dopo download):**
* Apre il file `.hgt` locale.
* Legge il valore di elevazione per le coordinate esatte.
* Mostra il risultato nell'etichetta "Result".
* Abilita i pulsanti "Show Browse Image (2D)" e "Show DEM Tile (3D)".
* **Scaricare Area (Download Area Tiles):**
1. Inserisci i limiti geografici dell'area.
2. Clicca "Download Area Tiles".
3. L'applicazione calcola quali tile 1x1 grado sono necessari.
4. Per ogni tile necessario:
* Controlla se il file HGT estratto esiste già in cache.
* Se non esiste, esegue i passi 5a-5c di "Ottenere Elevazione".
* Indipendentemente dal fatto che l'HGT esistesse già, controlla se l'immagine browse esiste in cache e tenta di scaricarla se manca.
5. L'operazione avviene in background per non bloccare l'interfaccia. Lo stato è mostrato nell'etichetta "Status".
6. Al termine, un messaggio riassume quanti tile sono stati processati e quanti file HGT sono stati effettivamente scaricati/estratti. Il pulsante "Show Area Browse Images (2D)" viene abilitato.
* **Visualizzazione 2D (Browse Image - Singolo Tile):**
1. Ottieni l'elevazione per un punto con successo.
2. Clicca "Show Browse Image (2D)".
3. L'applicazione cerca l'immagine browse corrispondente nella cache (`browse_images`).
4. Se trovata, carica l'immagine, aggiunge il nome del tile e la fonte come overlay.
5. Mostra l'immagine in una nuova finestra Matplotlib interattiva.
* **Visualizzazione 3D (DEM Tile):**
1. Ottieni l'elevazione per un punto con successo.
2. Clicca "Show DEM Tile (3D)".
3. L'applicazione cerca il file HGT corrispondente nella cache (`hgt_tiles`).
4. Se trovato, legge i dati di elevazione come array NumPy.
5. Genera un plot 3D della superficie usando Matplotlib (con sottocampionamento per performance).
6. Mostra il plot in una nuova finestra Matplotlib interattiva.
* **Visualizzazione 2D (Area - Mosaico Browse):**
1. Scarica un'area con successo usando "Download Area Tiles".
2. Clicca "Show Area Browse Images (2D)".
3. L'applicazione cerca tutte le immagini browse corrispondenti ai tile dell'area richiesta nella cache (`browse_images`).
4. Carica le immagini trovate.
5. Crea un'immagine composita "cucendo" insieme le immagini disponibili (usa placeholder grigi se un'immagine manca).
6. Disegna una griglia rossa per separare i tile e aggiunge l'etichetta con il nome a ciascun tile.
7. Mostra l'immagine composita in una nuova finestra Matplotlib interattiva.
* **Ottenere Elevazione (Get Elevation):** (Descrizione esistente invariata - si riferisce all'input manuale e al risultato nell'etichetta "Result")
* **Scaricare Area (Download Area Tiles):** (Descrizione esistente invariata - si riferisce al download dei dati DEM HGT/browse nella cache)
* **Visualizzazioni Statiche (Show Browse Image, Show DEM Tile, Show Area Composite):** (Descrizioni esistenti invariate - si riferiscono alle finestre Matplotlib non interattive per pan/zoom con strumenti standard di Matplotlib)
* **Visualizzatore Mappa Interattivo (View on Map buttons):**
* Attiva una finestra separata (basata su OpenCV) che visualizza mappe online.
* Quando aperto tramite "View Point on Map", la mappa è centrata sul punto e zoomata automaticamente per mostrare il tile DEM corrispondente (con un leggero buffer). Il bordo del tile DEM HGT è disegnato in rosso.
* Quando aperto tramite "View Area on Map", la mappa mostra l'intera estensione combinata di tutti i tile DEM disponibili (nella cache) per l'area definita. Il bordo dell'area richiesta (secondo i limiti inseriti) è disegnato in blu, e i bordi dei singoli tile DEM HGT disponibili sono disegnati in rosso con le loro etichette.
* La finestra mappa risponde agli eventi mouse:
* **Click Sinistro (Standard):**
1. Identifica le coordinate geografiche del punto cliccato.
2. Richiede all'ElevationManager l'elevazione per quel punto.
3. Aggiorna i campi "Map Info" nella GUI principale con le coordinate (Decimali e DMS), l'elevazione trovata ("NoData", "Unavailable", o valore in metri), e la dimensione dell'area visualizzata.
4. Disegna un marker sulla mappa nel punto cliccato.
5. Durante il fetch dell'elevazione (che potrebbe richiedere download), la schermata "Map Loading" potrebbe apparire brevemente.
* **Ctrl + Click Sinistro (Pan):**
1. Identifica le coordinate geografiche del punto cliccato.
2. Aggiorna la vista mappa in modo che il punto cliccato diventi il nuovo centro.
3. **Il livello di zoom corrente viene mantenuto.**
4. Vengono fetched e stitched i nuovi tile mappa necessari per la nuova area centrata.
5. Aggiorna i campi "Map Info" (nuovo centro, elevazione N/A, dimensione area visualizzata e zoom).
6. Durante il fetch/stitching, appare la schermata "Map Loading".
* **Click Destro (Zoom Out + Recenter):**
1. Identifica le coordinate geografiche del punto cliccato.
2. Aggiorna la vista mappa in modo che il punto cliccato diventi il nuovo centro.
3. **Diminuisce il livello di zoom di 1** (con limite minimo a zoom 0).
4. Vengono fetched e stitched i nuovi tile mappa necessari per la nuova area e il nuovo zoom.
5. Aggiorna i campi "Map Info" (nuovo centro, elevazione N/A, dimensione area visualizzata e zoom).
6. Durante il fetch/stitching, appare la schermata "Map Loading".
* **Shift + Click Sinistro (Zoom In + Recenter):**
1. Identifica le coordinate geografiche del punto cliccato.
2. Aggiorna la vista mappa in modo che il punto cliccato diventi il nuovo centro.
3. **Aumenta il livello di zoom di 1** (con limite massimo al zoom supportato dal servizio mappa, es. 19 per OSM).
4. Vengono fetched e stitched i nuovi tile mappa necessari per la nuova area e il nuovo zoom.
5. Aggiorna i campi "Map Info" (nuovo centro, elevazione N/A, dimensione area visualizzata e zoom).
6. Durante il fetch/stitching, appare la schermata "Map Loading".
**5. Casi d'Uso Comuni**
* **Trovare l'Altitudine di un Luogo Specifico:**
1. Avvia l'applicazione.
2. Inserisci la latitudine e la longitudine del luogo nei campi appositi.
3. Clicca "Get Elevation".
4. Leggi il risultato nell'etichetta "Result". Il primo accesso potrebbe richiedere qualche secondo per il download.
5. (Opzionale) Clicca "Show Browse Image (2D)" per vedere l'aspetto generale dell'area dal satellite/radar.
6. (Opzionale) Clicca "Show DEM Tile (3D)" per esplorare il rilievo del terreno in 3D.
3. Clicca "Get Elevation". Leggi il risultato nell'etichetta "Result". (Questo garantisce che il tile DEM sia disponibile e attiva i pulsanti).
4. Clicca "View Point on Map". Si aprirà la finestra mappa interattiva centrata sul punto.
5. (Opzionale) Clicca su un punto qualsiasi della mappa per ottenere l'elevazione precisa in quel punto e vedere le coordinate e l'elevazione aggiornarsi nel pannello "Map Info". Un marker rosso apparirà nel punto cliccato.
* **Verificare l'Elevazione Offline (Dati Già Scaricati):**
1. Avvia l'applicazione.
2. Inserisci latitudine e longitudine di un punto all'interno di un'area i cui dati sono già stati scaricati (nella cache `elevation_cache`).
3. Clicca "Get Elevation".
4. Il risultato apparirà quasi istantaneamente perché i dati vengono letti localmente.
2. Inserisci latitudine e longitudine di un punto all'interno di un'area i cui dati DEM sono già stati scaricati (nella cache `geoelevation_dem_cache`).
3. Clicca "Get Elevation". Il risultato apparirà quasi istantaneamente perché i dati HGT vengono letti localmente.
4. Clicca "View Point on Map". La mappa si caricherà utilizzando i tile mappa OpenStreetMap dalla cache locale (se disponibili) o scaricandoli se online, ma l'elevazione per il punto cliccato (passo 5 del caso d'uso precedente) sarà rapida se il DEM è in cache.
* **Preparare Dati per un'Area Prima di Andare Offline:**
1. Avvia l'applicazione (con connessione internet attiva).
2. Identifica i limiti (min/max lat/lon) dell'area di interesse.
3. Inserisci questi limiti nella sezione "Pre-Download Tiles for Area".
4. Clicca "Download Area Tiles".
5. Attendi il completamento (monitora l'etichetta "Status" e la console per i log). Potrebbe richiedere tempo a seconda della dimensione dell'area e della velocità di connessione.
6. (Opzionale) Clicca "Show Area Browse Images (2D)" per confermare visivamente quali tile sono stati scaricati.
7. Ora puoi usare l'applicazione offline per ottenere elevazioni rapide per qualsiasi punto all'interno dell'area scaricata.
* **Esplorare Visivamente un Singolo Tile:**
1. Inserisci le coordinate di un punto qualsiasi all'interno del tile che ti interessa.
2. Clicca "Get Elevation" (anche se conosci già l'elevazione, questo assicura che il tile sia disponibile e i pulsanti si attivino).
3. Clicca "Show Browse Image (2D)" o "Show DEM Tile (3D)".
* **Ottenere una Visione d'Insieme di un'Area:**
4. Clicca "Download Area Tiles". Attendi il completamento (monitora l'etichetta "Status" e la console).
5. Ora puoi usare l'applicazione offline per ottenere elevazioni rapide per qualsiasi punto all'interno dell'area DEM scaricata e visualizzare l'area in "View Area on Map" (la mappa tiled potrebbe non caricarsi offline se i suoi tile non sono in cache, ma i bordi DEM appariranno).
* **Esplorare Visivamente un'Area sulla Mappa Interattiva:**
1. Assicurati di aver già scaricato l'area desiderata usando "Download Area Tiles".
2. Clicca "Show Area Browse Images (2D)".
3. Esplora l'immagine composita usando gli strumenti di zoom/pan di Matplotlib per vedere la disposizione dei tile e l'aspetto generale della regione.
2. Clicca "View Area on Map". Si aprirà la finestra mappa interattiva mostrando l'area dei tile DEM scaricati. Vedrai i bordi dell'area richiesta (blu) e i bordi dei singoli tile DEM disponibili (rossi con etichette).
3. Usa **Ctrl + Click Sinistro** per spostarti (pan) sulla mappa mantenendo lo zoom.
4. Usa **Shift + Click Sinistro** per zoommare IN e ricentrare su un punto.
5. Usa **Click Destro** per zoommare OUT e ricentrare su un punto.
6. Clicca Sinistro standard su un punto per ottenere l'elevazione e vedere le info nel pannello "Map Info".
**6. Note e Risoluzione Problemi**
* **Errori di Download:**
* **401/403 (Unauthorized/Forbidden):** Problema con le credenziali Earthdata Login nel file `.netrc`. Verifica username, password e il nome `machine urs.earthdata.nasa.gov`.
* **404 (Not Found):** Il file richiesto (HGT zip o browse jpg) non esiste all'URL specificato. Potrebbe indicare una lacuna nei dati NASA per quell'area o un problema nella costruzione dell'URL (controlla le costanti URL nel codice `elevation_manager.py`).
* **Errori di Rete/Timeout:** Problemi temporanei di connessione. Lo script tenta un breve retry, ma errori persistenti richiedono una connessione stabile.
* **File `.netrc`:** Assicurati che sia nella directory corretta e che i permessi siano restrittivi (Linux/macOS).
* **Dipendenze Mancanti:** Se mancano librerie (Rasterio, Pillow, Matplotlib), l'applicazione mostrerà messaggi di avviso all'avvio e/o errori quando tenti di usare le funzionalità associate. Installa le librerie mancanti con `pip`.
* **Performance 3D:** Il rendering 3D di un tile completo può essere intensivo. Il sottocampionamento (`subsample`) aiuta, ma per aree molto complesse potrebbe comunque essere lento.
* **Finestre Bloccanti:** Le finestre di visualizzazione Matplotlib (sia 2D che 3D) sono eseguite in processi separati per non bloccare la GUI principale, ma la finestra Matplotlib stessa attenderà la chiusura da parte dell'utente.
* **Log:** Controlla sempre l'output nella console/terminale da cui hai avviato l'applicazione per messaggi di log dettagliati, specialmente in caso di errori.
* **Errori di Download:** (Descrizione esistente invariata)
* **File `.netrc`:** (Descrizione esistente invariata)
* **Dipendenze Mancanti:** Se mancano librerie cruciali per il visualizzatore mappa (Pillow, OpenCV, Mercantile, Pyproj), i pulsanti "View on Map" saranno disabilitati e/o appariranno messaggi di errore all'avvio. Se mancano librerie per le visualizzazioni statiche (Matplotlib, SciPy, Pillow), i relativi pulsanti "Show..." saranno disabilitati. Installa le librerie mancanti con `pip`.
* **Performance:**
* Il download di aree molto grandi può richiedere tempo.
* Il rendering 3D di un tile completo può essere intensivo.
* Il fetch e lo stitching di un gran numero di tile mappa per la visualizzazione interattiva (a livelli di zoom bassi o per aree estese) può richiedere tempo e memoria; la schermata "Map Loading" apparirà in questi casi.
* **Finestre Sepate:** Le finestre di visualizzazione (sia Matplotlib statiche che OpenCV interattiva) sono eseguite in processi separati per non bloccare la GUI principale. Potrebbero apparire in posizioni inaspettate sul tuo desktop.
* **Finestra "Map Loading":** Questa finestra è impostata per rimanere sempre in primo piano per garantire che tu la veda. Selezionando la finestra principale della GUI o la finestra mappa non la nasconderà. Su alcuni sistemi operativi o gestori di finestre, il comportamento "always on top" potrebbe variare leggermente.
* **Log:** Controlla sempre l'output nella console/terminale da cui hai avviato l'applicazione per messaggi di log dettagliati.
---

149
doc/map_manager.md Normal file
View File

@ -0,0 +1,149 @@
Ecco il piano operativo completo per il refactoring e la creazione del modulo `python-map-manager`. Questo documento è strutturato per essere inserito direttamente nella documentazione tecnica del progetto.
---
# Piano Operativo: Refactoring e Creazione Modulo `python-map-manager`
## 1. Obiettivo del Refactoring
L'obiettivo primario è disaccoppiare la logica di gestione, recupero e visualizzazione delle mappe geografiche (attualmente integrata nell'applicazione `geoelevation`) per creare un componente software autonomo, riutilizzabile e manutenibile separatamente.
Questo nuovo componente sarà gestito come **Git Submodule** e denominato **`python-map-manager`**.
### Obiettivi Specifici
1. **Indipendenza**: Il modulo non deve avere dipendenze dalla logica di business dell'applicazione ospite (es. non deve conoscere `ElevationManager`).
2. **Modularità**: Separazione netta tra logica di elaborazione (`Engine`) e logica di visualizzazione (`Visualizer`).
3. **Testabilità**: Inclusione di un tool di debug integrato (`debug_tool.py`) per lo sviluppo e il test isolato delle funzionalità.
4. **Interfaccia Chiara**: Esposizione di API semplici per richiedere immagini di mappe basate su aree, punti o raggi.
---
## 2. Architettura del Nuovo Modulo
Il modulo `python-map-manager` esporrà due componenti principali:
### A. `MapEngine` (Logica Backend)
È il cervello del modulo. Non ha interfaccia grafica.
* **Responsabilità**:
* Gestione dei provider di mappe (es. OpenStreetMap).
* Gestione della cache su disco (download, salvataggio, recupero).
* Calcoli matematici (conversioni coordinate Geo <-> Pixel, calcolo Bounding Box).
* Stitching (unione) delle tile per formare un'unica immagine PIL.
* **Funzionalità Chiave**:
* `get_image_for_area(bbox, max_size)`: Restituisce un'immagine ottimizzata per coprire un'area.
* `get_image_for_point(lat, lon, zoom, size)`: Restituisce un'immagine centrata su un punto.
### B. `MapVisualizer` (Interfaccia Frontend - Opzionale)
È il componente di visualizzazione interattiva (basato su OpenCV).
* **Responsabilità**:
* Apertura e gestione della finestra grafica.
* Gestione dell'input utente (Mouse, Zoom, Pan).
* Rendering dell'immagine fornita dall'`Engine`.
* **Disaccoppiamento**:
* Invece di chiamare funzioni esterne, il Visualizer emette **Eventi** (tramite callback) quando l'utente interagisce (es. `on_map_click`, `on_area_selected`). L'applicazione ospite si sottoscrive a questi eventi.
---
## 3. Struttura del Repository `python-map-manager`
```text
python-map-manager/
├── map_manager/ # Package Python principale
│ ├── __init__.py # Espone MapEngine e MapVisualizer
│ ├── engine.py # Classe MapEngine (Facade logica)
│ ├── visualizer.py # Classe MapVisualizer (Gestione Window/OpenCV)
│ ├── tile_manager.py # Gestione download e cache (ex map_manager.py)
│ ├── services.py # Definizioni Provider Mappe (ex map_services.py)
│ ├── utils.py # Calcoli geografici puri (ex map_utils.py)
│ └── drawing.py # Funzioni di disegno su PIL (ex map_drawing.py)
├── debug_tool.py # Tool CLI/GUI per testare il modulo isolatamente
├── requirements.txt # Dipendenze (requests, Pillow, opencv-python, mercantile, pyproj)
└── README.md # Documentazione API
```
---
## 4. Fasi di Implementazione
### Fase 1: Setup dell'Ambiente e Migrazione File "Puri"
In questa fase si crea la struttura base e si migrano le librerie di utilità che non richiedono refactoring logico.
1. Creare la cartella `python-map-manager` e inizializzare git.
2. Creare la struttura cartelle `map_manager/`.
3. **Migrazione Diretta**:
* Copiare `geoelevation/map_viewer/map_services.py` -> `map_manager/services.py`.
* Copiare `geoelevation/map_viewer/map_utils.py` -> `map_manager/utils.py`.
* Copiare `geoelevation/map_viewer/map_drawing.py` -> `map_manager/drawing.py`.
* Copiare `geoelevation/map_viewer/map_manager.py` -> `map_manager/tile_manager.py` (Rinominato per chiarezza).
4. **Normalizzazione Import**: Aggiornare gli import interni ai file copiati per puntare ai nuovi percorsi relativi (es. `from .services import ...` invece di `from .map_services import ...`).
### Fase 2: Implementazione di `MapEngine` (`engine.py`)
In questa fase si astrae la logica di calcolo e recupero immagini.
1. Creare `map_manager/engine.py`.
2. Definire la classe `MapEngine`.
3. Implementare il metodo `__init__` per configurare il `MapTileManager` e la cache.
4. Implementare `get_image_for_area`:
* Deve accettare coordinate `(min_lat, min_lon, max_lat, max_lon)`.
* Deve usare `utils.py` per calcolare lo zoom ottimale in base alle dimensioni pixel richieste.
* Deve chiamare `tile_manager.stitch_map_image`.
5. Implementare `get_image_for_point`:
* Accetta centro e livello di zoom.
* Calcola il Bounding Box necessario usando `utils.py`.
* Richiede lo stitching.
### Fase 3: Implementazione di `MapVisualizer` (`visualizer.py`)
In questa fase si crea il gestore della finestra, rimuovendo ogni logica di business specifica di `geoelevation`.
1. Creare `map_manager/visualizer.py`.
2. Definire la classe `MapVisualizer` che accetta un'istanza di `MapEngine`.
3. Estrarre la logica OpenCV da `geo_map_viewer.py` e `map_display.py`.
4. Implementare il loop di gestione eventi mouse (`cv2.setMouseCallback`).
5. **Refactoring Eventi**:
* Definire una proprietà `callback_on_click` (funzione che accetta lat, lon).
* Quando avviene un click, usare `engine` o `utils` per convertire Pixel -> Lat/Lon.
* Invocare `self.callback_on_click(lat, lon)` invece di chiamare `elevation_manager`.
### Fase 4: Creazione del `debug_tool.py`
Uno strumento essenziale per garantire che il modulo funzioni "out of the box".
1. Creare `debug_tool.py` nella root del repository.
2. Lo script deve:
* Istanziare `MapEngine` (con una cache temporanea o di debug).
* Istanziare `MapVisualizer`.
* Definire una funzione dummy: `def on_click(lat, lon): print(f"Clicked: {lat}, {lon}")`.
* Collegare la funzione al visualizzatore.
* Avviare la mappa su coordinate di default (es. Roma).
3. Questo tool servirà per verificare lo zoom, il pan e la correttezza del download delle tile.
### Fase 5: Integrazione nell'Applicazione Principale
Una volta che il submodule è stabile e pushato sul repository remoto.
1. In `geoelevation`, rimuovere la cartella `map_viewer` esistente.
2. Aggiungere il submodule:
```bash
git submodule add -b master <URL_REPO> external/python-map-manager
```
3. Configurare i path in `geoelevation/__init__.py` (o file di setup path dedicato) per includere il submodule.
4. Modificare `geoelevation/process_targets.py` (o dove risiede il processo mappa):
* Importare `MapEngine` e `MapVisualizer` dal submodule.
* Nel processo dedicato alla mappa, definire la funzione di callback reale che interroga `ElevationManager`.
* Passare questa callback al `MapVisualizer`.
---
## 5. Specifiche Tecniche e Standard
* **PEP8**: Tutto il codice deve seguire rigorosamente lo standard PEP8.
* **Type Hinting**: Ogni funzione deve avere le annotazioni di tipo (`-> Optional[Image.Image]`, ecc.).
* **Docstrings**: Ogni classe e metodo pubblico deve avere docstring in Inglese.
* **Dipendenze**:
* Non usare `try-except ImportError` per nascondere dipendenze mancanti all'interno del modulo. Se `MapVisualizer` richiede OpenCV, l'import deve fallire esplicitamente se manca, o essere gestito a livello di `__init__.py` per esporre le funzionalità disponibili.
* Il file `requirements.txt` deve elencare le versioni minime testate.
## 6. Risultato Atteso
Al termine di questo processo, avremo:
1. Un repository `python-map-manager` autonomo.
2. La possibilità di sviluppare e migliorare la gestione mappe lanciando solo `python debug_tool.py`.
3. L'applicazione `geoelevation` più leggera, che delega tutta la complessità cartografica al modulo esterno, mantenendo solo la logica di "cosa fare quando l'utente clicca un punto" (ovvero chiedere l'elevazione).

1
external/python-map-manager vendored Submodule

@ -0,0 +1 @@
Subproject commit 663a87a24ba7375316f0cd81ca658a62c3607ce4

View File

@ -0,0 +1,453 @@
# geoelevation/__init__.py
"""
GeoElevation Package.
Provides functionalities to retrieve elevation data for geographic coordinates,
a Graphical User Interface (GUI) for interactive exploration, a Command Line
Interface (CLI) for quick point elevation lookups, and an API for integration
into other Python applications.
Key public interfaces:
- get_point_elevation(): Function to retrieve elevation for a lat/lon point.
- run_gui_application(): Function to launch the main GUI.
- GEOELEVATION_DEM_CACHE_DEFAULT: Default cache path for DEM files.
- _LibraryProgressWindow: Internal utility for showing progress (exposed for typing/advanced use).
"""
# Standard library imports
import os
import sys
import tkinter as tk # For _LibraryProgressWindow and parts of run_gui_application
from tkinter import ttk # For _LibraryProgressWindow
import threading
import logging
import math # For math.isnan in get_point_elevation
from typing import Optional, Tuple, Any # Added Any
# --- Configure Logging for Library Use ---
# This setup ensures that the library itself doesn't output logs unless the
# consuming application configures handlers for the 'geoelevation' logger.
library_logger = logging.getLogger(__name__) # __name__ will be 'geoelevation'
if not library_logger.hasHandlers():
library_logger.addHandler(logging.NullHandler())
# By default, the library logger will inherit the level of the root logger,
# or it can be explicitly set, e.g., library_logger.setLevel(logging.WARNING).
# For now, let the consuming application control the level.
# --- Import Core Components and Configuration ---
# These imports make key classes and constants available and are used by
# the public API functions defined in this __init__.py.
## Ensure external `python-map-manager` package (workspace submodule) is importable
# This helps top-level modules (e.g. elevation_gui) import `map_manager` early.
try:
_pkg_root = os.path.dirname(__file__)
_candidate = os.path.normpath(os.path.join(_pkg_root, "..", "external", "python-map-manager"))
if os.path.isdir(_candidate) and _candidate not in sys.path:
# Prepend so it takes precedence over other installations
sys.path.insert(0, _candidate)
library_logger.debug(f"Added external python-map-manager to sys.path: {_candidate}")
except Exception:
# Non-fatal: if path manipulation fails, imports will raise as before and fallback handles it.
pass
try:
from .elevation_manager import ElevationManager
from .elevation_manager import RASTERIO_AVAILABLE # Critical dependency check
# MODIFIED: Import GEOELEVATION_DEM_CACHE_DEFAULT directly from .config
# WHY: Centralizes configuration, avoids potential import timing issues.
# HOW: Changed from defining a fallback here to importing from the config module.
from .config import GEOELEVATION_DEM_CACHE_DEFAULT
except ImportError as e_core_pkg_import:
# This signifies a critical failure if core components cannot be imported.
library_logger.critical(
f"Failed to import core components (ElevationManager, RASTERIO_AVAILABLE, or config) "
f"for GeoElevation package: {e_core_pkg_import}. The library will likely not function.",
exc_info=True
)
# Define fallback values/classes so that an application attempting to import
# 'geoelevation' doesn't crash immediately on this __init__.py, but can
# gracefully detect that the library is non-functional.
class ElevationManager: # type: ignore # Dummy class
def __init__(self, *args: Any, **kwargs: Any) -> None: pass
def get_elevation(self, *args: Any, **kwargs: Any) -> None: return None
RASTERIO_AVAILABLE = False
GEOELEVATION_DEM_CACHE_DEFAULT = "geoelevation_dem_cache_unavailable"
# --- Global Shared ElevationManager for Library Functions ---
# This instance is lazily initialized and shared for calls to get_point_elevation
# when geoelevation is used as a library, improving efficiency.
_shared_library_elevation_manager_instance: Optional[ElevationManager] = None
_shared_manager_init_lock = threading.Lock() # Ensures thread-safe initialization
def _get_shared_library_elevation_manager() -> ElevationManager:
"""
Lazily initializes and returns a globally shared ElevationManager instance.
This function is thread-safe.
Returns:
An initialized ElevationManager instance.
Raises:
RuntimeError: If Rasterio (a critical dependency) is not available,
or if the ElevationManager itself fails to initialize.
"""
global _shared_library_elevation_manager_instance
if not RASTERIO_AVAILABLE:
error_message = (
"Rasterio library is not installed or could not be imported. "
"This library is essential for elevation data retrieval functions. "
"Please install Rasterio (e.g., 'pip install rasterio') and ensure it is operational."
)
library_logger.error(error_message)
raise RuntimeError(error_message)
# Use a lock for thread-safe lazy initialization of the shared manager
with _shared_manager_init_lock:
if _shared_library_elevation_manager_instance is None:
library_logger.debug("Initializing shared ElevationManager for library use...")
try:
# Allow overriding the default DEM cache directory via an environment variable
dem_cache_directory_path = os.environ.get(
"GEOELEVATION_DEM_CACHE_DIR", GEOELEVATION_DEM_CACHE_DEFAULT
)
_shared_library_elevation_manager_instance = ElevationManager(
tile_directory=dem_cache_directory_path
)
library_logger.info(
f"GeoElevation library: Shared ElevationManager initialized. "
f"Using DEM Cache Directory: '{dem_cache_directory_path}'"
)
except Exception as e_manager_lib_init_fatal:
library_logger.error(
f"GeoElevation library: CRITICAL - Failed to initialize shared ElevationManager: {e_manager_lib_init_fatal}",
exc_info=True
)
raise RuntimeError(
f"Failed to initialize the core ElevationManager for GeoElevation library: {e_manager_lib_init_fatal}"
) from e_manager_lib_init_fatal
return _shared_library_elevation_manager_instance
class _LibraryProgressWindow(threading.Thread):
"""
A simple, non-blocking progress window using Tkinter, designed for use
when GeoElevation functions are called as a library and might take time.
Runs in its own thread.
"""
def __init__(
self,
window_title_text: str = "GeoElevation - Processing",
progress_message_text: str = "Accessing elevation data...\nPlease wait."
) -> None:
super().__init__(daemon=True) # Daemon threads exit when the main program exits
self.window_title: str = window_title_text
self.progress_message: str = progress_message_text
self.tkinter_root: Optional[tk.Tk] = None
self.is_tk_ready_event = threading.Event() # Signals when Tkinter root window is ready
self._please_stop_event = threading.Event() # Signals this thread's Tkinter loop to stop
def run(self) -> None:
"""Executes the Tkinter event loop for the progress window in this thread."""
try:
self.tkinter_root = tk.Tk()
self.tkinter_root.title(self.window_title)
# Window styling and positioning
win_width = 380
win_height = 120
screen_w = self.tkinter_root.winfo_screenwidth()
screen_h = self.tkinter_root.winfo_screenheight()
pos_x = int(screen_w / 2 - win_width / 2)
pos_y = int(screen_h / 2 - win_height / 2)
self.tkinter_root.geometry(f'{win_width}x{win_height}+{pos_x}+{pos_y}')
self.tkinter_root.resizable(False, False)
# MODIFIED: Set the window to be always on top.
# WHY: Ensures the progress window is visible and not hidden by other windows (like the map window).
# HOW: Used the `attributes()` method with the `'-topmost', True` option. Wrapped in try-except
# to handle potential TclErrors on platforms where this might not be fully supported.
try:
self.tkinter_root.attributes('-topmost', True)
library_logger.debug("Progress window set to be topmost.")
except tk.TclError as e_topmost:
library_logger.warning(f"Could not set progress window to topmost: {e_topmost}")
content_frame = ttk.Frame(self.tkinter_root, padding="15")
content_frame.pack(expand=True, fill=tk.BOTH)
msg_label = ttk.Label(
content_frame, text=self.progress_message, font=("Helvetica", 10), justify=tk.CENTER
)
msg_label.pack(pady=(0, 10))
prog_bar = ttk.Progressbar(content_frame, mode='indeterminate', length=300)
prog_bar.pack(pady=10)
prog_bar.start(25) # Animation interval in milliseconds
self.tkinter_root.protocol("WM_DELETE_WINDOW", self._trigger_stop_from_window_close)
self.is_tk_ready_event.set() # Signal that the Tkinter window is configured
self._check_stop_request_periodically() # Start periodic check for stop signal
self.tkinter_root.mainloop() # Blocks this thread until window is closed via quit()
except Exception as e_prog_win_thread_run:
library_logger.warning(
f"GeoElevation ProgressWindow: Error during thread run method: {e_prog_win_thread_run}", exc_info=True
)
self.is_tk_ready_event.set() # Ensure event is set to unblock any waiters, even on error
finally:
# Attempt to clean up Tkinter resources if mainloop exited unexpectedly
if hasattr(self, 'tkinter_root') and self.tkinter_root:
try:
if self.tkinter_root.winfo_exists():
# Mainloop should handle destroy on quit(), but as a fallback:
# self.tkinter_root.destroy()
pass
except tk.TclError as e_tcl_final_cleanup: # Catch Tcl errors if already destroyed
if "application has been destroyed" not in str(e_tcl_final_cleanup).lower():
library_logger.warning(f"GeoElevation ProgressWindow: TclError on final cleanup: {e_tcl_final_cleanup}")
except Exception as e_destroy_final_prog_win:
library_logger.warning(f"GeoElevation ProgressWindow: Exception on final cleanup: {e_destroy_final_prog_win}")
library_logger.debug("GeoElevation ProgressWindow: Thread execution finished.")
def _check_stop_request_periodically(self) -> None:
"""Internal method, run via Tkinter's 'after', to check for external stop requests."""
if self._please_stop_event.is_set(): # If stop has been requested
if self.tkinter_root and self.tkinter_root.winfo_exists():
try:
# This is executed in the Tkinter thread, so it's safe to call quit()
self.tkinter_root.quit() # Cleanly exit the mainloop
except Exception as e_tk_periodic_quit:
library_logger.warning(
f"GeoElevation ProgressWindow: Error during Tcl quit in _check_stop_request_periodically: {e_tk_periodic_quit}"
)
else:
# Reschedule this check if the window still exists
if self.tkinter_root and self.tkinter_root.winfo_exists():
try:
self.tkinter_root.after(150, self._check_stop_request_periodically) # Check again in 150ms
except tk.TclError: # Can occur if root is destroyed while 'after' is pending
pass # Window is gone, no need to reschedule
def _trigger_stop_from_window_close(self) -> None:
"""Callback for WM_DELETE_WINDOW (window 'X' button), runs in Tkinter thread."""
library_logger.debug("GeoElevation ProgressWindow: WM_DELETE_WINDOW (close button) invoked by user.")
self._please_stop_event.set() # Signal the stop event
# The _check_stop_request_periodically method will then see this and call root.quit()
def request_progress_window_stop(self) -> None:
"""Public method callable from any thread to request the progress window to stop."""
library_logger.debug("GeoElevation ProgressWindow: Stop requested from an external thread.")
self._please_stop_event.set()
# The actual closure is handled by the periodic check within the Tkinter thread.
# --- Public API Functions Exposed by the Package ---
def get_point_elevation(
latitude: float,
longitude: float,
# MODIFIED: Renamed parameter back to show_progress_dialog for backward compatibility.
# WHY: The external tool expects this parameter name.
# HOW: Changed the parameter name in the function signature.
show_progress_dialog: bool = False,
progress_dialog_custom_message: str = "Retrieving elevation data from remote sources.\nThis may take a moment..."
) -> Optional[float]:
"""
Retrieves the elevation for a given geographic point (latitude, longitude).
Args:
latitude (float): The latitude of the point.
longitude (float): The longitude of the point.
show_progress_dialog (bool, optional): If True, attempts to display a simple
progress window. Defaults to False.
(May only work in environments where
Tkinter can be safely run in a background
thread, like the main process).
progress_dialog_custom_message (str, optional): Custom message for the progress
dialog. Defaults to a standard message.
Returns:
Optional[float]: The elevation in meters, float('nan') if the point is on a NoData area,
or None if data is unavailable or an error occurs.
Raises:
RuntimeError: If core dependencies like Rasterio are not available.
ValueError: If input coordinates are invalid.
"""
progress_window_instance: Optional[_LibraryProgressWindow] = None
try:
elevation_manager_instance = _get_shared_library_elevation_manager()
if not (-90.0 <= latitude < 90.0): # Basic validation
library_logger.error(f"GeoElevation API: Invalid latitude provided: {latitude}")
return None
if not (-180.0 <= longitude < 180.0): # Basic validation
library_logger.error(f"GeoElevation API: Invalid longitude provided: {longitude}")
return None
# MODIFIED: Use the renamed parameter show_progress_dialog.
# WHY: The variable name inside the function must match the parameter name in the signature.
# HOW: Changed the variable name here.
if show_progress_dialog:
# MODIFIED: Check if _LibraryProgressWindow class is available before instantiating.
# WHY: Prevent NameError if the import failed due to missing dependencies.
# HOW: Added `if _LibraryProgressWindow is not None`.
if _LibraryProgressWindow is not None:
library_logger.debug("GeoElevation API: Progress dialog requested. Preparing window.")
try:
progress_window_instance = _LibraryProgressWindow(
progress_message_text=progress_dialog_custom_message
)
progress_window_instance.start()
# Wait for the Tkinter window to signal it's ready, with a timeout
# This wait is important because the main thread will proceed to a potentially blocking call.
tk_ready_signal = progress_window_instance.is_tk_ready_event.wait(timeout=2.5) # Slightly longer timeout
if not tk_ready_signal:
library_logger.warning(
"GeoElevation API: Progress dialog did not signal readiness within timeout. Continuing without it."
)
# If it didn't become ready, ensure we don't try to stop a non-existent/broken window later
# Signal it to stop, attempt a brief join, and set to None so finally block doesn't interact.
try: progress_window_instance.request_progress_window_stop()
except Exception: pass
try: progress_window_instance.join(timeout=0.5)
except Exception: pass
progress_window_instance = None # Treat as if not started for finally block
except Exception as e_prog_win_api:
library_logger.error(f"GeoElevation API Error: Failed to start progress window: {e_prog_win_api}", exc_info=True)
progress_window_instance = None # Ensure reference is None on failure
else:
library_logger.warning("GeoElevation API: Cannot show progress dialog: _LibraryProgressWindow class is not available.")
library_logger.info(f"GeoElevation API: Requesting elevation for Lat: {latitude}, Lon: {longitude}")
# The call to get_elevation might block if downloading data.
retrieved_elevation_value = elevation_manager_instance.get_elevation(latitude, longitude)
if retrieved_elevation_value is None:
library_logger.warning(f"GeoElevation API: Elevation data unavailable for Lat: {latitude}, Lon: {longitude}")
elif isinstance(retrieved_elevation_value, float) and math.isnan(retrieved_elevation_value):
library_logger.info(f"GeoElevation API: Point Lat: {latitude}, Lon: {longitude} is on a NoData area.")
else:
library_logger.info(
f"GeoElevation API: Elevation for Lat: {latitude}, Lon: {longitude} is {retrieved_elevation_value:.2f}m"
)
return retrieved_elevation_value
except RuntimeError: # Specifically from _get_shared_library_elevation_manager
library_logger.error("GeoElevation API: Critical runtime error (e.g., Rasterio missing).", exc_info=False) # Log less for expected runtime errors
raise # Re-raise critical initialization errors to the caller
except ValueError: # Propagated from ElevationManager for invalid coordinates for tile naming etc.
library_logger.error("GeoElevation API: Invalid value encountered during elevation retrieval.", exc_info=False)
raise
except Exception as e_api_call_unhandled:
library_logger.error(
f"GeoElevation API: Unexpected error in get_point_elevation for ({latitude},{longitude}): {e_api_call_unhandled}",
exc_info=True
)
return None # Return None for other unexpected errors
finally:
# MODIFIED: Check if progress_window_instance is not None before calling methods.
# WHY: The instance might be None if it failed to start.
# HOW: Added the check `if progress_window_instance:`.
if progress_window_instance:
if progress_window_instance.is_alive():
library_logger.debug("GeoElevation API: Signaling progress dialog to stop after operation.")
progress_window_instance.request_progress_window_stop()
# Wait for the progress thread to finish processing the stop request and exit its mainloop.
# Use a timeout to prevent the main thread from hanging indefinitely if the progress thread encounters an issue.
progress_window_instance.join(timeout=3.0) # Wait for thread to finish
if progress_window_instance.is_alive():
library_logger.warning(
"GeoElevation API: Progress dialog thread did not terminate cleanly after join."
)
else:
library_logger.debug("GeoElevation API: Progress dialog was not alive or already stopped. No stop signal sent.")
def run_gui_application() -> None:
"""
Launches the main GeoElevation Graphical User Interface (GUI).
(Full docstring from previous version)
"""
library_logger.info("GeoElevation: Attempting to launch GUI application via API call...")
try:
# Local import of ElevationApp to avoid top-level circular dependencies
# and to ensure it's only imported when GUI is explicitly requested.
from .elevation_gui import ElevationApp
except ImportError as e_gui_module_import:
library_logger.critical(
f"GeoElevation: Failed to import ElevationApp GUI components: {e_gui_module_import}. GUI cannot start.",
exc_info=True
)
print("ERROR: GeoElevation GUI could not be launched due to missing application components.", file=sys.stderr)
return # Exit this function if GUI components cannot be imported
except tk.TclError as e_tkinter_precheck: # Pre-check for basic Tkinter availability
library_logger.critical(
f"GeoElevation: Tkinter TclError occurred, possibly indicating an issue with the Tk environment: {e_tkinter_precheck}. GUI may not be available.",
exc_info=True
)
print(f"ERROR: A Tkinter problem prevents the GUI from launching: {e_tkinter_precheck}", file=sys.stderr)
return
# `multiprocessing.freeze_support()` is essential for packaged applications
# (e.g., created with PyInstaller) that use multiprocessing, especially on Windows.
# It should be called once at the beginning of the program if it's frozen.
# Calling it here ensures it's run before any GUI-related processes might start.
import multiprocessing # Import here, only when GUI is to be run
multiprocessing.freeze_support()
root_main_tk_window = tk.Tk()
try:
gui_application_instance = ElevationApp(parent_widget=root_main_tk_window)
root_main_tk_window.mainloop() # Start the Tkinter event loop for the GUI
except Exception as e_gui_runtime_error:
library_logger.critical(
f"GeoElevation: Unhandled exception during GUI application execution: {e_gui_runtime_error}", exc_info=True
)
# Attempt to display a simple error dialog as a last resort
try:
fallback_error_root = tk.Tk()
fallback_error_root.withdraw() # Hide the empty root window
from tkinter import messagebox # Local import for this specific use
messagebox.showerror(
"Fatal GUI Error",
f"GeoElevation GUI encountered a fatal error and cannot continue:\n{e_gui_runtime_error}\n\n"
"Please check the application console or logs for more detailed information.",
parent=None # No parent if the main window might be compromised or already destroyed
)
fallback_error_root.destroy()
except Exception:
# If even the fallback error dialog fails, print to stderr
print(
f"FATAL GUI ERROR: {e_gui_runtime_error}\n"
"(Additionally, failed to display a Tkinter error dialog for this error)",
file=sys.stderr
)
# Consider sys.exit(1) here if this is the main application entry point and GUI fails.
# If called as a library function, re-raising might be better.
# --- Define __all__ for the package's public API ---
# This list controls what `from geoelevation import *` imports.
# It's good practice to be explicit about the public interface.
__all__ = [
"get_point_elevation", # Primary API function for elevation
"run_gui_application", # Primary API function to launch the GUI
"ElevationManager", # Expose for advanced users or type hinting
"RASTERIO_AVAILABLE", # Allow checking for this critical dependency
"GEOELEVATION_DEM_CACHE_DEFAULT", # Expose the default DEM cache path constant
"_LibraryProgressWindow", # Expose the progress window class for library users
# Note: deg_to_dms_string is imported but not added to __all__ as it's a utility,
# typically accessed via the map_viewer subpackage or directly if needed,
# but not considered a primary public API function of the top-level package.
# Similarly, bounds functions are not added to __all__.
]
library_logger.debug("GeoElevation package (__init__.py) has been loaded and configured.")

View File

@ -1,81 +1,212 @@
# -*- coding: utf-8 -*-
# geoelevation/__main__.py
"""
Main entry point script for the GeoElevation application.
Works for both `python -m geoelevation` and as PyInstaller entry point.
Uses absolute imports.
Main entry point for the GeoElevation package when executed as a module
(e.g., `python -m geoelevation`).
This script handles command-line arguments to either:
1. Retrieve and print the elevation for a specific geographic point (CLI mode).
2. Launch the GeoElevation Graphical User Interface (GUI mode).
"""
# Standard library imports
import argparse
import sys
import os
import traceback
import multiprocessing
import tkinter as tk
import logging # For configuring logging at the application's entry point
import os # For providing hints in case of import errors during development
import math # For math.isnan in CLI output handling
# --- Import the necessary modules using ABSOLUTE paths ---
try:
# MODIFIED: Changed relative import to absolute import.
# WHY: To make this script work consistently as a direct entry point
# (for PyInstaller) and when run via `python -m`. Assumes the
# 'geoelevation' package is findable in sys.path.
# HOW: Changed 'from .elevation_gui import ElevationApp' to
# 'from geoelevation.elevation_gui import ElevationApp'.
from geoelevation.elevation_gui import ElevationApp
# Import other components if needed for setup (using absolute paths)
# from geoelevation.config import DEFAULT_CACHE_DIR # Example if you had config
except ImportError as e:
# Error message adjusted slightly for absolute import context
print(f"ERROR: Could not import required modules using absolute paths (e.g., 'geoelevation.elevation_gui').")
print(f" Ensure the 'geoelevation' package is correctly structured and accessible.")
print(f"ImportError: {e}")
print(f"Current sys.path: {sys.path}")
# Try to determine expected base path for context
try:
script_path = os.path.dirname(os.path.abspath(__file__))
parent_path = os.path.dirname(script_path)
print(f" Expected package 'geoelevation' potentially under: {parent_path}")
except NameError:
pass # __file__ might not be defined in some rare cases
traceback.print_exc()
sys.exit(1)
except Exception as e:
print(f"ERROR: An unexpected error occurred during initial imports: {e}")
traceback.print_exc()
sys.exit(1)
# --- Application Entry Point Logging Configuration ---
# Configure basic logging for the entire application session when this __main__.py is run.
# This setup will apply if the package is run as an application.
# If 'geoelevation' is imported as a library, its __init__.py configures a NullHandler
# by default, and the consuming application should set up its own logging.
logging.basicConfig(
level=logging.INFO, # Default level, can be overridden by the --verbose argument
format="%(asctime)s - %(levelname)s - [%(name)s] - %(message)s", # Include logger name
datefmt="%Y-%m-%d %H:%M:%S"
)
# Create a logger specific to this main entry script for its own messages
main_entry_logger = logging.getLogger("geoelevation.cli_entry")
def main():
def SCRIPT_ENTRY_POINT_ROUTINE() -> None: # PEP8: function names should be lowercase_with_underscores
"""
Initializes and runs the ElevationApp GUI application.
Parses command-line arguments and either performs a CLI action (get point elevation)
or launches the main GUI application.
This is the main operational function called when the script is executed.
"""
root_window = tk.Tk()
cli_argument_parser = argparse.ArgumentParser(
description="GeoElevation Tool: Retrieve elevation for a point via CLI or launch the GUI.",
formatter_class=argparse.RawTextHelpFormatter, # Allows for newlines in epilog/help
epilog=(
"Usage Examples:\n"
" python -m geoelevation --lat 45.0 --lon 7.0\n"
" python -m geoelevation --lat 45.0 --lon 7.0 --show-progress\n"
" python -m geoelevation --gui\n"
" python -m geoelevation (launches GUI by default if no point coordinates are specified)"
)
)
cli_argument_parser.add_argument(
"--lat",
type=float,
metavar="LATITUDE",
help="Latitude of the target point in decimal degrees (e.g., 45.0)."
)
cli_argument_parser.add_argument(
"--lon",
type=float,
metavar="LONGITUDE",
help="Longitude of the target point in decimal degrees (e.g., 7.0)."
)
cli_argument_parser.add_argument(
"--show-progress",
action="store_true", # If present, args.show_progress will be True
help="Display a visual progress dialog during data retrieval (applies to CLI point elevation mode)."
)
cli_argument_parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Enable verbose (DEBUG level) logging output for the entire 'geoelevation' package."
)
cli_argument_parser.add_argument(
"--gui",
action="store_true",
help="Force the launch of the GUI application, overriding other CLI arguments if present."
)
# Parse the command-line arguments provided by the user
parsed_command_line_args = cli_argument_parser.parse_args()
# Configure logging level based on verbosity argument
if parsed_command_line_args.verbose:
# Set the logging level for the root logger of the 'geoelevation' package to DEBUG
# This will affect all loggers within this package unless they have a more specific level set.
logging.getLogger("geoelevation").setLevel(logging.DEBUG)
main_entry_logger.debug("Verbose logging (DEBUG level) has been enabled for the 'geoelevation' package.")
else:
# Ensure the 'geoelevation' package logger is at least INFO if not verbose.
# This prevents it from being too quiet if the root logger (from basicConfig)
# was set to WARNING or ERROR by another part of a larger system (unlikely here).
geoelevation_package_logger = logging.getLogger("geoelevation")
if geoelevation_package_logger.getEffectiveLevel() > logging.INFO:
geoelevation_package_logger.setLevel(logging.INFO)
# --- Import Public API Functions ---
# Attempt to import the primary public API functions from the geoelevation package's __init__.py.
# This is the recommended way for an application entry point to access package functionality.
try:
# Instantiate ElevationApp, passing the root window
app = ElevationApp(parent_widget=root_window)
# Start the Tkinter event loop
root_window.mainloop()
except Exception as e:
print(f"FATAL ERROR: An unexpected error occurred during application execution: {e}")
traceback.print_exc()
try:
error_root = tk.Tk()
error_root.withdraw()
from tkinter import messagebox
messagebox.showerror(
"Fatal Error",
f"Application failed to run:\n{e}\n\nSee console for details.",
parent=error_root
from geoelevation import get_point_elevation # API for CLI
from geoelevation import run_gui_application # API for launching GUI
except ImportError as e_fatal_api_import:
main_entry_logger.critical(
f"CRITICAL FAILURE: Could not import core API functions (get_point_elevation, run_gui_application) "
f"from the GeoElevation package. This indicates a fundamental problem with the package "
f"installation, structure, or missing critical dependencies. Original Error: {e_fatal_api_import}",
exc_info=True # Include traceback for critical errors
)
# Provide a hint to the user if they might be running the command from an incorrect directory
# during development (e.g., inside the package folder instead of its parent).
current_dir = os.getcwd()
if "geoelevation" not in current_dir.lower() and \
os.path.isdir(os.path.join(os.path.dirname(current_dir), "geoelevation")): # Heuristic
main_entry_logger.info(
"DEVELOPMENT HINT: If running with `python -m geoelevation ...`, "
"ensure you are in the directory *above* the 'geoelevation' package folder."
)
error_root.destroy()
except Exception:
pass
sys.exit(1)
sys.exit(1) # Exit with an error code if core API cannot be loaded
# --- Determine Action Based on Parsed Arguments ---
if parsed_command_line_args.gui:
main_entry_logger.info("Launching GeoElevation GUI (explicitly requested via --gui argument)...")
run_gui_application()
elif parsed_command_line_args.lat is not None and parsed_command_line_args.lon is not None:
# CLI mode for getting point elevation
main_entry_logger.info(
f"CLI mode: Requesting elevation for Latitude: {parsed_command_line_args.lat}, "
f"Longitude: {parsed_command_line_args.lon}"
)
try:
# Call the public API function to get the elevation
elevation_result = get_point_elevation(
latitude=parsed_command_line_args.lat,
longitude=parsed_command_line_args.lon,
show_progress_dialog_flag=parsed_command_line_args.show_progress
)
# Print the result to standard output for CLI users
if elevation_result is None:
# This indicates an issue like data unavailability or an internal error.
print("Elevation: UNAVAILABLE (Data could not be retrieved or an error occurred. Please check logs for details.)")
sys.exit(1) # Exit with a non-zero code to indicate error for scripting
elif isinstance(elevation_result, float) and math.isnan(elevation_result):
# Point is on a NoData area within a valid DEM tile
print("Elevation: NODATA (Point is located on a NoData area within the DEM tile)")
sys.exit(0) # Successful query, result is NoData
else:
# Successful elevation retrieval
print(f"Elevation: {elevation_result:.2f} meters")
sys.exit(0) # Successful CLI operation
except RuntimeError as e_cli_runtime_get_elev:
# Catch RuntimeErrors (e.g., Rasterio missing, ElevationManager init failure)
print(f"CLI ERROR: A critical runtime error occurred: {e_cli_runtime_get_elev}", file=sys.stderr)
main_entry_logger.error(
f"Runtime error during CLI elevation retrieval: {e_cli_runtime_get_elev}", exc_info=True
)
sys.exit(1)
except ValueError as e_cli_value_get_elev:
# Catch ValueErrors (e.g., invalid coordinates passed to API that were not caught by argparse)
print(f"CLI ERROR: Invalid input value provided - {e_cli_value_get_elev}", file=sys.stderr)
main_entry_logger.error(
f"ValueError during CLI elevation retrieval: {e_cli_value_get_elev}", exc_info=False # Traceback often not needed for user input errors
)
sys.exit(1)
except Exception as e_cli_unexpected_get_elev:
# Catch any other unexpected exceptions during the API call
print(f"CLI ERROR: An unexpected error occurred during elevation retrieval: {e_cli_unexpected_get_elev}", file=sys.stderr)
main_entry_logger.error(
f"Unexpected error during CLI elevation retrieval: {e_cli_unexpected_get_elev}", exc_info=True
)
sys.exit(1)
else:
# Default behavior: Launch GUI if no CLI action for point elevation is fully specified
# This condition also catches cases where only --lat or only --lon was given without the other.
if parsed_command_line_args.lat is None and parsed_command_line_args.lon is None:
# No lat/lon args, and --gui was not specified. Default to GUI.
main_entry_logger.info(
"No specific CLI arguments for point elevation were provided. "
"Launching GeoElevation GUI by default..."
)
run_gui_application()
else:
# Partial arguments for point elevation (e.g., only --lat without --lon, or vice-versa)
cli_argument_parser.print_help(sys.stderr) # Print help message to standard error
# Add a specific error message for this case
print(
"\nError: For Command Line Interface (CLI) point elevation retrieval, "
"both --lat and --lon arguments must be specified together.",
file=sys.stderr
)
sys.exit(2) # Use a different exit code for incorrect CLI usage
# --- Main Execution Guard ---
if __name__ == "__main__":
# !!! IMPORTANT for PyInstaller and multiprocessing !!!
# Must be called in the main entry script for frozen executables.
multiprocessing.freeze_support()
# print(f"Running GeoElevation via __main__.py...")
main()
# This block is executed when the script is run as the main program, e.g.:
# `python -m geoelevation`
# It can also be the entry point for an executable created by PyInstaller
# if `geoelevation/__main__.py` is specified.
# Note: `multiprocessing.freeze_support()` is now strategically placed within
# `run_gui_application` (inside `geoelevation/__init__.py`) because it's only
# needed if the GUI (which might use multiprocessing for its tasks) is launched.
# This keeps `__main__.py` cleaner and ensures `freeze_support` is called
# at the appropriate juncture before any GUI-related processes might be spawned.
main_entry_logger.debug(
f"GeoElevation __main__.py executed as entry point. Python version: {sys.version_info.major}.{sys.version_info.minor}."
"Proceeding to CLI/GUI launch logic."
)
SCRIPT_ENTRY_POINT_ROUTINE() # Call the main operational function

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

90
geoelevation/_version.py Normal file
View File

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
# File generated by PyInstaller GUI Wrapper. DO NOT EDIT MANUALLY.
# Contains build-time information scraped from Git (if available)
# and a helper function to format version strings.
import re
# --- Version Data (Generated) ---
# This section is automatically generated by the build process.
__version__ = "v.0.0.0.4-dirty"
GIT_COMMIT_HASH = "25bda8238ddaa500af84c78fefe5251bb1eeacab"
GIT_BRANCH = "master"
BUILD_TIMESTAMP = "2025-05-06T07:19:12Z"
IS_GIT_REPO = True
# --- Default Values (for comparison or fallback) ---
DEFAULT_VERSION = "0.0.0+unknown"
DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown"
# --- Helper Function ---
def get_version_string(format_string=None):
"""
Returns a formatted string based on the build version information.
Args:
format_string (str, optional): A format string using placeholders.
Defaults to "{{version}} ({{branch}}/{{commit_short}})" if None.
Placeholders:
{{version}}: Full version string (e.g., 'v1.0.0-5-gabcdef-dirty')
{{tag}}: Clean tag part if exists (e.g., 'v1.0.0'), else DEFAULT_VERSION.
{{commit}}: Full Git commit hash.
{{commit_short}}: Short Git commit hash (7 chars).
{{branch}}: Git branch name.
{{dirty}}: '-dirty' if the repo was dirty, empty otherwise.
{{timestamp}}: Full build timestamp (ISO 8601 UTC).
{{timestamp_short}}: Build date only (YYYY-MM-DD).
{{is_git}}: 'Git' if IS_GIT_REPO is True, 'Unknown' otherwise.
Returns:
str: The formatted version string, or an error message if formatting fails.
"""
if format_string is None:
format_string = "{version} ({branch}/{commit_short})" # Sensible default
replacements = {}
try:
# Prepare data dictionary for substitution
replacements['version'] = __version__ if __version__ else DEFAULT_VERSION
replacements['commit'] = GIT_COMMIT_HASH if GIT_COMMIT_HASH else DEFAULT_COMMIT
replacements['commit_short'] = GIT_COMMIT_HASH[:7] if GIT_COMMIT_HASH and len(GIT_COMMIT_HASH) >= 7 else DEFAULT_COMMIT
replacements['branch'] = GIT_BRANCH if GIT_BRANCH else DEFAULT_BRANCH
replacements['timestamp'] = BUILD_TIMESTAMP if BUILD_TIMESTAMP else "Unknown"
replacements['timestamp_short'] = BUILD_TIMESTAMP.split('T')[0] if BUILD_TIMESTAMP and 'T' in BUILD_TIMESTAMP else "Unknown"
replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown"
replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else ""
# Extract clean tag using regex (handles versions like v1.0.0, 1.0.0)
tag = DEFAULT_VERSION
if __version__ and IS_GIT_REPO:
# Match optional 'v' prefix, then major.minor.patch
match = re.match(r'^(v?([0-9]+)\.([0-9]+)\.([0-9]+))', __version__)
if match:
tag = match.group(1) # Get the full tag (e.g., 'v1.0.0')
replacements['tag'] = tag
# Perform substitution using regex to find placeholders {placeholder}
output_string = format_string
# Iterate through placeholders and replace them in the format string
for placeholder, value in replacements.items():
# Compile regex pattern for {placeholder}, allowing for whitespace inside braces
pattern = re.compile(r'{\s*' + re.escape(placeholder) + r'\s*}')
# Substitute found patterns with the corresponding string value
output_string = pattern.sub(str(value), output_string)
# Optional: Check if any placeholders remain unsubstituted (could indicate typo)
if re.search(r'{\s*[\w_]+\s*}', output_string):
# You might want to log this or handle it, for now, we return the string as is
# print(f"Warning: Unsubstituted placeholders remain in version string: {output_string}")
pass
return output_string
except Exception as e:
# Return a simple error message in case of unexpected formatting issues
# Avoid printing directly from this generated function
return f"[Formatting Error: {e}]"

41
geoelevation/config.py Normal file
View File

@ -0,0 +1,41 @@
# geoelevation/config.py
"""
Configuration constants for the GeoElevation package.
This module centralizes default values and settings used across the
GeoElevation application and library.
"""
# --- Cache Directory Configurations ---
# Default cache directory for Digital Elevation Model (DEM) HGT tiles
# This is used by ElevationManager.
GEOELEVATION_DEM_CACHE_DEFAULT: str = "geoelevation_dem_cache"
# Default root cache directory for map tiles (e.g., OpenStreetMap tiles)
# Used by map_viewer.MapTileManager. A sub-directory for each service
# (e.g., 'osm') will be created under this root.
DEFAULT_MAP_TILE_CACHE_ROOT_DIR: str = "geoelevation_map_tile_cache"
# --- Map Viewer Default Settings ---
# Default zoom level for map displays when not otherwise specified.
DEFAULT_MAP_DISPLAY_ZOOM_LEVEL: int = 3
# Default area size (in kilometers) to display around a point on the map
# when showing a map for a single point.
DEFAULT_MAP_VIEW_AREA_SIZE_KM: float = 5.0
# --- Logging Configuration (Example - can be more complex) ---
# Default logging level for the application if not overridden by CLI args.
# Options: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
# APPLICATION_DEFAULT_LOG_LEVEL: str = "INFO"
# --- Other Potential Configurations ---
# Example: Placeholder color for map tiles if they fail to load
# MAP_TILE_PLACEHOLDER_COLOR_RGB: tuple[int, int, int] = (220, 220, 220)
# Example: Network timeout for tile downloads
# NETWORK_REQUEST_TIMEOUT_SECONDS: int = 10

File diff suppressed because it is too large Load Diff

View File

@ -239,8 +239,8 @@ def create_composite_area_image(
logging.info(f"Attempting to create composite image for {len(tile_info_list)} tile locations.")
# 1. Determine grid dimensions and range from tile coordinates
lats = [info["latitude_coord"] for info in tile_info_list]
lons = [info["longitude_coord"] for info in tile_info_list]
lats = [info["latitude_coord"] for info in tile_info_list if "latitude_coord" in info]
lons = [info["longitude_coord"] for info in tile_info_list if "longitude_coord" in info]
if not lats or not lons:
logging.warning("Tile info list seems empty or lacks coordinates.")
return None # Cannot proceed without coordinates

View File

@ -0,0 +1,375 @@
# geoelevation/process_targets.py
"""
Contains multiprocessing target functions executed in separate processes.
These functions perform visualization tasks (2D/3D plots, map viewer).
"""
# Standard library imports needed at the top level for process entry points
import multiprocessing
import sys
import logging
from typing import Optional, Tuple, List, Dict, Any, TYPE_CHECKING # Needed for type hinting
# Local application/package imports that the target functions will use.
# Imports needed *within* the target functions should be handled carefully
# (e.g., within try blocks or using local imports) as the child process
# might have a different import context or dependencies.
# Configure logging for the process target module itself.
# Note: The run_map_viewer_process_target explicitly configures logging,
# but other process targets might not, so a basic config here is safer.
# Using NullHandler by default, similar to how libraries are configured,
# letting the *actual process entry point* decide on the final handlers.
logger = logging.getLogger(__name__)
if not logger.hasHandlers():
logger.addHandler(logging.NullHandler())
# The run_map_viewer_process_target function will configure a StreamHandler.
# Define the multiprocessing target functions
def process_target_show_image(image_path: str, tile_name: str, window_title: str, extent: Optional[List[float]] = None) -> None:
"""
Multiprocessing target function to load and display a 2D image.
Expected to be run in a separate process.
"""
try:
# Local imports needed by this target function
from geoelevation.image_processor import load_prepare_single_browse as local_lpst
from geoelevation.image_processor import PIL_AVAILABLE as local_pil_ok
from geoelevation.visualizer import show_image_matplotlib as local_sim
from geoelevation.visualizer import MATPLOTLIB_AVAILABLE as local_mpl_ok
import os as local_os
# Basic check for critical libraries within the child process
if not (local_pil_ok and local_mpl_ok):
# Use print for crucial messages in process targets, as logging might not be fully set up yet.
print("PROCESS ERROR (show_image): Pillow/Matplotlib missing in child process. Cannot display image.")
return
# Load and prepare the image (adds overlay)
prepared_image = local_lpst(image_path, tile_name)
if prepared_image:
# Display the image using Matplotlib
print(f"PROCESS (show_image): Showing '{window_title}' (Extent: {'Yes' if extent is not None else 'No'})...")
local_sim(prepared_image, window_title, extent=extent) # Pass the extent
# Explicitly close matplotlib figures to free memory in the child process
# Not strictly necessary as process exits, but good practice.
try:
import matplotlib.pyplot as local_plt # Import pyplot locally for closing
local_plt.close('all')
# print("PROCESS (show_image): Closed Matplotlib figures.")
except Exception:
pass # Ignore errors if matplotlib closing fails
else:
print(f"PROCESS ERROR (show_image): Could not prepare image from {local_os.path.basename(image_path)}")
except Exception as e_proc_img:
# Log/print unexpected errors with traceback
print(f"PROCESS ERROR in process_target_show_image: {e_proc_img}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
# The process will exit when this function returns.
def process_target_show_3d(
hgt_data: Optional["np_typing.ndarray"],
plot_title: str,
initial_subsample: int,
smooth_sigma: Optional[float],
interpolation_factor: int,
plot_grid_points: int
) -> None:
"""
Multiprocessing target function to display a 3D elevation plot.
Expected to be run in a separate process.
"""
try:
# Local imports needed by this target function
from geoelevation.visualizer import show_3d_matplotlib as local_s3d
from geoelevation.visualizer import MATPLOTLIB_AVAILABLE as local_mpl_ok
from geoelevation.visualizer import SCIPY_AVAILABLE as local_scipy_ok
import numpy as local_np # Import numpy locally if not already available
# Basic check for critical libraries
if not local_mpl_ok:
print("PROCESS ERROR (show_3d): Matplotlib missing in child process. Cannot display 3D plot.")
return
effective_smooth_sigma = smooth_sigma
effective_interpolation_factor = interpolation_factor
# Check SciPy availability for advanced features
if (interpolation_factor > 1 or smooth_sigma is not None) and not local_scipy_ok:
print("PROCESS WARNING (show_3d): SciPy missing in child. Disabling 3D plot smoothing/interpolation features.")
effective_smooth_sigma = None
effective_interpolation_factor = 1
if hgt_data is not None and isinstance(hgt_data, local_np.ndarray): # Ensure data is a numpy array
# Perform the 3D visualization
print(f"PROCESS (show_3d): Plotting '{plot_title}' (InitialSub:{initial_subsample}, Smooth:{effective_smooth_sigma}, Interp:{effective_interpolation_factor}x, PlotGridTarget:{plot_grid_points})...")
local_s3d(
hgt_data,
plot_title,
initial_subsample,
effective_smooth_sigma,
effective_interpolation_factor,
plot_grid_points
)
# Explicitly close matplotlib figures
try:
import matplotlib.pyplot as local_plt # Import pyplot locally for closing
local_plt.close('all')
# print("PROCESS (show_3d): Closed Matplotlib figures.")
except Exception:
pass # Ignore errors if matplotlib closing fails
else:
print("PROCESS ERROR (show_3d): No valid HGT data array received for plotting.")
except Exception as e_proc_3d:
# Log/print unexpected errors with traceback
print(f"PROCESS ERROR in process_target_show_3d: {e_proc_3d}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
# The process will exit when this function returns.
def process_target_create_show_area(tile_info_list_data: List[Dict], window_title_str: str, extent: Optional[List[float]] = None) -> None:
"""
Multiprocessing target function to create a composite image for an area
and display it. Expected to be run in a separate process.
"""
try:
# Local imports needed by this target function
from geoelevation.image_processor import create_composite_area_image as local_ccai
from geoelevation.image_processor import PIL_AVAILABLE as local_pil_ok
from geoelevation.visualizer import show_image_matplotlib as local_sim
from geoelevation.visualizer import MATPLOTLIB_AVAILABLE as local_mpl_ok
# Basic check for critical libraries
if not (local_pil_ok and local_mpl_ok):
print("PROCESS ERROR (show_area): Pillow/Matplotlib libraries are missing in the child process. Cannot create/display composite.")
return
print("PROCESS (show_area): Creating composite area image...")
composite_pil_image = local_ccai(tile_info_list_data)
if composite_pil_image:
# Display the composite image
print(f"PROCESS (show_area): Displaying composite image '{window_title_str}' (Extent: {'Yes' if extent is not None else 'No'})...")
local_sim(composite_pil_image, window_title_str, extent=extent) # Pass the extent
# Explicitly close matplotlib figures
try:
import matplotlib.pyplot as local_plt # Import pyplot locally for closing
local_plt.close('all')
# print("PROCESS (show_area): Closed Matplotlib figures.")
except Exception:
pass # Ignore errors if matplotlib closing fails
else:
print("PROCESS ERROR (show_area): Failed to create composite area image.")
except Exception as e_proc_area:
# Log/print unexpected errors with traceback
print(f"PROCESS ERROR in process_target_create_show_area: {e_proc_area}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
# The process will exit when this function returns.
def run_map_viewer_process_target(
map_interaction_q: multiprocessing.Queue,
operation_mode: str,
center_latitude: Optional[float],
center_longitude: Optional[float], # Type hint corrected
area_bounding_box: Optional[Tuple[float, float, float, float]],
dem_data_cache_dir: str,
display_scale_factor: float
) -> None:
"""
Multiprocessing target function to run the interactive map viewer.
Expected to be run in a separate process.
"""
# --- Initial Process Setup and Logging ---
# Configure logging for the child process explicitly.
# This ensures its logs are visible and don't interfere with a parent's logging setup.
child_logger = logging.getLogger("GeoElevationMapViewerChildProcess")
# Check if handlers already exist (e.g., from basicConfig in __main__.py).
# If not, set up basic stream handling.
if not child_logger.hasHandlers():
# Use basicConfig if no handlers exist in this process's logger or its ancestors.
# Using basicConfig might affect the root logger if no other configuration exists globally in this process.
# A safer approach if deeper control is needed might be to manually add handlers.
# For a dedicated process target, basicConfig is usually fine.
logging.basicConfig(
level=logging.INFO, # Default level for this process, can be adjusted
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
stream=sys.stdout # Send output to stdout, which is captured by parent process's console.
)
# Ensure the 'geoelevation' package logger in this process has a handler
# if its level is below the root default.
geoelevation_proc_logger = logging.getLogger("geoelevation")
if not geoelevation_proc_logger.hasHandlers():
geoelevation_proc_logger.addHandler(logging.StreamHandler(sys.stdout)) # Add a stream handler
child_logger.info("Map viewer process started.")
# --- Import Critical Libraries for Map Viewer Process ---
child_map_viewer_instance: Optional[Any] = None
critical_libs_available = False
try:
# Local imports: use the external map_manager package for map rendering and engine
from map_manager.engine import MapEngine as ChildMapEngine
from map_manager.visualizer import MapVisualizer as ChildMapVisualizer
from geoelevation.elevation_manager import ElevationManager as ChildElevationManager
import cv2 as child_cv2 # Optional, used by MapVisualizer if available
import numpy as child_np
critical_libs_available = True
except ImportError as e_child_imp_map:
child_logger.critical(f"CRITICAL: Map viewer components or essential libraries not found in child process: {e_child_imp_map}", exc_info=True)
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
"latitude_dms_str": "Error", "longitude_dms_str": "Error",
"elevation_str": f"Fatal Error: {type(e_child_imp_map).__name__} (Import)",
"map_area_size_str": "Map System N/A"}
try: map_interaction_q.put(error_payload)
except Exception as e_put_err: child_logger.error(f"Failed to put error payload to queue before exit: {e_put_err}")
return
# --- Initialize Map Viewer and Run Main Loop ---
try:
child_logger.info(f"Initializing GeoElevationMapViewer instance for mode '{operation_mode}', display scale: {display_scale_factor:.2f}...")
# Initialize ElevationManager and the map engine/visualizer from external package
local_em = ChildElevationManager(tile_directory=dem_data_cache_dir)
# Initialize map engine and visualizer
engine = ChildMapEngine(service_name='osm', cache_dir=dem_data_cache_dir, enable_online=True)
visual = ChildMapVisualizer(engine)
child_logger.info("Child MapEngine and MapVisualizer instances initialized.")
# Show map depending on requested operation mode
if operation_mode == 'point' and center_latitude is not None and center_longitude is not None:
child_logger.info(f"Showing point {center_latitude},{center_longitude} via MapVisualizer.show_point")
try:
visual.show_point(center_latitude, center_longitude)
except Exception as e_vis:
child_logger.error(f"Error while showing point via MapVisualizer: {e_vis}")
elif operation_mode == 'area' and area_bounding_box:
child_logger.info(f"Creating area image for bbox {area_bounding_box} via MapEngine.get_image_for_area")
try:
img = engine.get_image_for_area(area_bounding_box, zoom=None, max_size=800)
if img:
visual.show_pil_image(img)
else:
child_logger.error("MapEngine returned no image for area request")
except Exception as e_area:
child_logger.error(f"Error while creating/showing area image: {e_area}")
else:
child_logger.error(f"Invalid operation mode ('{operation_mode}') or missing parameters passed to map process target.")
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
"latitude_dms_str": "Error", "longitude_dms_str": "Error",
"elevation_str": f"Fatal Error: Invalid Map Args",
"map_area_size_str": "Invalid Args"}
try: map_interaction_q.put(error_payload)
except Exception as e_put_err: child_logger.error(f"Failed to put error payload to queue before exit: {e_put_err}")
return
# if operation_mode == "point" and center_latitude is not None and center_longitude is not None:
# child_logger.info(f"Calling display_map_for_point for ({center_latitude:.5f},{center_longitude:.5f}).")
# child_map_viewer_instance.display_map_for_point(center_latitude, center_longitude)
# elif operation_mode == "area" and area_bounding_box:
# child_logger.info(f"Calling display_map_for_area for BBox {area_bounding_box}.")
# child_map_viewer_instance.display_map_for_area(area_bounding_box)
# else:
# child_logger.error(f"Invalid operation mode ('{operation_mode}') or missing parameters passed to map process target.")
# error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
# "latitude_dms_str": "Error", "longitude_dms_str": "Error",
# "elevation_str": f"Fatal Error: Invalid Map Args",
# "map_area_size_str": "Invalid Args"}
# try: map_interaction_q.put(error_payload)
# except Exception as e_put_err: child_logger.error(f"Failed to put error payload to queue before exit: {e_put_err}")
# return
child_logger.info("Initial map display call complete. Entering OpenCV event loop.")
# Main loop to keep the OpenCV window alive and process events.
# Use OpenCV window visibility checks rather than relying on an application-specific
# `map_display_window_controller` attribute which may not exist with the external visualizer.
is_map_active = True
window_name = 'Map' if 'child_cv2' in locals() and child_cv2 is not None else None
while is_map_active:
# Pass a short delay to yield CPU time and let OpenCV process window events
key = child_cv2.waitKey(100) if window_name else -1
try:
# If there's an OpenCV window, check its visibility property. If the window is closed
# by the user, getWindowProperty returns a value < 1. Use that as the signal to exit.
if window_name:
try:
vis = child_cv2.getWindowProperty(window_name, child_cv2.WND_PROP_VISIBLE)
if vis < 1:
child_logger.info("Map window detected as closed (OpenCV).")
break
except Exception:
# If getWindowProperty is unsupported for some reason, fall back to key checks.
pass
# Check for specific key presses (like 'q' or Escape) to allow closing via keyboard
if key != -1:
child_logger.debug(f"Map window received key press: {key}")
try:
if chr(key & 0xFF) in ('q', 'Q'):
child_logger.info("Map window closing due to 'q'/'Q' key press.")
break
if key == 27:
child_logger.info("Map window closing due to Escape key press.")
break
except Exception:
pass
except Exception as e_loop_check:
child_logger.debug(f"Error during OpenCV loop checks: {e_loop_check}")
break
child_logger.info("OpenCV event loop finished. Map viewer process target returning.")
except Exception as e_map_proc_fatal:
# Catch any unhandled exceptions during the map viewer's operation loop.
child_logger.critical(f"FATAL: Unhandled exception in map viewer process: {e_map_proc_fatal}", exc_info=True)
# Send a fatal error message back to the GUI.
# MODIFIED: Include DMS fields with error state for consistency with GUI update logic.
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
"latitude_dms_str": "Error", "longitude_dms_str": "Error",
"elevation_str": f"Fatal Error: {type(e_map_proc_fatal).__name__}",
"map_area_size_str": "Fatal Error"}
try: map_interaction_q.put(error_payload)
except Exception as e_put_err: child_logger.error(f"Failed to put error payload to queue before exit: {e_put_err}")
finally:
# Ensure cleanup is performed regardless of how the function exits (success, planned loop exit, exception).
child_logger.info("Map viewer process target finally block reached. Performing cleanup.")
if child_map_viewer_instance and hasattr(child_map_viewer_instance, 'shutdown'):
child_map_viewer_instance.shutdown() # Call the instance's shutdown method
child_logger.info("Child map viewer instance shutdown complete.")
# cv2.destroyAllWindows() is often called here in simple examples, but destroying the specific window
# via the instance's shutdown method is usually better practice if multiple OpenCV windows might exist.
# Let's add a safety destroyAll just in case, although the instance shutdown should handle it.
try:
if 'child_cv2' in locals() and child_cv2 is not None:
child_cv2.destroyAllWindows()
child_logger.debug("Called cv2.destroyAllWindows() in child process.")
except Exception as e_destroy_all:
child_logger.debug(f"Error calling cv2.destroyAllWindows() in child process: {e_destroy_all}")
child_logger.info("Map viewer process target finished.")

View File

@ -2,7 +2,7 @@
import logging
import os
from typing import Optional, Union, TYPE_CHECKING
from typing import Optional, Union, TYPE_CHECKING, List # Import List for extent type hint
import time # For benchmarking processing time
# --- Dependency Checks ---
@ -115,11 +115,20 @@ if TYPE_CHECKING:
def show_image_matplotlib(
image_source: Union[str, "np_typing.ndarray", "PILImage_typing.Image"],
title: str = "Image Preview"
title: str = "Image Preview",
extent: Optional[List[float]] = None # MODIFIED: Added optional extent parameter
):
"""
Displays an image in a separate Matplotlib window with interactive zoom/pan.
Supports loading from a file path (str), a NumPy array, or a PIL Image object.
Optionally applies a geographic extent for correct aspect ratio display.
Args:
image_source (Union[str, np.ndarray, PIL.Image.Image]): The image data or path.
title (str): The title for the plot window.
extent (Optional[List[float]]): A list [left, right, bottom, top] in geographic
coordinates. If provided, Matplotlib will use this
for plot limits and aspect ratio.
"""
if not MATPLOTLIB_AVAILABLE:
logging.error("Cannot display image: Matplotlib is not available.")
@ -127,7 +136,11 @@ def show_image_matplotlib(
img_display_np: Optional["np_typing.ndarray"] = None
source_type = type(image_source).__name__
logging.info(f"Attempting to display image '{title}' from source type: {source_type}")
# MODIFIED: Added log info about whether extent is provided.
# WHY: Useful for debugging.
# HOW: Check if extent is None in the log message.
logging.info(f"Attempting to display image '{title}' from source type: {source_type}. Extent provided: {'Yes' if extent is not None else 'No'}")
try:
if isinstance(image_source, str):
@ -145,6 +158,9 @@ def show_image_matplotlib(
return
elif isinstance(image_source, np.ndarray):
img_display_np = image_source.copy()
# MODIFIED: Added check for PIL_AVAILABLE_VIS before isinstance check against Image.Image.
# WHY: Avoids NameError if Image is the dummy class.
# HOW: Added the boolean check.
elif PIL_AVAILABLE_VIS and isinstance(image_source, Image.Image): # type: ignore
img_display_np = np.array(image_source)
else:
@ -156,9 +172,26 @@ def show_image_matplotlib(
return
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(img_display_np)
# MODIFIED: Pass the extent parameter to imshow.
# WHY: Allows Matplotlib to display the image with the correct geographic aspect ratio.
# HOW: Added extent=extent argument.
ax.imshow(img_display_np, extent=extent)
ax.set_title(title)
ax.axis('off')
# MODIFIED: Only turn off axis if extent is NOT provided.
# WHY: If extent is provided, the axes represent geographic coordinates and should usually be visible.
# HOW: Added conditional check.
if extent is None:
ax.axis('off') # Turn off axes for simple image display
else:
ax.set_xlabel("Longitude") # Label axes with geographic meaning
ax.set_ylabel("Latitude")
ax.set_aspect('auto', adjustable='box') # Let Matplotlib adjust aspect based on extent and figure size
# It might be better to use 'equal' if we strictly want geographic square pixels,
# but 'auto' often gives a better fit within the figure while respecting the extent.
# Let's stick with default 'auto' when extent is provided, which implicitly
# respects the aspect ratio defined by the extent if adjustable='box'.
plt.show()
logging.debug(f"Plot window for '{title}' closed.")
except Exception as e:

125
pyvista_dem_example.py Normal file
View File

@ -0,0 +1,125 @@
# File: pyvista_dem_example_v2.py
# Description: A more robust version of the interactive DEM viewer example.
import numpy as np
import pyvista as pv
import sys # Import sys for checking version
def create_sample_dem_data(width=100, height=100):
"""Creates a sample NumPy array representing DEM data for demonstration."""
# Create coordinates as float32 from the start to avoid UserWarning
x = np.arange(-width / 2, width / 2, 1, dtype=np.float32)
y = np.arange(-height / 2, height / 2, 1, dtype=np.float32)
xx, yy = np.meshgrid(x, y)
# Create some interesting terrain with hills and valleys
z = (np.sin(np.sqrt(xx**2 + yy**2) * 0.3) * 20 +
np.cos(xx * 0.2) * 15 +
np.sin(yy * 0.3) * 10)
# Add a larger mountain feature
mountain_x, mountain_y = 20, -15
mountain_height = 80
mountain_spread = 25
distance_from_peak = np.sqrt((xx - mountain_x)**2 + (yy - mountain_y)**2)
mountain = mountain_height * np.exp(-(distance_from_peak**2 / (2 * mountain_spread**2)))
z += mountain
return z.astype(np.float32) # Ensure final Z is also float
def run_interactive_dem_viewer(dem_data_array: np.ndarray):
"""
Creates and displays an interactive 3D scene from a DEM data array using PyVista.
Args:
dem_data_array (np.ndarray): 2D NumPy array of elevation values.
"""
if not dem_data_array.any():
print("Error: DEM data is empty.")
return
# 1. Create a 3D mesh from the 2D NumPy array.
height, width = dem_data_array.shape
x_coords = np.arange(width, dtype=np.float32)
y_coords = np.arange(height, dtype=np.float32)
x_grid, y_grid = np.meshgrid(x_coords, y_coords)
z_grid = dem_data_array
terrain_mesh = pv.StructuredGrid(x_grid, y_grid, z_grid)
terrain_mesh.points[:, 2] *= 0.3
# 2. Set up the 3D plotter/scene
plotter = pv.Plotter(window_size=[1200, 800])
plotter.add_mesh(terrain_mesh, cmap='terrain', show_edges=False)
# 3. Define the observer (radar) position and direction
observer_position = np.array([width / 2, height / 2, 150.0], dtype=np.float32)
observer_direction = np.array([0.8, 0.8, -0.6], dtype=np.float32)
observer_direction /= np.linalg.norm(observer_direction)
# 4. Add visual objects to the scene
plotter.add_mesh(
pv.Sphere(radius=2, center=observer_position),
color='red',
label='Observer'
)
ray_length = 200
ray_end_point = observer_position + observer_direction * ray_length
plotter.add_mesh(
pv.Tube(pointa=observer_position, pointb=ray_end_point, radius=0.5),
color='cyan',
label='Radar Beam'
)
# 5. Perform Ray Tracing - CHECK IF METHOD EXISTS
if hasattr(plotter, 'ray_trace'):
intersection_points, _ = plotter.ray_trace(observer_position, ray_end_point)
if len(intersection_points) > 0:
target_point = intersection_points[0]
print(f"Radar beam intersects terrain at coordinates: {target_point}")
plotter.add_mesh(
pv.Sphere(radius=1.5, center=target_point),
color='yellow',
label='Target'
)
else:
print("Radar beam does not intersect the terrain (pointing at the sky).")
else:
print("\n--- WARNING ---")
print("Your version of PyVista is outdated and does not have the 'ray_trace' method.")
print("Please upgrade with: pip install --upgrade pyvista")
print("-----------------\n")
# 6. Customize the scene and show it
plotter.add_axes()
plotter.add_legend()
plotter.camera_position = 'xy'
plotter.camera.zoom(1.5)
print("\n--- Interactive Controls ---")
print("Mouse Left: Rotate | Mouse Middle/Shift+Left: Pan | Mouse Right/Scroll: Zoom")
print("Press 'q' to close the window.")
try:
# This call is blocking and opens the interactive window
plotter.show()
except Exception as e:
print("\n---!!! FAILED TO LAUNCH 3D WINDOW !!!---")
print(f"An error occurred during rendering: {e}")
print("This is likely an OpenGL/Graphics Driver issue.")
print("Please check the following:")
print("1. Are you running this via Remote Desktop (RDP)? Try running it locally.")
print("2. Are your graphics card drivers up to date? Visit NVIDIA/AMD/Intel's website.")
print("3. If in a VM, is 3D acceleration enabled?")
print("------------------------------------------")
if __name__ == '__main__':
# Generate sample data
dem_data = create_sample_dem_data(width=200, height=200)
# Run the interactive viewer
run_interactive_dem_viewer(dem_data)

44
requirements.txt Normal file
View File

@ -0,0 +1,44 @@
# Requirements generated by DependencyAnalyzer for GeoElevation
# Python Version (analysis env): 3.13.3
# --- Standard Library Modules Used (part of Python 3.13.3) ---
# io (Used in: geoelevation\elevation_manager.py)
# logging (Used in: geoelevation\elevation_gui.py, geoelevation\elevation_manager.py, geoelevation\image_processor.py, ...)
# math (Used in: geoelevation\elevation_gui.py, geoelevation\elevation_manager.py, geoelevation\image_processor.py)
# multiprocessing (Used in: geoelevation\__main__.py, geoelevation\elevation_gui.py)
# netrc (Used in: geoelevation\elevation_manager.py)
# os (Used in: geoelevation\__main__.py, geoelevation\elevation_gui.py, geoelevation\elevation_manager.py, ...)
# re (Used in: geoelevation\_version.py)
# sys (Used in: geoelevation\__main__.py)
# threading (Used in: geoelevation\elevation_gui.py)
# time (Used in: geoelevation\elevation_manager.py, geoelevation\visualizer.py)
# tkinter (Used in: geoelevation\__main__.py, geoelevation\elevation_gui.py)
# traceback (Used in: geoelevation\__main__.py, geoelevation\elevation_gui.py)
# typing (Used in: geoelevation\elevation_gui.py, geoelevation\elevation_manager.py, geoelevation\image_processor.py, ...)
# zipfile (Used in: geoelevation\elevation_manager.py)
# --- External Dependencies (for pip install) ---
# Found (imported as 'PIL') in: geoelevation\image_processor.py, geoelevation\visualizer.py
# Detected version: 11.2.1
Pillow
# Found in: geoelevation\visualizer.py
# Detected version: 3.10.1
matplotlib
# Found in: geoelevation\elevation_gui.py, geoelevation\elevation_manager.py, geoelevation\visualizer.py
# Detected version: 2.2.5
numpy
# Found in: geoelevation\elevation_manager.py
# Detected version: 1.4.3
rasterio
# Found in: geoelevation\elevation_manager.py
# Detected version: 2.32.3
requests
# Found in: geoelevation\visualizer.py
# Detected version: 1.15.2
scipy

7
source_c/CMakeLists.txt Normal file
View File

@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.10)
project(GeoElevationCpp)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(elevation_cli main.cpp ElevationReader.cpp)

View File

@ -0,0 +1,134 @@
#include "ElevationReader.h"
#include <cmath>
#include <fstream>
#include <iostream>
#include <sstream>
#include <iomanip>
#include <limits>
// The value used within HGT files to represent "no data" points (e.g., water bodies).
// This corresponds to the minimum possible value for a signed 16-bit integer.
constexpr int16_t HGT_NO_DATA_VALUE = std::numeric_limits<int16_t>::min(); // -32768
ElevationReader::ElevationReader(const std::string& hgt_directory)
: hgt_dir_path_(hgt_directory) {}
double ElevationReader::get_elevation(double latitude, double longitude) {
// Validate input coordinates to be within the standard WGS 84 range.
if (latitude < -90.0 || latitude >= 90.0 || longitude < -180.0 || longitude >= 180.0) {
return USER_NO_DATA_VALUE;
}
// Determine which tile file is needed for the given coordinates.
std::string basename = get_tile_basename(latitude, longitude);
// Load the tile from disk if it's not already in the cache.
if (!ensure_tile_is_loaded(basename)) {
// This occurs if the file doesn't exist or is invalid (e.g., wrong size).
std::cerr << "Warning: Tile file for " << basename << ".hgt not found or is invalid." << std::endl;
return USER_NO_DATA_VALUE;
}
// Retrieve the tile data from the cache. `at()` is used for safe access.
const auto& data = tile_cache_.at(basename);
// Calculate the fractional part of the coordinates to find the position within the 1x1 degree tile.
double lat_fractional = latitude - floor(latitude);
double lon_fractional = longitude - floor(longitude);
// IMPORTANT: HGT data is stored in rows from North to South (top to bottom).
// A higher latitude corresponds to a lower row index. So, we invert the latitude logic.
// Example: For N45 tile (45-46 deg), lat 45.99 is near row 0; lat 45.01 is near row 3600.
int row = static_cast<int>(round((1.0 - lat_fractional) * (HGT_GRID_DIMENSION - 1)));
int col = static_cast<int>(round(lon_fractional * (HGT_GRID_DIMENSION - 1)));
// Calculate the 1D index into the data vector.
size_t index = static_cast<size_t>(row) * HGT_GRID_DIMENSION + col;
// Safety check to prevent out-of-bounds access.
if (index >= data.size()) {
return USER_NO_DATA_VALUE;
}
int16_t elevation = data[index];
// Check if the point corresponds to a "no data" value within the file.
if (elevation == HGT_NO_DATA_VALUE) {
return USER_NO_DATA_VALUE;
}
return static_cast<double>(elevation);
}
std::string ElevationReader::get_tile_basename(double latitude, double longitude) const {
// The HGT filename convention is based on the integer coordinates of the bottom-left (SW) corner.
int lat_int = static_cast<int>(floor(latitude));
int lon_int = static_cast<int>(floor(longitude));
char lat_char = (lat_int >= 0) ? 'N' : 'S';
char lon_char = (lon_int >= 0) ? 'E' : 'W';
// Use a stringstream for safe and clean string formatting.
std::stringstream ss;
ss << lat_char << std::setfill('0') << std::setw(2) << std::abs(lat_int)
<< lon_char << std::setfill('0') << std::setw(3) << std::abs(lon_int);
return ss.str();
}
bool ElevationReader::ensure_tile_is_loaded(const std::string& tile_basename) {
// 1. Check if the tile is already in our memory cache to avoid disk I/O.
if (tile_cache_.count(tile_basename)) {
return true;
}
// 2. Construct the full path to the .hgt file.
std::string file_path = hgt_dir_path_ + "/" + tile_basename + ".hgt";
std::ifstream file(file_path, std::ios::binary);
if (!file) {
return false; // File does not exist or cannot be opened.
}
// 3. Validate the file size to ensure it's a complete 1-arc-second tile.
file.seekg(0, std::ios::end);
std::streampos file_size = file.tellg();
file.seekg(0, std::ios::beg);
const std::streampos expected_size = HGT_GRID_DIMENSION * HGT_GRID_DIMENSION * sizeof(int16_t);
if (file_size != expected_size) {
std::cerr << "Warning: Invalid file size for " << file_path
<< ". Expected " << expected_size << " bytes, but got " << file_size << " bytes." << std::endl;
return false;
}
// 4. Read the entire file into a vector of 16-bit integers.
std::vector<int16_t> tile_data(HGT_GRID_DIMENSION * HGT_GRID_DIMENSION);
file.read(reinterpret_cast<char*>(tile_data.data()), expected_size);
if (!file) {
std::cerr << "Error: Failed to read all data from " << file_path << std::endl;
return false;
}
// 5. CRITICAL STEP: Convert every value from Big-Endian to the host's native endianness.
for (auto& val : tile_data) {
val = byteswap(val);
}
// 6. Store the loaded and processed data in the cache using std::move for efficiency.
tile_cache_[tile_basename] = std::move(tile_data);
return true;
}
int16_t ElevationReader::byteswap(int16_t value) const {
// Cast to unsigned to prevent issues with sign extension during bit shifts on negative numbers.
uint16_t u_value = static_cast<uint16_t>(value);
// Example: 0x1234 (Big Endian) becomes 0x3412 (Little Endian)
// - (u_value & 0xFF00) >> 8 -> (0x1200) >> 8 -> 0x0012
// - (u_value & 0x00FF) << 8 -> (0x0034) << 8 -> 0x3400
// - 0x0012 | 0x3400 = 0x3412
return static_cast<int16_t>(((u_value & 0xFF00) >> 8) | ((u_value & 0x00FF) << 8));
}

View File

@ -0,0 +1,83 @@
#pragma once
#include <string>
#include <vector>
#include <map>
// A constant for the expected grid dimension of 1-arc-second NASADEM HGT files.
constexpr int HGT_GRID_DIMENSION = 3601;
// The special value returned to the user when elevation data is not available.
constexpr double USER_NO_DATA_VALUE = -999.0;
/**
* @class ElevationReader
* @brief Reads elevation data from .hgt files downloaded by the GeoElevation tool.
*
* This class manages the loading, caching, and querying of 1-arc-second NASADEM .hgt files.
* It is designed to find the correct tile for a given coordinate, handle the binary
* file reading, and correctly interpret the big-endian data format used in HGT files.
*/
class ElevationReader {
public:
/**
* @brief Constructs an ElevationReader.
* @param hgt_directory The path to the directory containing the .hgt tile files.
*/
explicit ElevationReader(const std::string& hgt_directory);
/**
* @brief Gets the elevation for a specific geographic coordinate.
*
* This is the main public method of the class. It orchestrates finding the correct
* tile, loading it if necessary, and calculating the elevation.
*
* @param latitude The latitude in decimal degrees (e.g., 45.072).
* @param longitude The longitude in decimal degrees (e.g., 7.685).
* @return The elevation in meters as a double. Returns USER_NO_DATA_VALUE (-999.0)
* if the corresponding .hgt tile is not found, coordinates are invalid,
* or if the point corresponds to a "no data" value in the file.
*/
double get_elevation(double latitude, double longitude);
private:
/**
* @brief Generates the base filename (e.g., "N45E007") for a given coordinate.
* @param latitude The latitude of the point.
* @param longitude The longitude of the point.
* @return The standard base filename for the tile.
*/
std::string get_tile_basename(double latitude, double longitude) const;
/**
* @brief Loads a tile from disk into the cache if it's not already present.
*
* This method implements a lazy-loading strategy. A tile is only read from disk
* the first time it is needed. Subsequent requests for the same tile will use
* the in-memory cache.
*
* @param tile_basename The base name of the tile to load (e.g., "N45E007").
* @return True if the tile was loaded successfully or was already in the cache, false otherwise.
*/
bool ensure_tile_is_loaded(const std::string& tile_basename);
/**
* @brief Converts a 16-bit integer from big-endian to the host system's endianness.
*
* HGT files store data in big-endian (network byte order), while most modern CPUs
* (x86/x64) are little-endian. This byte swap is a critical step to correctly
* interpret the elevation values.
*
* @param value The big-endian short value read from the file.
* @return The value converted to the host system's native byte order.
*/
int16_t byteswap(int16_t value) const;
// The path to the directory where .hgt files are stored.
std::string hgt_dir_path_;
// In-memory cache for tile data to avoid repeated, slow file reads from disk.
// The key is the tile basename (e.g., "N45E007"), the value is the elevation data.
std::map<std::string, std::vector<int16_t>> tile_cache_;
};

BIN
source_c/elevation_cli.exe Normal file

Binary file not shown.

58
source_c/main.cpp Normal file
View File

@ -0,0 +1,58 @@
#include <iostream>
#include <string>
#include <stdexcept>
#include "ElevationReader.h"
void print_usage()
{
std::cerr << "Usage: elevation_cli <path_to_hgt_directory> <latitude> <longitude>" << std::endl;
std::cerr << "Example: elevation_cli ../geoelevation_dem_cache/hgt_tiles 45.07 7.68" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 4)
{
print_usage();
return 1;
}
std::string hgt_dir;
double lat, lon;
try
{
hgt_dir = argv[1];
lat = std::stod(argv[2]);
lon = std::stod(argv[3]);
} catch (const std::invalid_argument& e)
{
std::cerr << "Error: Invalid latitude or longitude format." << std::endl;
print_usage();
return 1;
} catch (const std::out_of_range& e)
{
std::cerr << "Error: Latitude or longitude value is out of range." << std::endl;
print_usage();
return 1;
}
try
{
ElevationReader reader(hgt_dir);
double elevation = reader.get_elevation(lat, lon);
std::cout << "Querying for Latitude=" << lat << ", Longitude=" << lon << std::endl;
if (elevation == USER_NO_DATA_VALUE)
{
std::cout << "Result: Elevation data not available for this location." << std::endl;
}
else
{
std::cout << "Result: Elevation is " << elevation << " meters." << std::endl;
}
} catch (const std::exception& e)
{
std::cerr << "An unexpected error occurred: " << e.what() << std::endl;
return 1;
}
return 0;
}

192
todo.md Normal file
View File

@ -0,0 +1,192 @@
Ciao! Ho analizzato con molto interesse il codice della tua applicazione `geoelevation`. Prima di tutto, complimenti per la struttura: è ben organizzata, modulare e fa un uso corretto del multiprocessing per non bloccare la GUI, una pratica eccellente. Ho compreso a fondo il suo funzionamento: scarica, gestisce in cache e visualizza dati DEM, sia in 2D che in 3D, tramite una GUI Tkinter.
Hai colto esattamente il punto debole della visualizzazione 3D attuale: **Matplotlib è una libreria di plotting, non un motore di rendering 3D in tempo reale.** È fantastica per creare grafici e visualizzazioni statiche o pre-calcolate, ma non è progettata per un'interazione fluida e reattiva come quella di un videogioco o di un simulatore. La sua performance degrada rapidamente con l'aumentare dei dati e non è ottimizzata per l'accelerazione hardware (GPU) nel modo in cui lo sono le librerie grafiche moderne.
### La tua richiesta: è possibile realizzarla?
La risposta breve è: **assolutamente sì!** Quello che descrivi è un obiettivo molto realistico e rientra nel campo della visualizzazione scientifica interattiva e della simulazione. Per realizzarlo, dobbiamo semplicemente sostituire Matplotlib con una libreria più adatta a questo scopo.
L'idea è di creare una scena 3D in cui il terreno (la mappa DEM) è un oggetto fisso e la camera (il tuo punto di vista) o un altro oggetto (l'aereo/elicottero) possono muoversi liberamente. Le librerie che ti propongo sono pensate per sfruttare la GPU, garantendo una fluidità che Matplotlib non potrà mai offrire.
### Librerie consigliate per il tuo scopo
Considerando il tuo background in Python e C++, ecco alcune opzioni eccellenti, dalla più semplice e scientifica alla più completa e "game-oriented":
1. **PyVista**: Questa è probabilmente la **scelta migliore e più diretta** per il tuo caso d'uso.
* **Perché?**: PyVista è un'interfaccia Python di alto livello per la potentissima libreria VTK (Visualization Toolkit), che è scritta in C++. È pensata specificamente per la visualizzazione scientifica 3D. Gestisce nativamente mesh complesse (come il tuo terreno), è estremamente performante e ha già funzionalità integrate per fare esattamente quello che chiedi, come il "ray tracing" (lanciare un raggio per vedere dove colpisce una superficie), perfetto per simulare la linea di vista del radar. L'integrazione con NumPy è eccellente.
2. **Panda3D**: Un motore di gioco open-source molto maturo, sviluppato originariamente dalla Disney e ora mantenuto dalla Carnegie Mellon.
* **Perché?**: È scritto in C++ ma il suo utilizzo primario è tramite Python, il che lo rende molto accessibile per te. Essendo un game engine, ha tutto il necessario per gestire camere, luci, input dell'utente e scene complesse in modo molto efficiente. Potrebbe essere un'ottima scelta se vuoi spingere molto sull'aspetto "simulazione di volo".
3. **Godot (con Godot-Python)**: Un motore di gioco 2D/3D open-source incredibilmente popolare e potente.
* **Perché?**: Il motore è scritto in C++ e permette di estenderlo con C++, quindi il tuo skillset è perfetto. Grazie a progetti come `godot-python`, puoi scrivere la logica di gioco/simulazione in Python. Questa è l'opzione più potente se vuoi creare un'applicazione quasi-standalone con una fisica più complessa, una UI più ricca all'interno della visualizzazione, ecc.
**Il mio consiglio è di partire con PyVista.** È il passo successivo più logico da Matplotlib per un'applicazione scientifica, è estremamente potente e ti permetterà di raggiungere il tuo obiettivo rapidamente senza dover imparare tutte le complessità di un motore di gioco completo.
### Piano d'azione: Integrare PyVista nella tua applicazione
Ecco come potremmo procedere per integrare una visualizzazione interattiva 3D con PyVista.
**Passo 1: Preparazione dei dati**
Il tuo `hgt_array` da `get_hgt_data` è un array NumPy 2D. Per PyVista, dobbiamo trasformarlo in una mesh 3D. Possiamo farlo creando una griglia strutturata (`StructuredGrid`) dove le coordinate X e Y corrispondono agli indici dell'array e la coordinata Z corrisponde al valore di elevazione.
**Passo 2: Creazione della scena 3D**
Creeremo una scena 3D contenente:
* La mesh del terreno generata dai dati DEM.
* Un oggetto 3D (es. una piccola sfera o un cono) per rappresentare l'aereo/radar.
* Una linea retta che parte dall'aereo per rappresentare la direzione in cui il radar sta "guardando".
* Un marcatore (es. una sfera più piccola) nel punto in cui il raggio del radar interseca il terreno.
**Passo 3: Interattività**
PyVista fornisce di default una finestra interattiva dove puoi usare il mouse per ruotare, muovere (pan) e zoomare la camera. Questo soddisfa già gran parte della tua richiesta di "muovermi nella scena". Possiamo anche aggiungere callback personalizzati per muovere l'oggetto "aereo" con la tastiera.
**Passo 4: Simulazione della linea di vista (Ray Tracing)**
PyVista ha una funzione `ray_trace` che fa esattamente ciò di cui hai bisogno. Dati un punto di origine (la posizione del tuo aereo) e una direzione, calcola il punto esatto in cui questo "raggio" interseca la mesh del terreno. Questo ti darà le coordinate 3D di ciò che il radar sta osservando.
**Passo 5: Integrazione nell'architettura esistente**
Manterremo la tua ottima architettura.
* Creeremo un nuovo modulo, ad esempio `interactive_viewer.py`.
* In `process_targets.py`, aggiungeremo una nuova funzione target per il multiprocessing, ad esempio `process_target_show_interactive_3d`.
* Questa funzione riceverà l'array NumPy dei dati DEM, proprio come fa `process_target_show_3d`.
* All'interno di questa funzione, verrà eseguito tutto il codice PyVista per creare e mostrare la scena.
* La GUI Tkinter lancerà questo nuovo processo quando l'utente clicca un nuovo pulsante "Interactive 3D View".
In questo modo, la finestra 3D interattiva sarà completamente separata dalla GUI, non la bloccherà e sfrutterà al massimo le risorse del PC per il rendering.
### Esempio pratico con PyVista
Per darti un'idea concreta, ecco un piccolo script di esempio autonomo che dimostra i concetti chiave. Puoi eseguirlo dopo aver installato PyVista (`pip install pyvista`).
```python
# File: pyvista_dem_example.py
# Description: A standalone example demonstrating interactive 3D DEM visualization with PyVista.
import numpy as np
import pyvista as pv
def create_sample_dem_data(width=100, height=100):
"""Creates a sample NumPy array representing DEM data for demonstration."""
x = np.arange(-width / 2, width / 2, 1)
y = np.arange(-height / 2, height / 2, 1)
xx, yy = np.meshgrid(x, y)
# Create some interesting terrain with hills and valleys
z = (np.sin(np.sqrt(xx**2 + yy**2) * 0.3) * 20 +
np.cos(xx * 0.2) * 15 +
np.sin(yy * 0.3) * 10)
# Add a larger mountain feature
mountain_x, mountain_y = 20, -15
mountain_height = 80
mountain_spread = 25
distance_from_peak = np.sqrt((xx - mountain_x)**2 + (yy - mountain_y)**2)
mountain = mountain_height * np.exp(-(distance_from_peak**2 / (2 * mountain_spread**2)))
z += mountain
return xx, yy, z
def run_interactive_dem_viewer(dem_data_array: np.ndarray):
"""
Creates and displays an interactive 3D scene from a DEM data array using PyVista.
Args:
dem_data_array (np.ndarray): 2D NumPy array of elevation values.
"""
if not dem_data_array.any():
print("Error: DEM data is empty.")
return
# 1. Create a 3D mesh from the 2D NumPy array.
# We use a StructuredGrid, which is perfect for this kind of data.
# Get dimensions
height, width = dem_data_array.shape
# Create X and Y coordinates corresponding to the array indices
x_coords = np.arange(width)
y_coords = np.arange(height)
# Create a meshgrid for X and Y
x_grid, y_grid = np.meshgrid(x_coords, y_coords)
# The Z coordinates are the elevation values from the array itself.
# We need to flatten all coordinate arrays for PyVista.
z_grid = dem_data_array
# Create the StructuredGrid object
terrain_mesh = pv.StructuredGrid(x_grid, y_grid, z_grid)
# Optional: We can scale the Z axis to exaggerate elevation
terrain_mesh.points[:, 2] *= 0.3 # Make elevation less extreme for better viewing
# 2. Set up the 3D plotter/scene
plotter = pv.Plotter(window_size=[1200, 800])
plotter.add_mesh(terrain_mesh, cmap='terrain', show_edges=False)
# 3. Define the observer (radar) position and direction
observer_position = np.array([width / 2, height / 2, 150]) # Centered, high above terrain
observer_direction = np.array([0.8, 0.8, -0.6]) # Pointing forward, right, and down
observer_direction /= np.linalg.norm(observer_direction) # Normalize the direction vector
# 4. Add visual objects to the scene
# Observer (e.g., a red sphere)
observer_actor = plotter.add_mesh(
pv.Sphere(radius=2, center=observer_position),
color='red',
label='Observer'
)
# Radar beam (a line/tube)
ray_length = 200
ray_end_point = observer_position + observer_direction * ray_length
radar_beam_actor = plotter.add_mesh(
pv.Tube(pointa=observer_position, pointb=ray_end_point, radius=0.5),
color='cyan',
label='Radar Beam'
)
# 5. Perform Ray Tracing to find the intersection point
# plotter.ray_trace returns a list of intersection points and cell indices
intersection_points, intersection_cells = plotter.ray_trace(observer_position, ray_end_point)
target_marker_actor = None
if len(intersection_points) > 0:
# The first intersection point is what the radar "sees"
target_point = intersection_points[0]
print(f"Radar beam intersects terrain at coordinates: {target_point}")
# Add a marker at the intersection point (e.g., a yellow sphere)
target_marker_actor = plotter.add_mesh(
pv.Sphere(radius=1.5, center=target_point),
color='yellow',
label='Target'
)
else:
print("Radar beam does not intersect the terrain (pointing at the sky).")
# 6. Customize the scene and show it
plotter.add_axes()
plotter.add_legend()
plotter.camera_position = 'xy' # Set initial camera view
plotter.camera.zoom(1.5)
print("\n--- Interactive Controls ---")
print("Mouse Left: Rotate")
print("Mouse Middle / Shift+Left: Pan")
print("Mouse Right / Scroll: Zoom")
print("Press 'q' to close the window.")
# This call is blocking and opens the interactive window
plotter.show()
if __name__ == '__main__':
# Generate sample data
_, _, dem_data = create_sample_dem_data(width=200, height=200)
# Run the interactive viewer
run_interactive_dem_viewer(dem_data)
```
### Prossimi passi
1. **Sperimenta con PyVista**: Ti consiglio di installare `pyvista` e provare l'esempio che ti ho fornito. Gioca con i controlli del mouse per vedere la fluidità e l'interattività.
2. **Integrazione**: Se ti piace come funziona, possiamo discutere nel dettaglio come integrarlo. Potremmo modificare il modulo `visualizer.py` per aggiungere la nuova funzione che usa PyVista e aggiornare `process_targets.py` e `elevation_gui.py` per poterla richiamare.
Questo approccio ti darà una visualizzazione 3D performante, interattiva e scientificamente accurata, perfettamente in linea con le tue necessità. Fammi sapere cosa ne pensi e se vuoi procedere con l'integrazione