Add WMS support and measurement functionality

- Implemented WMS layer handling for Slovenian aerial imagery and overlays for cadastral parcels and forest roads.
- Added distance measurement feature allowing users to measure distances on the map with long press interactions.
- Updated localization files for Croatian, Italian, and Slovenian languages to include new measurement and WMS-related strings.
- Enhanced the map layer model to support WMS-specific properties.
- Introduced a new tile provider for debugging WMS requests.
- Updated the drawing toolbar to include measurement mode and clear measurement functionality.
- Integrated SharedPreferences to persist overlay visibility states across app sessions.
- Added utility functions for handling the Slovenian coordinate reference system (EPSG:3794).
This commit is contained in:
Janez T
2025-10-27 21:49:58 +01:00
parent a03cc4a363
commit a4dc85d130
29 changed files with 3327 additions and 20 deletions

View File

@@ -649,6 +649,44 @@
"description": "Beschreibung für Rechteckzeichnungsmodus"
},
"measureDistance": "Entfernung messen",
"@measureDistance": {
"description": "Kartenzeichnungsmodus: Entfernung messen"
},
"measureDistanceDesc": "Zwei Punkte lang drücken zum Messen",
"@measureDistanceDesc": {
"description": "Beschreibung für Entfernungsmessungsmodus"
},
"clearMeasurement": "Messung löschen",
"@clearMeasurement": {
"description": "Tooltip zum Löschen der Messung"
},
"distanceLabel": "Entfernung: {distance}",
"@distanceLabel": {
"description": "Beschriftung mit gemessener Entfernung",
"placeholders": {
"distance": {"type": "String"}
}
},
"longPressForSecondPoint": "Langer Druck für zweiten Punkt",
"@longPressForSecondPoint": {
"description": "Anweisung wenn erster Messpunkt gesetzt ist"
},
"longPressToStartMeasurement": "Langer Druck für ersten Punkt",
"@longPressToStartMeasurement": {
"description": "Anweisung zum Starten der Messung"
},
"longPressToStartNewMeasurement": "Langer Druck für neue Messung",
"@longPressToStartNewMeasurement": {
"description": "Anweisung zum Neustarten der Messung nach Abschluss"
},
"shareDrawings": "Zeichnungen teilen",
"@shareDrawings": {
"description": "Aktion zum Teilen von Zeichnungen im Netzwerk"
@@ -2551,5 +2589,11 @@
"currentVersion": "Aktuell",
"latestVersion": "Neueste",
"downloadUpdate": "Herunterladen",
"updateLater": "Später"
"updateLater": "Später",
"cadastralParcels": "Katasterparzellen",
"forestRoads": "Waldwege",
"showCadastralParcels": "Katasterparzellen anzeigen",
"showForestRoads": "Waldwege anzeigen",
"wmsOverlays": "WMS Überlagerungen"
}

View File

@@ -649,6 +649,44 @@
"description": "Description for rectangle drawing mode"
},
"measureDistance": "Measure Distance",
"@measureDistance": {
"description": "Map drawing mode: measure distance"
},
"measureDistanceDesc": "Long press two points to measure",
"@measureDistanceDesc": {
"description": "Description for distance measurement mode"
},
"clearMeasurement": "Clear Measurement",
"@clearMeasurement": {
"description": "Tooltip to clear measurement"
},
"distanceLabel": "Distance: {distance}",
"@distanceLabel": {
"description": "Label showing measured distance",
"placeholders": {
"distance": {"type": "String"}
}
},
"longPressForSecondPoint": "Long press for second point",
"@longPressForSecondPoint": {
"description": "Instruction when first measurement point is set"
},
"longPressToStartMeasurement": "Long press to set first point",
"@longPressToStartMeasurement": {
"description": "Instruction to start measurement"
},
"longPressToStartNewMeasurement": "Long press to start new measurement",
"@longPressToStartNewMeasurement": {
"description": "Instruction to restart measurement after completion"
},
"shareDrawings": "Share Drawings",
"@shareDrawings": {
"description": "Action to share drawings to network"
@@ -3163,5 +3201,30 @@
"updateLater": "Later",
"@updateLater": {
"description": "Button to dismiss update dialog"
},
"cadastralParcels": "Cadastral Parcels",
"@cadastralParcels": {
"description": "Label for cadastral parcels WMS overlay layer"
},
"forestRoads": "Forest Roads",
"@forestRoads": {
"description": "Label for forest roads WMS overlay layer"
},
"showCadastralParcels": "Show Cadastral Parcels",
"@showCadastralParcels": {
"description": "Tooltip for cadastral parcels overlay toggle button"
},
"showForestRoads": "Show Forest Roads",
"@showForestRoads": {
"description": "Tooltip for forest roads overlay toggle button"
},
"wmsOverlays": "WMS Overlays",
"@wmsOverlays": {
"description": "Section header for WMS overlay layers in layer selector"
}
}

View File

@@ -649,6 +649,44 @@
"description": "Descripción del modo de dibujo de rectángulo"
},
"measureDistance": "Medir distancia",
"@measureDistance": {
"description": "Modo de dibujo del mapa: medir distancia"
},
"measureDistanceDesc": "Presión prolongada en dos puntos para medir",
"@measureDistanceDesc": {
"description": "Descripción del modo de medición de distancia"
},
"clearMeasurement": "Borrar medición",
"@clearMeasurement": {
"description": "Tooltip para borrar la medición"
},
"distanceLabel": "Distancia: {distance}",
"@distanceLabel": {
"description": "Etiqueta que muestra la distancia medida",
"placeholders": {
"distance": {"type": "String"}
}
},
"longPressForSecondPoint": "Presión prolongada para el segundo punto",
"@longPressForSecondPoint": {
"description": "Instrucción cuando se ha establecido el primer punto de medición"
},
"longPressToStartMeasurement": "Presión prolongada para establecer el primer punto",
"@longPressToStartMeasurement": {
"description": "Instrucción para comenzar la medición"
},
"longPressToStartNewMeasurement": "Presión prolongada para nueva medición",
"@longPressToStartNewMeasurement": {
"description": "Instrucción para reiniciar la medición después de completarla"
},
"shareDrawings": "Compartir dibujos",
"@shareDrawings": {
"description": "Acción para compartir dibujos a la red"
@@ -2546,5 +2584,11 @@
"currentVersion": "Actual",
"latestVersion": "Última",
"downloadUpdate": "Descargar",
"updateLater": "Más Tarde"
"updateLater": "Más Tarde",
"cadastralParcels": "Parcelas Catastrales",
"forestRoads": "Caminos Forestales",
"showCadastralParcels": "Mostrar parcelas catastrales",
"showForestRoads": "Mostrar caminos forestales",
"wmsOverlays": "Superposiciones WMS"
}

View File

@@ -649,6 +649,44 @@
"description": "Description du mode de dessin de rectangle"
},
"measureDistance": "Mesurer la distance",
"@measureDistance": {
"description": "Mode de dessin de carte : mesurer la distance"
},
"measureDistanceDesc": "Appui long sur deux points pour mesurer",
"@measureDistanceDesc": {
"description": "Description du mode de mesure de distance"
},
"clearMeasurement": "Effacer la mesure",
"@clearMeasurement": {
"description": "Infobulle pour effacer la mesure"
},
"distanceLabel": "Distance : {distance}",
"@distanceLabel": {
"description": "Étiquette affichant la distance mesurée",
"placeholders": {
"distance": {"type": "String"}
}
},
"longPressForSecondPoint": "Appui long pour le deuxième point",
"@longPressForSecondPoint": {
"description": "Instruction lorsque le premier point de mesure est défini"
},
"longPressToStartMeasurement": "Appui long pour définir le premier point",
"@longPressToStartMeasurement": {
"description": "Instruction pour commencer la mesure"
},
"longPressToStartNewMeasurement": "Appui long pour nouvelle mesure",
"@longPressToStartNewMeasurement": {
"description": "Instruction pour redémarrer la mesure après achèvement"
},
"shareDrawings": "Partager les dessins",
"@shareDrawings": {
"description": "Action pour partager les dessins sur le réseau"
@@ -2551,5 +2589,11 @@
"currentVersion": "Actuelle",
"latestVersion": "Dernière",
"downloadUpdate": "Télécharger",
"updateLater": "Plus Tard"
"updateLater": "Plus Tard",
"cadastralParcels": "Parcelles Cadastrales",
"forestRoads": "Chemins Forestiers",
"showCadastralParcels": "Afficher les parcelles cadastrales",
"showForestRoads": "Afficher les chemins forestiers",
"wmsOverlays": "Superpositions WMS"
}

View File

@@ -227,6 +227,20 @@
"drawRectangleDesc": "Nacrtaj pravokutno područje na karti",
"measureDistance": "Izmjeri udaljenost",
"measureDistanceDesc": "Dugi pritisak na dvije točke za mjerenje",
"clearMeasurement": "Očisti mjerenje",
"distanceLabel": "Udaljenost: {distance}",
"longPressForSecondPoint": "Dugi pritisak za drugu točku",
"longPressToStartMeasurement": "Dugi pritisak za prvu točku",
"longPressToStartNewMeasurement": "Dugi pritisak za novo mjerenje",
"shareDrawings": "Podijeli crteže",
"clearAllDrawings": "Očisti sve crteže",
@@ -955,5 +969,11 @@
"currentVersion": "Trenutna",
"latestVersion": "Najnovija",
"downloadUpdate": "Preuzmi",
"updateLater": "Kasnije"
"updateLater": "Kasnije",
"cadastralParcels": "Katastarske čestice",
"forestRoads": "Šumske ceste",
"showCadastralParcels": "Prikaži katastarske čestice",
"showForestRoads": "Prikaži šumske ceste",
"wmsOverlays": "WMS Prekrivanja"
}

View File

@@ -649,6 +649,44 @@
"description": "Descrizione per la modalità disegno rettangolo"
},
"measureDistance": "Misura Distanza",
"@measureDistance": {
"description": "Modalità disegno mappa: misura distanza"
},
"measureDistanceDesc": "Premi a lungo su due punti per misurare",
"@measureDistanceDesc": {
"description": "Descrizione per la modalità misurazione distanza"
},
"clearMeasurement": "Cancella Misurazione",
"@clearMeasurement": {
"description": "Tooltip per cancellare la misurazione"
},
"distanceLabel": "Distanza: {distance}",
"@distanceLabel": {
"description": "Etichetta che mostra la distanza misurata",
"placeholders": {
"distance": {"type": "String"}
}
},
"longPressForSecondPoint": "Premi a lungo per il secondo punto",
"@longPressForSecondPoint": {
"description": "Istruzione quando è impostato il primo punto di misurazione"
},
"longPressToStartMeasurement": "Premi a lungo per impostare il primo punto",
"@longPressToStartMeasurement": {
"description": "Istruzione per avviare la misurazione"
},
"longPressToStartNewMeasurement": "Premi a lungo per nuova misurazione",
"@longPressToStartNewMeasurement": {
"description": "Istruzione per riavviare la misurazione dopo il completamento"
},
"shareDrawings": "Condividi Disegni",
"@shareDrawings": {
"description": "Azione per condividere disegni sulla rete"
@@ -2551,5 +2589,11 @@
"currentVersion": "Attuale",
"latestVersion": "Ultima",
"downloadUpdate": "Scarica",
"updateLater": "Più Tardi"
"updateLater": "Più Tardi",
"cadastralParcels": "Particelle Catastali",
"forestRoads": "Strade Forestali",
"showCadastralParcels": "Mostra particelle catastali",
"showForestRoads": "Mostra strade forestali",
"wmsOverlays": "Sovrapposizioni WMS"
}

View File

@@ -791,6 +791,48 @@ abstract class AppLocalizations {
/// **'Draw a rectangular area on the map'**
String get drawRectangleDesc;
/// Map drawing mode: measure distance
///
/// In en, this message translates to:
/// **'Measure Distance'**
String get measureDistance;
/// Description for distance measurement mode
///
/// In en, this message translates to:
/// **'Long press two points to measure'**
String get measureDistanceDesc;
/// Tooltip to clear measurement
///
/// In en, this message translates to:
/// **'Clear Measurement'**
String get clearMeasurement;
/// Label showing measured distance
///
/// In en, this message translates to:
/// **'Distance: {distance}'**
String distanceLabel(String distance);
/// Instruction when first measurement point is set
///
/// In en, this message translates to:
/// **'Long press for second point'**
String get longPressForSecondPoint;
/// Instruction to start measurement
///
/// In en, this message translates to:
/// **'Long press to set first point'**
String get longPressToStartMeasurement;
/// Instruction to restart measurement after completion
///
/// In en, this message translates to:
/// **'Long press to start new measurement'**
String get longPressToStartNewMeasurement;
/// Action to share drawings to network
///
/// In en, this message translates to:
@@ -3374,6 +3416,36 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Later'**
String get updateLater;
/// Label for cadastral parcels WMS overlay layer
///
/// In en, this message translates to:
/// **'Cadastral Parcels'**
String get cadastralParcels;
/// Label for forest roads WMS overlay layer
///
/// In en, this message translates to:
/// **'Forest Roads'**
String get forestRoads;
/// Tooltip for cadastral parcels overlay toggle button
///
/// In en, this message translates to:
/// **'Show Cadastral Parcels'**
String get showCadastralParcels;
/// Tooltip for forest roads overlay toggle button
///
/// In en, this message translates to:
/// **'Show Forest Roads'**
String get showForestRoads;
/// Section header for WMS overlay layers in layer selector
///
/// In en, this message translates to:
/// **'WMS Overlays'**
String get wmsOverlays;
}
class _AppLocalizationsDelegate

View File

@@ -395,6 +395,29 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get drawRectangleDesc => 'Rechteckigen Bereich auf der Karte zeichnen';
@override
String get measureDistance => 'Entfernung messen';
@override
String get measureDistanceDesc => 'Zwei Punkte lang drücken zum Messen';
@override
String get clearMeasurement => 'Messung löschen';
@override
String distanceLabel(String distance) {
return 'Entfernung: $distance';
}
@override
String get longPressForSecondPoint => 'Langer Druck für zweiten Punkt';
@override
String get longPressToStartMeasurement => 'Langer Druck für ersten Punkt';
@override
String get longPressToStartNewMeasurement => 'Langer Druck für neue Messung';
@override
String get shareDrawings => 'Zeichnungen teilen';
@@ -1885,4 +1908,19 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get updateLater => 'Später';
@override
String get cadastralParcels => 'Katasterparzellen';
@override
String get forestRoads => 'Waldwege';
@override
String get showCadastralParcels => 'Katasterparzellen anzeigen';
@override
String get showForestRoads => 'Waldwege anzeigen';
@override
String get wmsOverlays => 'WMS Überlagerungen';
}

View File

@@ -392,6 +392,30 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get drawRectangleDesc => 'Draw a rectangular area on the map';
@override
String get measureDistance => 'Measure Distance';
@override
String get measureDistanceDesc => 'Long press two points to measure';
@override
String get clearMeasurement => 'Clear Measurement';
@override
String distanceLabel(String distance) {
return 'Distance: $distance';
}
@override
String get longPressForSecondPoint => 'Long press for second point';
@override
String get longPressToStartMeasurement => 'Long press to set first point';
@override
String get longPressToStartNewMeasurement =>
'Long press to start new measurement';
@override
String get shareDrawings => 'Share Drawings';
@@ -1864,4 +1888,19 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get updateLater => 'Later';
@override
String get cadastralParcels => 'Cadastral Parcels';
@override
String get forestRoads => 'Forest Roads';
@override
String get showCadastralParcels => 'Show Cadastral Parcels';
@override
String get showForestRoads => 'Show Forest Roads';
@override
String get wmsOverlays => 'WMS Overlays';
}

View File

@@ -393,6 +393,33 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get drawRectangleDesc => 'Dibujar un área rectangular en el mapa';
@override
String get measureDistance => 'Medir distancia';
@override
String get measureDistanceDesc =>
'Presión prolongada en dos puntos para medir';
@override
String get clearMeasurement => 'Borrar medición';
@override
String distanceLabel(String distance) {
return 'Distancia: $distance';
}
@override
String get longPressForSecondPoint =>
'Presión prolongada para el segundo punto';
@override
String get longPressToStartMeasurement =>
'Presión prolongada para establecer el primer punto';
@override
String get longPressToStartNewMeasurement =>
'Presión prolongada para nueva medición';
@override
String get shareDrawings => 'Compartir dibujos';
@@ -1886,4 +1913,19 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get updateLater => 'Más Tarde';
@override
String get cadastralParcels => 'Parcelas Catastrales';
@override
String get forestRoads => 'Caminos Forestales';
@override
String get showCadastralParcels => 'Mostrar parcelas catastrales';
@override
String get showForestRoads => 'Mostrar caminos forestales';
@override
String get wmsOverlays => 'Superposiciones WMS';
}

View File

@@ -395,6 +395,31 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get drawRectangleDesc => 'Tracer une zone rectangulaire sur la carte';
@override
String get measureDistance => 'Mesurer la distance';
@override
String get measureDistanceDesc => 'Appui long sur deux points pour mesurer';
@override
String get clearMeasurement => 'Effacer la mesure';
@override
String distanceLabel(String distance) {
return 'Distance : $distance';
}
@override
String get longPressForSecondPoint => 'Appui long pour le deuxième point';
@override
String get longPressToStartMeasurement =>
'Appui long pour définir le premier point';
@override
String get longPressToStartNewMeasurement =>
'Appui long pour nouvelle mesure';
@override
String get shareDrawings => 'Partager les dessins';
@@ -1892,4 +1917,19 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get updateLater => 'Plus Tard';
@override
String get cadastralParcels => 'Parcelles Cadastrales';
@override
String get forestRoads => 'Chemins Forestiers';
@override
String get showCadastralParcels => 'Afficher les parcelles cadastrales';
@override
String get showForestRoads => 'Afficher les chemins forestiers';
@override
String get wmsOverlays => 'Superpositions WMS';
}

View File

@@ -392,6 +392,29 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get drawRectangleDesc => 'Nacrtaj pravokutno područje na karti';
@override
String get measureDistance => 'Izmjeri udaljenost';
@override
String get measureDistanceDesc => 'Dugi pritisak na dvije točke za mjerenje';
@override
String get clearMeasurement => 'Očisti mjerenje';
@override
String distanceLabel(String distance) {
return 'Udaljenost: $distance';
}
@override
String get longPressForSecondPoint => 'Dugi pritisak za drugu točku';
@override
String get longPressToStartMeasurement => 'Dugi pritisak za prvu točku';
@override
String get longPressToStartNewMeasurement => 'Dugi pritisak za novo mjerenje';
@override
String get shareDrawings => 'Podijeli crteže';
@@ -1874,4 +1897,19 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get updateLater => 'Kasnije';
@override
String get cadastralParcels => 'Katastarske čestice';
@override
String get forestRoads => 'Šumske ceste';
@override
String get showCadastralParcels => 'Prikaži katastarske čestice';
@override
String get showForestRoads => 'Prikaži šumske ceste';
@override
String get wmsOverlays => 'WMS Prekrivanja';
}

View File

@@ -394,6 +394,31 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get drawRectangleDesc => 'Disegna un\'area rettangolare sulla mappa';
@override
String get measureDistance => 'Misura Distanza';
@override
String get measureDistanceDesc => 'Premi a lungo su due punti per misurare';
@override
String get clearMeasurement => 'Cancella Misurazione';
@override
String distanceLabel(String distance) {
return 'Distanza: $distance';
}
@override
String get longPressForSecondPoint => 'Premi a lungo per il secondo punto';
@override
String get longPressToStartMeasurement =>
'Premi a lungo per impostare il primo punto';
@override
String get longPressToStartNewMeasurement =>
'Premi a lungo per nuova misurazione';
@override
String get shareDrawings => 'Condividi Disegni';
@@ -1883,4 +1908,19 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get updateLater => 'Più Tardi';
@override
String get cadastralParcels => 'Particelle Catastali';
@override
String get forestRoads => 'Strade Forestali';
@override
String get showCadastralParcels => 'Mostra particelle catastali';
@override
String get showForestRoads => 'Mostra strade forestali';
@override
String get wmsOverlays => 'Sovrapposizioni WMS';
}

View File

@@ -392,6 +392,29 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get drawRectangleDesc => 'Nariši pravokotno področje na zemljevidu';
@override
String get measureDistance => 'Meri razdaljo';
@override
String get measureDistanceDesc => 'Dolg pritisk na dve točki za merjenje';
@override
String get clearMeasurement => 'Počisti meritev';
@override
String distanceLabel(String distance) {
return 'Razdalja: $distance';
}
@override
String get longPressForSecondPoint => 'Dolg pritisk za drugo točko';
@override
String get longPressToStartMeasurement => 'Dolg pritisk za prvo točko';
@override
String get longPressToStartNewMeasurement => 'Dolg pritisk za novo meritev';
@override
String get shareDrawings => 'Deli risbe';
@@ -1875,4 +1898,19 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get updateLater => 'Kasneje';
@override
String get cadastralParcels => 'Katastrske parcele';
@override
String get forestRoads => 'Gozdne ceste';
@override
String get showCadastralParcels => 'Prikaži katastrske parcele';
@override
String get showForestRoads => 'Prikaži gozdne ceste';
@override
String get wmsOverlays => 'WMS Prekrivanja';
}

View File

@@ -227,6 +227,20 @@
"drawRectangleDesc": "Nariši pravokotno področje na zemljevidu",
"measureDistance": "Meri razdaljo",
"measureDistanceDesc": "Dolg pritisk na dve točki za merjenje",
"clearMeasurement": "Počisti meritev",
"distanceLabel": "Razdalja: {distance}",
"longPressForSecondPoint": "Dolg pritisk za drugo točko",
"longPressToStartMeasurement": "Dolg pritisk za prvo točko",
"longPressToStartNewMeasurement": "Dolg pritisk za novo meritev",
"shareDrawings": "Deli risbe",
"clearAllDrawings": "Počisti vse risbe",
@@ -955,5 +969,11 @@
"currentVersion": "Trenutna",
"latestVersion": "Najnovejša",
"downloadUpdate": "Prenesi",
"updateLater": "Kasneje"
"updateLater": "Kasneje",
"cadastralParcels": "Katastrske parcele",
"forestRoads": "Gozdne ceste",
"showCadastralParcels": "Prikaži katastrske parcele",
"showForestRoads": "Prikaži gozdne ceste",
"wmsOverlays": "WMS Prekrivanja"
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import '../l10n/app_localizations.dart';
enum MapLayerType {
@@ -10,6 +11,7 @@ enum MapLayerType {
googleRoadmap,
googleTerrain,
vectorMbtiles,
wmsBase,
}
class MapLayer {
@@ -26,6 +28,15 @@ class MapLayer {
final String? sourceName;
final bool? isGzipped;
// WMS specific properties
final bool isWms;
final String? wmsBaseUrl;
final List<String>? wmsLayers;
final String? wmsFormat;
final bool? wmsTransparent;
final List<String>? wmsStyles;
final Crs? crs;
const MapLayer({
required this.type,
required this.name,
@@ -37,6 +48,13 @@ class MapLayer {
this.styleUrl,
this.sourceName,
this.isGzipped,
this.isWms = false,
this.wmsBaseUrl,
this.wmsLayers,
this.wmsFormat,
this.wmsTransparent,
this.wmsStyles,
this.crs,
});
/// Get localized name for the layer
@@ -58,6 +76,9 @@ class MapLayer {
case MapLayerType.vectorMbtiles:
// For vector tiles, use the name from metadata
return name;
case MapLayerType.wmsBase:
// For WMS layers, use the name (will be localized separately)
return name;
}
}
@@ -110,6 +131,25 @@ class MapLayer {
maxZoom: 20, // Google Maps maximum
);
/// Slovenian Aerial Imagery 2024 (Ortofoto) - WMS Base Layer
/// Uses EPSG:3794 coordinate system (GeoWebCache tile matrix zoom 0-15)
/// Note: CRS is initialized at runtime in getSlovenianAerial2024()
static MapLayer getSlovenianAerial2024(Crs slovenianCrs) {
return MapLayer(
type: MapLayerType.wmsBase,
name: 'Ortofoto 2024 (Slovenija)',
urlTemplate: '', // Not used for WMS
attribution: '© GURS (Geodetska uprava Republike Slovenije)',
maxZoom: 15, // GeoWebCache tile matrix maximum
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geowebcache/service/wms?',
wmsLayers: const ['pregledovalnik:DOF_2024'],
wmsFormat: 'image/jpeg',
wmsTransparent: false,
crs: slovenianCrs,
);
}
static const List<MapLayer> allLayers = [
openStreetMap,
openTopoMap,
@@ -117,6 +157,7 @@ class MapLayer {
googleHybrid,
googleRoadmap,
googleTerrain,
// Note: Slovenian aerial layer is added dynamically via getSlovenianAerial2024()
];
static MapLayer fromType(MapLayerType type) {

View File

@@ -6,7 +6,7 @@ import '../models/map_drawing.dart';
import '../utils/drawing_message_parser.dart';
/// Drawing mode state
enum DrawingMode { none, line, rectangle }
enum DrawingMode { none, line, rectangle, measure }
/// Provider for managing map drawings
class DrawingProvider with ChangeNotifier {
@@ -26,6 +26,11 @@ class DrawingProvider with ChangeNotifier {
List<LatLng> _currentLinePoints = [];
LatLng? _rectangleStartPoint;
// Distance measurement state
LatLng? _measurementPoint1;
LatLng? _measurementPoint2;
double? _measuredDistance; // in meters
// Getters
DrawingMode get drawingMode => _drawingMode;
Color get selectedColor => _selectedColor;
@@ -46,6 +51,9 @@ class DrawingProvider with ChangeNotifier {
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
LatLng? get rectangleStartPoint => _rectangleStartPoint;
bool get isDrawing => _drawingMode != DrawingMode.none;
LatLng? get measurementPoint1 => _measurementPoint1;
LatLng? get measurementPoint2 => _measurementPoint2;
double? get measuredDistance => _measuredDistance;
/// Initialize and load saved drawings
Future<void> initialize() async {
@@ -126,8 +134,9 @@ class DrawingProvider with ChangeNotifier {
/// Update rectangle end point (for preview)
void updateRectangleEndPoint(LatLng endPoint) {
if (_drawingMode != DrawingMode.rectangle || _rectangleStartPoint == null)
if (_drawingMode != DrawingMode.rectangle || _rectangleStartPoint == null) {
return;
}
// Create preview rectangle
_currentDrawing = RectangleDrawing(
@@ -195,11 +204,47 @@ class DrawingProvider with ChangeNotifier {
notifyListeners();
}
/// Set first measurement point
void setMeasurementPoint1(LatLng point) {
if (_drawingMode != DrawingMode.measure) return;
_measurementPoint1 = point;
_measurementPoint2 = null;
_measuredDistance = null;
notifyListeners();
}
/// Set second measurement point and calculate distance
void setMeasurementPoint2(LatLng point) {
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) return;
_measurementPoint2 = point;
_measuredDistance = _calculateDistance(_measurementPoint1!, point);
notifyListeners();
}
/// Calculate distance between two points using Haversine formula
double _calculateDistance(LatLng point1, LatLng point2) {
const Distance distance = Distance();
return distance.as(LengthUnit.Meter, point1, point2);
}
/// Clear measurement points
void clearMeasurement() {
_measurementPoint1 = null;
_measurementPoint2 = null;
_measuredDistance = null;
notifyListeners();
}
/// Cancel current drawing in progress
void _cancelCurrentDrawing() {
_currentLinePoints = [];
_rectangleStartPoint = null;
_currentDrawing = null;
_measurementPoint1 = null;
_measurementPoint2 = null;
_measuredDistance = null;
}
/// Clear current drawing (public method)

View File

@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/location_trail.dart';
import '../models/map_drawing.dart';
@@ -16,6 +17,10 @@ class MapProvider with ChangeNotifier {
bool _isTrailVisible = true;
final List<LocationTrail> _trailHistory = [];
// WMS overlay toggles
bool _showCadastralOverlay = false;
bool _showForestRoadsOverlay = false;
LatLng? get targetLocation => _targetLocation;
double? get targetZoom => _targetZoom;
bool get shouldAnimate => _shouldAnimate;
@@ -27,6 +32,10 @@ class MapProvider with ChangeNotifier {
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
bool get isTrailActive => _currentTrail?.isActive ?? false;
// WMS overlay getters
bool get showCadastralOverlay => _showCadastralOverlay;
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
void navigateToLocation({
required LatLng location,
double zoom = 15.0,
@@ -207,4 +216,33 @@ class MapProvider with ChangeNotifier {
if (_currentTrail == null) return Duration.zero;
return _currentTrail!.duration;
}
/// Toggle cadastral parcels overlay
Future<void> toggleCadastralOverlay() async {
_showCadastralOverlay = !_showCadastralOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Toggle forest roads overlay
Future<void> toggleForestRoadsOverlay() async {
_showForestRoadsOverlay = !_showForestRoadsOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Load overlay state from SharedPreferences
Future<void> loadOverlayState() async {
final prefs = await SharedPreferences.getInstance();
_showCadastralOverlay = prefs.getBool('map_show_cadastral_overlay') ?? false;
_showForestRoadsOverlay = prefs.getBool('map_show_forest_roads_overlay') ?? false;
notifyListeners();
}
/// Save overlay state to SharedPreferences
Future<void> _saveOverlayState() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('map_show_cadastral_overlay', _showCadastralOverlay);
await prefs.setBool('map_show_forest_roads_overlay', _showForestRoadsOverlay);
}
}

View File

@@ -12,6 +12,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:vector_map_tiles/vector_map_tiles.dart';
import 'package:vector_tile_renderer/vector_tile_renderer.dart' as vtr;
import 'package:http/http.dart' as http;
import '../utils/slovenian_crs.dart';
import '../providers/contacts_provider.dart';
import '../providers/messages_provider.dart';
import '../providers/map_provider.dart';
@@ -75,6 +76,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// MBTiles layers
List<MapLayer> _mbtilesLayers = [];
// WMS layers (Slovenian)
late final MapLayer _slovenianAerialLayer;
// Vector tile theme
vtr.Theme? _vectorTheme;
bool _isLoadingTheme = false;
@@ -110,6 +114,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
@override
void initState() {
super.initState();
// Initialize Slovenian aerial layer with CRS
_slovenianAerialLayer = MapLayer.getSlovenianAerial2024(slovenianCrs);
_loadSettings();
_loadMbtilesLayers();
_initializeTileCache();
@@ -120,6 +126,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
WidgetsBinding.instance.addPostFrameCallback((_) {
final mapProvider = context.read<MapProvider>();
mapProvider.addListener(_handleMapNavigation);
// Load WMS overlay state
mapProvider.loadOverlayState();
// Initialize background location service with BLE service
final appProvider = context.read<AppProvider>();
@@ -239,8 +247,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
}
}
/// Get all available layers (default + MBTiles)
List<MapLayer> get _allLayers => [...MapLayer.allLayers, ..._mbtilesLayers];
/// Get all available layers (default + WMS + MBTiles)
List<MapLayer> get _allLayers => [...MapLayer.allLayers, _slovenianAerialLayer, ..._mbtilesLayers];
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
@@ -537,6 +545,21 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
Navigator.pop(context);
},
)),
// Slovenian WMS base layer
ListTile(
leading: _currentLayer == _slovenianAerialLayer
? const Icon(Icons.check_circle, color: Colors.green)
: const Icon(Icons.radio_button_unchecked),
title: Text(_slovenianAerialLayer.name),
subtitle: Text(_slovenianAerialLayer.attribution),
onTap: () async {
setState(() {
_currentLayer = _slovenianAerialLayer;
});
_saveSettings();
Navigator.pop(context);
},
),
// Offline MBTiles layers section
if (_mbtilesLayers.isNotEmpty) ...[
const Divider(),
@@ -576,6 +599,43 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
},
)),
],
// WMS Overlays section
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
AppLocalizations.of(context)!.wmsOverlays,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Colors.grey[600],
),
),
),
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
return Column(
children: [
CheckboxListTile(
secondary: const Icon(Icons.grid_on, color: Colors.blue),
title: Text(AppLocalizations.of(context)!.cadastralParcels),
subtitle: const Text('© GURS'),
value: mapProvider.showCadastralOverlay,
onChanged: (value) {
mapProvider.toggleCadastralOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.route, color: Colors.green),
title: Text(AppLocalizations.of(context)!.forestRoads),
subtitle: const Text('© GURS'),
value: mapProvider.showForestRoadsOverlay,
onChanged: (value) {
mapProvider.toggleForestRoadsOverlay();
},
),
],
);
},
),
],
),
),
@@ -855,6 +915,15 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
);
}
/// Format distance for display
String _formatDistance(double meters) {
if (meters < 1000) {
return '${meters.toStringAsFixed(1)} m';
} else {
return '${(meters / 1000).toStringAsFixed(2)} km';
}
}
/// Show SAR dialog with pre-populated location from map long press
void _showSarDialogWithLocation(LatLng location) {
// Create a Position object from the LatLng coordinates
@@ -1065,6 +1134,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
// Use the layer's CRS if it has one (for WMS layers), otherwise default to EPSG:3857
crs: _currentLayer.crs ?? const Epsg3857(),
// Use saved position if available, otherwise use calculated center
initialCenter: _savedMapCenter ?? center,
initialZoom: _savedMapZoom ?? _defaultZoom,
@@ -1082,11 +1153,29 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
}
},
onLongPress: (tapPosition, point) {
// Skip if in drawing mode
if (drawingProvider.isDrawing) return;
// Handle measurement mode - set measurement points
if (drawingProvider.drawingMode == DrawingMode.measure) {
if (drawingProvider.measurementPoint1 == null) {
// Set first measurement point
drawingProvider.setMeasurementPoint1(point);
} else if (drawingProvider.measurementPoint2 == null) {
// Set second measurement point
drawingProvider.setMeasurementPoint2(point);
} else {
// Clear and start new measurement
drawingProvider.clearMeasurement();
drawingProvider.setMeasurementPoint1(point);
}
// Continue to also drop SAR marker pin
}
// Drop a pin at long press location (if no pin exists)
if (_droppedPinLocation == null) {
// Skip if in other drawing modes (but not measure mode)
if (drawingProvider.isDrawing && drawingProvider.drawingMode != DrawingMode.measure) return;
// Drop a pin at long press location
// In measurement mode, this allows creating SAR markers at measurement points
// The pin moves to the latest long press location
if (_droppedPinLocation == null || drawingProvider.drawingMode == DrawingMode.measure) {
setState(() {
_droppedPinLocation = point;
});
@@ -1183,13 +1272,116 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
}),
maximumZoom: _currentLayer.maxZoom,
)
else if (!_currentLayer.isVector)
else if (_currentLayer.isWms && _currentLayer.wmsBaseUrl != null && _currentLayer.crs != null)
// WMS Base Layer (e.g., Slovenian Aerial Imagery)
flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: _currentLayer.wmsBaseUrl!,
layers: _currentLayer.wmsLayers ?? [],
styles: _currentLayer.wmsStyles ?? [],
format: _currentLayer.wmsFormat ?? 'image/jpeg',
transparent: _currentLayer.wmsTransparent ?? false,
crs: _currentLayer.crs!,
),
// Use cached tile provider for offline support
tileProvider: _tileCache.getTileProviderForWms(_currentLayer),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: _currentLayer.maxZoom,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 WMS Base Layer tile error at ${tile.coordinates}: $error');
},
)
else if (!_currentLayer.isVector && !_currentLayer.isWms)
flutter_map.TileLayer(
urlTemplate: _currentLayer.urlTemplate,
tileProvider: _tileCache.getTileProvider(_currentLayer),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: _currentLayer.maxZoom,
),
// WMS Overlays (rendered after base layer, before polylines)
// Note: These overlays only work with EPSG:3794 CRS (Slovenian coordinate system)
// Cadastral parcels overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
// Only show if enabled and map is using Slovenian CRS
if (!mapProvider.showCadastralOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geowebcache/service/wms?',
layers: const ['pregledovalnik:kn_parcele'],
styles: const ['parcele'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Cadastral Parcels',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geowebcache/service/wms?',
wmsLayers: const ['pregledovalnik:kn_parcele'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Cadastral overlay tile error at ${tile.coordinates}: $error');
if (stackTrace != null) {
debugPrint(' StackTrace: $stackTrace');
}
},
);
},
),
// Forest roads overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
// Only show if enabled and map is using Slovenian CRS
if (!mapProvider.showForestRoadsOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:gozdne_ceste'],
styles: const ['gozdne_ceste'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Forest Roads',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:gozdne_ceste'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Forest roads overlay tile error at ${tile.coordinates}: $error');
if (stackTrace != null) {
debugPrint(' StackTrace: $stackTrace');
}
},
);
},
),
// Advertisement path polylines (rendered before markers)
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
@@ -1214,6 +1406,23 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
),
// Location trail layer (rendered after paths, before drawings)
const LocationTrailLayer(),
// Measurement line layer (rendered before drawings)
if (drawingProvider.measurementPoint1 != null && drawingProvider.measurementPoint2 != null)
PolylineLayer(
polylines: [
Polyline(
points: [
drawingProvider.measurementPoint1!,
drawingProvider.measurementPoint2!,
],
color: Colors.yellow.withValues(alpha: 0.8),
strokeWidth: 3.0,
borderColor: Colors.black.withValues(alpha: 0.5),
borderStrokeWidth: 1.0,
pattern: StrokePattern.dashed(segments: [10, 5]),
),
],
),
// Drawing layer (rendered after paths, before markers)
DrawingLayer(
drawings: drawingProvider.drawings,
@@ -1257,6 +1466,104 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
position: _locationService.currentPosition,
context: context,
)!,
// Measurement point 1 marker
if (drawingProvider.measurementPoint1 != null)
Marker(
point: drawingProvider.measurementPoint1!,
width: 60,
height: 80,
rotate: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.yellow.shade700,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'Start',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 2),
Container(
decoration: BoxDecoration(
color: Colors.yellow,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(6),
child: const Icon(
Icons.location_on,
color: Colors.white,
size: 18,
),
),
],
),
),
// Measurement point 2 marker
if (drawingProvider.measurementPoint2 != null)
Marker(
point: drawingProvider.measurementPoint2!,
width: 60,
height: 80,
rotate: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.yellow.shade700,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'End',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 2),
Container(
decoration: BoxDecoration(
color: Colors.yellow,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(6),
child: const Icon(
Icons.location_on,
color: Colors.white,
size: 18,
),
),
],
),
),
// Dropped pin marker with label
if (_droppedPinLocation != null)
Marker(
@@ -1410,6 +1717,90 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
objectCount: messagesProvider.objectMarkers.length,
),
),
// Measurement distance overlay
if (drawingProvider.drawingMode == DrawingMode.measure)
Positioned(
top: 16,
left: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.yellow.shade700.withValues(alpha: 0.95),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.straighten,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!.measureDistance,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
if (drawingProvider.measuredDistance != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.distanceLabel(_formatDistance(drawingProvider.measuredDistance!)),
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
AppLocalizations.of(context)!.longPressToStartNewMeasurement,
style: const TextStyle(
color: Colors.white70,
fontSize: 10,
),
),
],
)
else if (drawingProvider.measurementPoint1 != null)
Text(
AppLocalizations.of(context)!.longPressForSecondPoint,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
)
else
Text(
AppLocalizations.of(context)!.longPressToStartMeasurement,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
],
),
),
),
// Map controls - right side (hidden in fullscreen mode)
if (!_isFullscreen)
Positioned(

View File

@@ -57,6 +57,26 @@ class TileCacheService {
);
}
/// Get tile provider for WMS layers with caching support
/// WMS layers require special handling because they use WMSTileLayerOptions
FMTCTileProvider getTileProviderForWms(MapLayer layer) {
if (!_isInitialized) {
throw StateError(
'TileCacheService not initialized. Call initialize() first.',
);
}
if (!layer.isWms) {
throw ArgumentError('Layer must be a WMS layer');
}
// Return the same cached tile provider
// The WMS URL construction is handled by flutter_map's WMSTileLayerOptions
return _store.getTileProvider(
loadingStrategy: BrowseLoadingStrategy.cacheFirst,
cachedValidDuration: const Duration(days: 30),
);
}
Future<void> downloadRegion({
required MapLayer layer,
required LatLngBounds bounds,

View File

@@ -0,0 +1,73 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:http/http.dart' as http;
/// Custom tile provider that logs WMS URLs for debugging
class DebugWmsTileProvider extends TileProvider {
final http.Client httpClient;
DebugWmsTileProvider() : httpClient = http.Client();
@override
ImageProvider getImage(TileCoordinates coordinates, TileLayer options) {
return DebugNetworkTileProvider(
coordinates: coordinates,
options: options,
httpClient: httpClient,
);
}
@override
void dispose() {
httpClient.close();
super.dispose();
}
}
class DebugNetworkTileProvider extends ImageProvider<DebugNetworkTileProvider> {
final TileCoordinates coordinates;
final TileLayer options;
final http.Client httpClient;
const DebugNetworkTileProvider({
required this.coordinates,
required this.options,
required this.httpClient,
});
@override
ImageStreamCompleter loadImage(DebugNetworkTileProvider key, ImageDecoderCallback decode) {
// Get the WMS URL from the tile layer options
final wmsOptions = options.wmsOptions;
if (wmsOptions == null) {
throw Exception('WMSTileLayerOptions is required for DebugWmsTileProvider');
}
// Build the WMS URL
final url = wmsOptions.getUrl(coordinates, 256, false);
// Log the URL for debugging
debugPrint('🌐 WMS Request URL: $url');
// Use NetworkImage to load the tile
return NetworkImage(url, headers: {'User-Agent': 'MeshCore SAR'})
.loadImage(NetworkImage(url), decode);
}
@override
Future<DebugNetworkTileProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<DebugNetworkTileProvider>(this);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DebugNetworkTileProvider &&
other.coordinates == coordinates &&
other.options == options;
}
@override
int get hashCode => Object.hash(coordinates, options);
}

View File

@@ -0,0 +1,86 @@
import 'dart:math' show Point;
import 'dart:ui' show Rect;
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;
/// EPSG:3794 - Slovenia 1996 / Slovene National Grid
/// Transverse Mercator projection for Slovenia
///
/// Official definition from https://epsg.io/3794:
/// +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=-5000000
/// +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs
///
/// This CRS is used by Slovenian government WMS services (prostor.zgs.gov.si)
/// Register and get EPSG:3794 projection
proj4.Projection getEpsg3794Projection() {
const epsg3794Def = '+proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 '
'+x_0=500000 +y_0=-5000000 +ellps=GRS80 '
'+towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs';
// Register projection if not already registered
try {
return proj4.Projection.get('EPSG:3794') ?? proj4.Projection.add('EPSG:3794', epsg3794Def);
} catch (e) {
// If already registered, get it
return proj4.Projection.get('EPSG:3794')!;
}
}
/// Create Proj4Crs for EPSG:3794
///
/// Configuration matches the GeoWebCache tile grid used by prostor.zgs.gov.si
///
/// Official tile grid from WMTS GetCapabilities:
/// - TopLeftCorner: 373217.6542445397, 246158.298050262
/// - Bounds: X: 373217.65 to 695777.65, Y: 31118.30 to 246158.30
/// - Tile size: 256x256 pixels
/// - Scale denominators converted to resolutions using: resolution = scaleDenom * 0.00028
Crs getSlovenianCrs() {
final projection = getEpsg3794Projection();
// Resolutions calculated from GeoWebCache scale denominators
// Formula: resolution (m/px) = scaleDenominator * 0.00028 (OGC standard)
final resolutions = <double>[
420.0, // Zoom 0 - ScaleDenom: 1500000
280.0, // Zoom 1 - ScaleDenom: 1000000
210.0, // Zoom 2 - ScaleDenom: 750000
140.0, // Zoom 3 - ScaleDenom: 500000
70.0, // Zoom 4 - ScaleDenom: 250000
28.0, // Zoom 5 - ScaleDenom: 100000
14.0, // Zoom 6 - ScaleDenom: 50000
7.0, // Zoom 7 - ScaleDenom: 25000
4.2, // Zoom 8 - ScaleDenom: 15000
2.8, // Zoom 9 - ScaleDenom: 10000
1.4, // Zoom 10 - ScaleDenom: 5000
0.56, // Zoom 11 - ScaleDenom: 2000
0.28, // Zoom 12 - ScaleDenom: 1000
0.14, // Zoom 13 - ScaleDenom: 500
0.07, // Zoom 14 - ScaleDenom: 250
0.028, // Zoom 15 - ScaleDenom: 100
];
// Bounds from WMS capabilities (actual data extent in Slovenia)
final bounds = Rect.fromLTRB(
373217.65, // min X (west)
31118.30, // min Y (south) - top in Rect coordinates
695777.65, // max X (east)
246158.30, // max Y (north) - bottom in Rect coordinates
);
// Origin from WMTS TileMatrixSet TopLeftCorner
// This is the top-left corner of the tile pyramid (min X, max Y)
final origin = Point<double>(373217.6542445397, 246158.298050262);
return Proj4Crs.fromFactory(
code: 'EPSG:3794',
proj4Projection: projection,
resolutions: resolutions,
bounds: bounds,
origins: [origin],
);
}
/// Singleton instance of Slovenian CRS for reuse
final Crs slovenianCrs = getSlovenianCrs();

View File

@@ -127,6 +127,18 @@ class DrawingToolbar extends StatelessWidget {
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
// Clear measurement
if (drawingProvider.drawingMode == DrawingMode.measure &&
drawingProvider.measurementPoint1 != null)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => drawingProvider.clearMeasurement(),
tooltip: AppLocalizations.of(context)!.clearMeasurement,
color: Colors.orange,
iconSize: 20,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
// Complete line drawing
if (drawingProvider.drawingMode == DrawingMode.line &&
drawingProvider.currentLinePoints.length >= 2)
@@ -207,6 +219,15 @@ class DrawingToolbar extends StatelessWidget {
drawingProvider.setDrawingMode(DrawingMode.rectangle);
},
),
ListTile(
leading: const Icon(Icons.straighten),
title: Text(AppLocalizations.of(sheetContext)!.measureDistance),
subtitle: Text(AppLocalizations.of(sheetContext)!.measureDistanceDesc),
onTap: () {
Navigator.pop(sheetContext);
drawingProvider.setDrawingMode(DrawingMode.measure);
},
),
const Divider(),
// Toggle received drawings visibility
SwitchListTile(
@@ -465,6 +486,8 @@ class DrawingToolbar extends StatelessWidget {
return Icons.show_chart;
case DrawingMode.rectangle:
return Icons.crop_square;
case DrawingMode.measure:
return Icons.straighten;
case DrawingMode.none:
return Icons.edit;
}
@@ -477,6 +500,8 @@ class DrawingToolbar extends StatelessWidget {
return AppLocalizations.of(context)!.drawLine;
case DrawingMode.rectangle:
return AppLocalizations.of(context)!.drawRectangle;
case DrawingMode.measure:
return AppLocalizations.of(context)!.measureDistance;
case DrawingMode.none:
return AppLocalizations.of(context)!.drawing;
}
@@ -489,6 +514,8 @@ class DrawingToolbar extends StatelessWidget {
return 'Tap map to add points\nTap ✓ to finish';
case DrawingMode.rectangle:
return 'Tap start point, then end point';
case DrawingMode.measure:
return 'Long press two points to measure';
case DrawingMode.none:
return '';
}