Add WMS overlays for hiking trails, main roads, house numbers, fire hazard zones, historical fires, firebreaks, Kras fire zones, place names, and municipality borders

- Updated localization files for German, English, Spanish, French, Croatian, Italian, and Slovenian to include new overlay labels.
- Implemented new WMS layers in the map provider and map tab for displaying hiking trails, main roads, house numbers, fire hazard zones, historical fires, firebreaks, Kras fire zones, place names, and municipality borders.
- Added toggle functionality for each new overlay in the map provider.
- Enhanced the map tab UI to include checkboxes for the new overlays, allowing users to enable or disable them.
- Integrated the new WMS layers into the map rendering logic.
This commit is contained in:
Janez T
2025-10-28 22:26:35 +01:00
parent 0371c3cd04
commit 30bd3315aa
26 changed files with 1799 additions and 138 deletions

View File

@@ -2712,6 +2712,17 @@
"showForestRoads": "Waldwege anzeigen",
"wmsOverlays": "WMS Überlagerungen",
"hikingTrails": "Wanderwege",
"mainRoads": "Hauptstraßen",
"houseNumbers": "Hausnummern",
"fireHazardZones": "Brandgefährdungszonen",
"historicalFires": "Historische Brände",
"firebreaks": "Brandschneisen",
"krasFireZones": "Kras-Brandzonen",
"placeNames": "Ortsnamen",
"municipalityBorders": "Gemeindegrenzen",
"topographicMap": "Topographische Karte 1:25000",
"recentMessages": "Aktuelle Nachrichten",
"addChannel": "Kanal hinzufügen",

View File

@@ -3343,6 +3343,56 @@
"description": "Section header for WMS overlay layers in layer selector"
},
"hikingTrails": "Hiking Trails",
"@hikingTrails": {
"description": "Label for hiking/mountain trails WMS overlay layer"
},
"mainRoads": "Main Roads",
"@mainRoads": {
"description": "Label for main roads WMS overlay layer"
},
"houseNumbers": "House Numbers",
"@houseNumbers": {
"description": "Label for house numbers WMS overlay layer"
},
"fireHazardZones": "Fire Hazard Zones",
"@fireHazardZones": {
"description": "Label for fire hazard risk zones WMS overlay layer"
},
"historicalFires": "Historical Fires",
"@historicalFires": {
"description": "Label for historical forest fires WMS overlay layer"
},
"firebreaks": "Firebreaks",
"@firebreaks": {
"description": "Label for firebreaks WMS overlay layer"
},
"krasFireZones": "Kras Fire Zones",
"@krasFireZones": {
"description": "Label for Kras fire zones WMS overlay layer"
},
"placeNames": "Place Names",
"@placeNames": {
"description": "Label for geographic place names WMS overlay layer"
},
"municipalityBorders": "Municipality Borders",
"@municipalityBorders": {
"description": "Label for municipality borders WMS overlay layer"
},
"topographicMap": "Topographic Map 1:25000",
"@topographicMap": {
"description": "Label for DTK25 topographic base map layer"
},
"recentMessages": "Recent Messages",
"@recentMessages": {
"description": "Header for recent messages overlay on map in fullscreen mode"

View File

@@ -2707,6 +2707,17 @@
"showForestRoads": "Mostrar caminos forestales",
"wmsOverlays": "Superposiciones WMS",
"hikingTrails": "Senderos de Montaña",
"mainRoads": "Carreteras Principales",
"houseNumbers": "Números de Casa",
"fireHazardZones": "Zonas de Riesgo de Incendio",
"historicalFires": "Incendios Históricos",
"firebreaks": "Cortafuegos",
"krasFireZones": "Zonas de Incendio Kras",
"placeNames": "Nombres de Lugares",
"municipalityBorders": "Límites Municipales",
"topographicMap": "Mapa Topográfico 1:25000",
"recentMessages": "Mensajes Recientes",
"addChannel": "Agregar Canal",

View File

@@ -2712,6 +2712,17 @@
"showForestRoads": "Afficher les chemins forestiers",
"wmsOverlays": "Superpositions WMS",
"hikingTrails": "Sentiers de Randonnée",
"mainRoads": "Routes Principales",
"houseNumbers": "Numéros de Maison",
"fireHazardZones": "Zones à Risque d'Incendie",
"historicalFires": "Incendies Historiques",
"firebreaks": "Coupe-feu",
"krasFireZones": "Zones d'Incendie Kras",
"placeNames": "Noms de Lieux",
"municipalityBorders": "Limites Municipales",
"topographicMap": "Carte Topographique 1:25000",
"recentMessages": "Messages Récents",
"@recentMessages": {
"description": "Header for recent messages overlay on map in fullscreen mode"

View File

@@ -1010,6 +1010,17 @@
"showForestRoads": "Prikaži šumske ceste",
"wmsOverlays": "WMS prekrivanja",
"hikingTrails": "Planinske staze",
"mainRoads": "Glavne ceste",
"houseNumbers": "Kućni brojevi",
"fireHazardZones": "Požarna ugroženost",
"historicalFires": "Povijesni požari",
"firebreaks": "Protupožarni pojasi",
"krasFireZones": "Kraška požarišta",
"placeNames": "Zemljopisna imena",
"municipalityBorders": "Općinske granice",
"topographicMap": "Topografska karta 1:25000",
"recentMessages": "Nedavne poruke",
"addChannel": "Dodaj kanal",

View File

@@ -2712,6 +2712,17 @@
"showForestRoads": "Mostra strade forestali",
"wmsOverlays": "Sovrapposizioni WMS",
"hikingTrails": "Sentieri Escursionistici",
"mainRoads": "Strade Principali",
"houseNumbers": "Numeri Civici",
"fireHazardZones": "Zone a Rischio Incendio",
"historicalFires": "Incendi Storici",
"firebreaks": "Fasce Tagliafuoco",
"krasFireZones": "Zone di Incendio Kras",
"placeNames": "Nomi di Luoghi",
"municipalityBorders": "Confini Comunali",
"topographicMap": "Carta Topografica 1:25000",
"recentMessages": "Messaggi Recenti",
"@recentMessages": {
"description": "Header for recent messages overlay on map in fullscreen mode"

View File

@@ -3555,6 +3555,66 @@ abstract class AppLocalizations {
/// **'WMS Overlays'**
String get wmsOverlays;
/// Label for hiking/mountain trails WMS overlay layer
///
/// In en, this message translates to:
/// **'Hiking Trails'**
String get hikingTrails;
/// Label for main roads WMS overlay layer
///
/// In en, this message translates to:
/// **'Main Roads'**
String get mainRoads;
/// Label for house numbers WMS overlay layer
///
/// In en, this message translates to:
/// **'House Numbers'**
String get houseNumbers;
/// Label for fire hazard risk zones WMS overlay layer
///
/// In en, this message translates to:
/// **'Fire Hazard Zones'**
String get fireHazardZones;
/// Label for historical forest fires WMS overlay layer
///
/// In en, this message translates to:
/// **'Historical Fires'**
String get historicalFires;
/// Label for firebreaks WMS overlay layer
///
/// In en, this message translates to:
/// **'Firebreaks'**
String get firebreaks;
/// Label for Kras fire zones WMS overlay layer
///
/// In en, this message translates to:
/// **'Kras Fire Zones'**
String get krasFireZones;
/// Label for geographic place names WMS overlay layer
///
/// In en, this message translates to:
/// **'Place Names'**
String get placeNames;
/// Label for municipality borders WMS overlay layer
///
/// In en, this message translates to:
/// **'Municipality Borders'**
String get municipalityBorders;
/// Label for DTK25 topographic base map layer
///
/// In en, this message translates to:
/// **'Topographic Map 1:25000'**
String get topographicMap;
/// Header for recent messages overlay on map in fullscreen mode
///
/// In en, this message translates to:

View File

@@ -1989,6 +1989,36 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get wmsOverlays => 'WMS Überlagerungen';
@override
String get hikingTrails => 'Wanderwege';
@override
String get mainRoads => 'Hauptstraßen';
@override
String get houseNumbers => 'Hausnummern';
@override
String get fireHazardZones => 'Brandgefährdungszonen';
@override
String get historicalFires => 'Historische Brände';
@override
String get firebreaks => 'Brandschneisen';
@override
String get krasFireZones => 'Kras-Brandzonen';
@override
String get placeNames => 'Ortsnamen';
@override
String get municipalityBorders => 'Gemeindegrenzen';
@override
String get topographicMap => 'Topographische Karte 1:25000';
@override
String get recentMessages => 'Aktuelle Nachrichten';

View File

@@ -1969,6 +1969,36 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get wmsOverlays => 'WMS Overlays';
@override
String get hikingTrails => 'Hiking Trails';
@override
String get mainRoads => 'Main Roads';
@override
String get houseNumbers => 'House Numbers';
@override
String get fireHazardZones => 'Fire Hazard Zones';
@override
String get historicalFires => 'Historical Fires';
@override
String get firebreaks => 'Firebreaks';
@override
String get krasFireZones => 'Kras Fire Zones';
@override
String get placeNames => 'Place Names';
@override
String get municipalityBorders => 'Municipality Borders';
@override
String get topographicMap => 'Topographic Map 1:25000';
@override
String get recentMessages => 'Recent Messages';

View File

@@ -1994,6 +1994,36 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get wmsOverlays => 'Superposiciones WMS';
@override
String get hikingTrails => 'Senderos de Montaña';
@override
String get mainRoads => 'Carreteras Principales';
@override
String get houseNumbers => 'Números de Casa';
@override
String get fireHazardZones => 'Zonas de Riesgo de Incendio';
@override
String get historicalFires => 'Incendios Históricos';
@override
String get firebreaks => 'Cortafuegos';
@override
String get krasFireZones => 'Zonas de Incendio Kras';
@override
String get placeNames => 'Nombres de Lugares';
@override
String get municipalityBorders => 'Límites Municipales';
@override
String get topographicMap => 'Mapa Topográfico 1:25000';
@override
String get recentMessages => 'Mensajes Recientes';

View File

@@ -1998,6 +1998,36 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get wmsOverlays => 'Superpositions WMS';
@override
String get hikingTrails => 'Sentiers de Randonnée';
@override
String get mainRoads => 'Routes Principales';
@override
String get houseNumbers => 'Numéros de Maison';
@override
String get fireHazardZones => 'Zones à Risque d\'Incendie';
@override
String get historicalFires => 'Incendies Historiques';
@override
String get firebreaks => 'Coupe-feu';
@override
String get krasFireZones => 'Zones d\'Incendie Kras';
@override
String get placeNames => 'Noms de Lieux';
@override
String get municipalityBorders => 'Limites Municipales';
@override
String get topographicMap => 'Carte Topographique 1:25000';
@override
String get recentMessages => 'Messages Récents';

View File

@@ -1979,6 +1979,36 @@ class AppLocalizationsHr extends AppLocalizations {
@override
String get wmsOverlays => 'WMS prekrivanja';
@override
String get hikingTrails => 'Planinske staze';
@override
String get mainRoads => 'Glavne ceste';
@override
String get houseNumbers => 'Kućni brojevi';
@override
String get fireHazardZones => 'Požarna ugroženost';
@override
String get historicalFires => 'Povijesni požari';
@override
String get firebreaks => 'Protupožarni pojasi';
@override
String get krasFireZones => 'Kraška požarišta';
@override
String get placeNames => 'Zemljopisna imena';
@override
String get municipalityBorders => 'Općinske granice';
@override
String get topographicMap => 'Topografska karta 1:25000';
@override
String get recentMessages => 'Nedavne poruke';

View File

@@ -1989,6 +1989,36 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get wmsOverlays => 'Sovrapposizioni WMS';
@override
String get hikingTrails => 'Sentieri Escursionistici';
@override
String get mainRoads => 'Strade Principali';
@override
String get houseNumbers => 'Numeri Civici';
@override
String get fireHazardZones => 'Zone a Rischio Incendio';
@override
String get historicalFires => 'Incendi Storici';
@override
String get firebreaks => 'Fasce Tagliafuoco';
@override
String get krasFireZones => 'Zone di Incendio Kras';
@override
String get placeNames => 'Nomi di Luoghi';
@override
String get municipalityBorders => 'Confini Comunali';
@override
String get topographicMap => 'Carta Topografica 1:25000';
@override
String get recentMessages => 'Messaggi Recenti';

View File

@@ -1979,6 +1979,36 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get wmsOverlays => 'WMS prekrivanja';
@override
String get hikingTrails => 'Planinske poti';
@override
String get mainRoads => 'Glavne ceste';
@override
String get houseNumbers => 'Hišne številke';
@override
String get fireHazardZones => 'Požarna ogroženost';
@override
String get historicalFires => 'Zgodovinski požari';
@override
String get firebreaks => 'Protipožarne preseke';
@override
String get krasFireZones => 'Kraška požarišča';
@override
String get placeNames => 'Zemljepisna imena';
@override
String get municipalityBorders => 'Občinske meje';
@override
String get topographicMap => 'Topografska karta 1:25000';
@override
String get recentMessages => 'Nedavna sporočila';

View File

@@ -1014,6 +1014,17 @@
"showForestRoads": "Prikaži gozdne ceste",
"wmsOverlays": "WMS prekrivanja",
"hikingTrails": "Planinske poti",
"mainRoads": "Glavne ceste",
"houseNumbers": "Hišne številke",
"fireHazardZones": "Požarna ogroženost",
"historicalFires": "Zgodovinski požari",
"firebreaks": "Protipožarne preseke",
"krasFireZones": "Kraška požarišča",
"placeNames": "Zemljepisna imena",
"municipalityBorders": "Občinske meje",
"topographicMap": "Topografska karta 1:25000",
"recentMessages": "Nedavna sporočila",
"addChannel": "Dodaj kanal",

View File

@@ -150,6 +150,25 @@ class MapLayer {
);
}
/// Slovenian Topographic Map 1:25000 (DTK25) - WMS Base Layer
/// Uses EPSG:3794 coordinate system (GeoWebCache tile matrix zoom 0-15)
/// Note: CRS is initialized at runtime in getDTK25()
static MapLayer getDTK25(Crs slovenianCrs) {
return MapLayer(
type: MapLayerType.wmsBase,
name: 'DTK25 (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:DTK25'],
wmsFormat: 'image/jpeg',
wmsTransparent: false,
crs: slovenianCrs,
);
}
static const List<MapLayer> allLayers = [
openStreetMap,
openTopoMap,

View File

@@ -21,6 +21,15 @@ class MapProvider with ChangeNotifier {
// WMS overlay toggles
bool _showCadastralOverlay = false;
bool _showForestRoadsOverlay = false;
bool _showHikingTrailsOverlay = false;
bool _showMainRoadsOverlay = false;
bool _showHouseNumbersOverlay = false;
bool _showFireHazardZonesOverlay = false;
bool _showHistoricalFiresOverlay = false;
bool _showFirebreaksOverlay = false;
bool _showKrasFireZonesOverlay = false;
bool _showPlaceNamesOverlay = false;
bool _showMunicipalityBordersOverlay = false;
// Contact trail toggles
bool _showAllContactTrails = false;
@@ -46,6 +55,15 @@ class MapProvider with ChangeNotifier {
// WMS overlay getters
bool get showCadastralOverlay => _showCadastralOverlay;
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
bool get showHikingTrailsOverlay => _showHikingTrailsOverlay;
bool get showMainRoadsOverlay => _showMainRoadsOverlay;
bool get showHouseNumbersOverlay => _showHouseNumbersOverlay;
bool get showFireHazardZonesOverlay => _showFireHazardZonesOverlay;
bool get showHistoricalFiresOverlay => _showHistoricalFiresOverlay;
bool get showFirebreaksOverlay => _showFirebreaksOverlay;
bool get showKrasFireZonesOverlay => _showKrasFireZonesOverlay;
bool get showPlaceNamesOverlay => _showPlaceNamesOverlay;
bool get showMunicipalityBordersOverlay => _showMunicipalityBordersOverlay;
// Contact trail getters
bool get showAllContactTrails => _showAllContactTrails;
@@ -252,11 +270,83 @@ class MapProvider with ChangeNotifier {
await _saveOverlayState();
}
/// Toggle hiking trails overlay
Future<void> toggleHikingTrailsOverlay() async {
_showHikingTrailsOverlay = !_showHikingTrailsOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Toggle main roads overlay
Future<void> toggleMainRoadsOverlay() async {
_showMainRoadsOverlay = !_showMainRoadsOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Toggle house numbers overlay
Future<void> toggleHouseNumbersOverlay() async {
_showHouseNumbersOverlay = !_showHouseNumbersOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Toggle fire hazard zones overlay
Future<void> toggleFireHazardZonesOverlay() async {
_showFireHazardZonesOverlay = !_showFireHazardZonesOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Toggle historical fires overlay
Future<void> toggleHistoricalFiresOverlay() async {
_showHistoricalFiresOverlay = !_showHistoricalFiresOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Toggle firebreaks overlay
Future<void> toggleFirebreaksOverlay() async {
_showFirebreaksOverlay = !_showFirebreaksOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Toggle Kras fire zones overlay
Future<void> toggleKrasFireZonesOverlay() async {
_showKrasFireZonesOverlay = !_showKrasFireZonesOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Toggle place names overlay
Future<void> togglePlaceNamesOverlay() async {
_showPlaceNamesOverlay = !_showPlaceNamesOverlay;
notifyListeners();
await _saveOverlayState();
}
/// Toggle municipality borders overlay
Future<void> toggleMunicipalityBordersOverlay() async {
_showMunicipalityBordersOverlay = !_showMunicipalityBordersOverlay;
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;
_showHikingTrailsOverlay = prefs.getBool('map_show_hiking_trails_overlay') ?? false;
_showMainRoadsOverlay = prefs.getBool('map_show_main_roads_overlay') ?? false;
_showHouseNumbersOverlay = prefs.getBool('map_show_house_numbers_overlay') ?? false;
_showFireHazardZonesOverlay = prefs.getBool('map_show_fire_hazard_zones_overlay') ?? false;
_showHistoricalFiresOverlay = prefs.getBool('map_show_historical_fires_overlay') ?? false;
_showFirebreaksOverlay = prefs.getBool('map_show_firebreaks_overlay') ?? false;
_showKrasFireZonesOverlay = prefs.getBool('map_show_kras_fire_zones_overlay') ?? false;
_showPlaceNamesOverlay = prefs.getBool('map_show_place_names_overlay') ?? false;
_showMunicipalityBordersOverlay = prefs.getBool('map_show_municipality_borders_overlay') ?? false;
notifyListeners();
}
@@ -265,6 +355,15 @@ class MapProvider with ChangeNotifier {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('map_show_cadastral_overlay', _showCadastralOverlay);
await prefs.setBool('map_show_forest_roads_overlay', _showForestRoadsOverlay);
await prefs.setBool('map_show_hiking_trails_overlay', _showHikingTrailsOverlay);
await prefs.setBool('map_show_main_roads_overlay', _showMainRoadsOverlay);
await prefs.setBool('map_show_house_numbers_overlay', _showHouseNumbersOverlay);
await prefs.setBool('map_show_fire_hazard_zones_overlay', _showFireHazardZonesOverlay);
await prefs.setBool('map_show_historical_fires_overlay', _showHistoricalFiresOverlay);
await prefs.setBool('map_show_firebreaks_overlay', _showFirebreaksOverlay);
await prefs.setBool('map_show_kras_fire_zones_overlay', _showKrasFireZonesOverlay);
await prefs.setBool('map_show_place_names_overlay', _showPlaceNamesOverlay);
await prefs.setBool('map_show_municipality_borders_overlay', _showMunicipalityBordersOverlay);
}
/// Toggle all contact trails on/off

View File

@@ -30,7 +30,6 @@ import '../services/map_marker_service.dart';
import '../services/mbtiles_service.dart';
import '../services/trail_color_service.dart';
import '../widgets/map_debug_info.dart';
import '../widgets/map/map_legend.dart';
import '../widgets/map/compass_widget.dart';
import '../widgets/map/detailed_compass_dialog.dart';
import '../widgets/map/drawing_layer.dart';
@@ -68,7 +67,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
MapLayer _currentLayer = MapLayer.openStreetMap;
double? _compassHeading; // Compass sensor heading
bool _rotateMarkerWithHeading = false; // Toggle for rotation
bool _showLegend = false;
bool _showMapDebugInfo = false; // Toggle for debug info
bool _isFullscreen = false; // Toggle for fullscreen mode
double _gpsUpdateDistance = 3.0; // meters
@@ -82,6 +80,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
// WMS layers (Slovenian)
late final MapLayer _slovenianAerialLayer;
late final MapLayer _dtk25Layer;
// Vector tile theme
vtr.Theme? _vectorTheme;
@@ -118,8 +117,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
@override
void initState() {
super.initState();
// Initialize Slovenian aerial layer with CRS
// Initialize Slovenian WMS layers with CRS
_slovenianAerialLayer = MapLayer.getSlovenianAerial2024(slovenianCrs);
_dtk25Layer = MapLayer.getDTK25(slovenianCrs);
_loadSettings();
_loadMbtilesLayers();
_initializeTileCache();
@@ -260,7 +260,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
}
/// Get all available layers (default + WMS + MBTiles)
List<MapLayer> get _allLayers => [...MapLayer.allLayers, _slovenianAerialLayer, ..._mbtilesLayers];
List<MapLayer> get _allLayers => [...MapLayer.allLayers, _slovenianAerialLayer, _dtk25Layer, ..._mbtilesLayers];
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
@@ -275,7 +275,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
final lastLayerName = prefs.getString('map_last_layer_name');
setState(() {
_showLegend = prefs.getBool('map_show_legend') ?? false;
_rotateMarkerWithHeading = prefs.getBool('map_rotate_with_heading') ?? false;
_showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? false;
_isFullscreen = prefs.getBool('map_fullscreen') ?? false;
@@ -326,7 +325,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
Future<void> _saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('map_show_legend', _showLegend);
await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading);
await prefs.setBool('map_show_debug_info', _showMapDebugInfo);
await prefs.setBool('map_fullscreen', _isFullscreen);
@@ -464,18 +462,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
}
}
// Reset map rotation to north (0 degrees)
void _resetMapRotation() {
if (!_isMapReady) return;
try {
final camera = _mapController.camera;
_mapController.moveAndRotate(camera.center, camera.zoom, 0);
} catch (e) {
// Silently fail if map controller not ready
debugPrint('Failed to reset map rotation: $e');
}
}
LatLng _calculateCenter(List<Contact> contacts, List<SarMarker> sarMarkers) {
return _markerService.calculateCenter(
contacts: contacts,
@@ -592,9 +578,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
Navigator.pop(context);
},
)),
// Slovenian WMS base layer (only for Slovenian/Croatian regions)
// Slovenian WMS base layers (only for Slovenian/Croatian regions)
if (AppLocalizations.of(context)!.localeName == 'sl' ||
AppLocalizations.of(context)!.localeName == 'hr')
AppLocalizations.of(context)!.localeName == 'hr') ...[
ListTile(
leading: _currentLayer == _slovenianAerialLayer
? const Icon(Icons.check_circle, color: Colors.green)
@@ -617,6 +603,29 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
Navigator.pop(context);
},
),
ListTile(
leading: _currentLayer == _dtk25Layer
? const Icon(Icons.check_circle, color: Colors.green)
: const Icon(Icons.radio_button_unchecked),
title: Text(AppLocalizations.of(context)!.topographicMap),
subtitle: Text(_dtk25Layer.attribution),
onTap: () async {
setState(() {
_currentLayer = _dtk25Layer;
// Clamp zoom level if current zoom exceeds new layer's max
// For WMS layers, use a middle zoom (11) instead of max zoom to avoid extreme close-up
if (_isMapReady && _mapController.camera.zoom > _dtk25Layer.maxZoom) {
_mapController.move(
_mapController.camera.center,
11.0, // Middle zoom for WMS
);
}
});
_saveSettings();
Navigator.pop(context);
},
),
],
// Offline MBTiles layers section
if (_mbtilesLayers.isNotEmpty) ...[
const Divider(),
@@ -699,6 +708,87 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
mapProvider.toggleForestRoadsOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.hiking, color: Colors.brown),
title: Text(AppLocalizations.of(context)!.hikingTrails),
subtitle: const Text('© GURS'),
value: mapProvider.showHikingTrailsOverlay,
onChanged: (value) {
mapProvider.toggleHikingTrailsOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.alt_route, color: Colors.grey),
title: Text(AppLocalizations.of(context)!.mainRoads),
subtitle: const Text('© GURS'),
value: mapProvider.showMainRoadsOverlay,
onChanged: (value) {
mapProvider.toggleMainRoadsOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.numbers, color: Colors.purple),
title: Text(AppLocalizations.of(context)!.houseNumbers),
subtitle: const Text('© GURS'),
value: mapProvider.showHouseNumbersOverlay,
onChanged: (value) {
mapProvider.toggleHouseNumbersOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.warning_amber, color: Colors.orange),
title: Text(AppLocalizations.of(context)!.fireHazardZones),
subtitle: const Text('© GURS'),
value: mapProvider.showFireHazardZonesOverlay,
onChanged: (value) {
mapProvider.toggleFireHazardZonesOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.local_fire_department, color: Colors.red),
title: Text(AppLocalizations.of(context)!.historicalFires),
subtitle: const Text('© GURS'),
value: mapProvider.showHistoricalFiresOverlay,
onChanged: (value) {
mapProvider.toggleHistoricalFiresOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.forest, color: Colors.teal),
title: Text(AppLocalizations.of(context)!.firebreaks),
subtitle: const Text('© GURS'),
value: mapProvider.showFirebreaksOverlay,
onChanged: (value) {
mapProvider.toggleFirebreaksOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.warning, color: Colors.deepOrange),
title: Text(AppLocalizations.of(context)!.krasFireZones),
subtitle: const Text('© GURS'),
value: mapProvider.showKrasFireZonesOverlay,
onChanged: (value) {
mapProvider.toggleKrasFireZonesOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.place, color: Colors.indigo),
title: Text(AppLocalizations.of(context)!.placeNames),
subtitle: const Text('© GURS'),
value: mapProvider.showPlaceNamesOverlay,
onChanged: (value) {
mapProvider.togglePlaceNamesOverlay();
},
),
CheckboxListTile(
secondary: const Icon(Icons.border_outer, color: Colors.cyan),
title: Text(AppLocalizations.of(context)!.municipalityBorders),
subtitle: const Text('© GURS'),
value: mapProvider.showMunicipalityBordersOverlay,
onChanged: (value) {
mapProvider.toggleMunicipalityBordersOverlay();
},
),
],
);
},
@@ -755,54 +845,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
),
),
const Divider(),
// Legend toggle
SwitchListTile(
secondary: const Icon(Icons.info_outline),
title: Text(AppLocalizations.of(context)!.showLegend),
subtitle: Text(AppLocalizations.of(context)!.displayMarkerTypeCounts),
value: _showLegend,
onChanged: (value) {
setState(() {
_showLegend = value;
});
setModalState(() {});
_saveSettings();
},
),
const Divider(),
// Compass rotation toggle
SwitchListTile(
secondary: const Icon(Icons.explore),
title: Text(AppLocalizations.of(context)!.rotateMapWithHeading),
subtitle: Text(AppLocalizations.of(context)!.mapFollowsDirection),
value: _rotateMarkerWithHeading,
onChanged: (value) {
setState(() {
_rotateMarkerWithHeading = value;
// Reset map rotation when disabling (only if map is ready)
if (_isMapReady) {
try {
final camera = _mapController.camera;
if (!_rotateMarkerWithHeading) {
_mapController.moveAndRotate(camera.center, camera.zoom, 0);
} else if (_currentHeading != null) {
// Apply current heading rotation when enabling (if heading is valid)
_mapController.moveAndRotate(
camera.center,
camera.zoom,
-_currentHeading!,
);
}
} catch (e) {
// Map not ready yet, ignore
}
}
});
setModalState(() {});
_saveSettings();
},
),
const Divider(),
// Map Debug Info toggle
SwitchListTile(
secondary: const Icon(Icons.developer_mode),
@@ -1442,6 +1484,331 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
);
},
),
// Hiking trails overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
if (!mapProvider.showHikingTrailsOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:KGI_LINIJE_PLANINSKE_POTI_G'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Hiking Trails',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:KGI_LINIJE_PLANINSKE_POTI_G'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Hiking trails overlay tile error at ${tile.coordinates}: $error');
},
);
},
),
// Main roads overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
if (!mapProvider.showMainRoadsOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:KGI_LINIJE_CESTE_G'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Main Roads',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:KGI_LINIJE_CESTE_G'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Main roads overlay tile error at ${tile.coordinates}: $error');
},
);
},
),
// House numbers overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
if (!mapProvider.showHouseNumbersOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:NEP_HISNE_STEVILKE'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'House Numbers',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:NEP_HISNE_STEVILKE'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 House numbers overlay tile error at ${tile.coordinates}: $error');
},
);
},
),
// Fire hazard zones overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
if (!mapProvider.showFireHazardZonesOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:pozarna_ogrozenost'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Fire Hazard Zones',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:pozarna_ogrozenost'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Fire hazard zones overlay tile error at ${tile.coordinates}: $error');
},
);
},
),
// Historical fires overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
if (!mapProvider.showHistoricalFiresOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:gozdni_pozari'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Historical Fires',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:gozdni_pozari'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Historical fires overlay tile error at ${tile.coordinates}: $error');
},
);
},
),
// Firebreaks overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
if (!mapProvider.showFirebreaksOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:protipozarne_preseke'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Firebreaks',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:protipozarne_preseke'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Firebreaks overlay tile error at ${tile.coordinates}: $error');
},
);
},
),
// Kras fire zones overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
if (!mapProvider.showKrasFireZonesOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:pozarisce_kras'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Kras Fire Zones',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:pozarisce_kras'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Kras fire zones overlay tile error at ${tile.coordinates}: $error');
},
);
},
),
// Place names overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
if (!mapProvider.showPlaceNamesOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:zemljepisna_imena'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Place Names',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:zemljepisna_imena'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Place names overlay tile error at ${tile.coordinates}: $error');
},
);
},
),
// Municipality borders overlay
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
if (!mapProvider.showMunicipalityBordersOverlay || _currentLayer.crs == null) {
return const SizedBox.shrink();
}
return flutter_map.TileLayer(
wmsOptions: WMSTileLayerOptions(
baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
layers: const ['pregledovalnik:NEP_RPE_OBCINE'],
styles: const ['obcine'],
format: 'image/png',
transparent: true,
crs: slovenianCrs,
),
tileProvider: _tileCache.getTileProviderForWms(
MapLayer(
type: MapLayerType.wmsBase,
name: 'Municipality Borders',
urlTemplate: '',
attribution: '© GURS',
maxZoom: 19,
isWms: true,
wmsBaseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?',
wmsLayers: const ['pregledovalnik:NEP_RPE_OBCINE'],
wmsFormat: 'image/png',
crs: slovenianCrs,
),
),
userAgentPackageName: 'com.meshcore.sar',
maxZoom: 19,
errorTileCallback: (tile, error, stackTrace) {
debugPrint('🔴 Municipality borders overlay tile error at ${tile.coordinates}: $error');
},
);
},
),
// Imported trail layer (rendered at bottom for reference)
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
@@ -1904,19 +2271,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
),
),
),
// Map legend overlay (hidden in fullscreen mode)
if (_showLegend && !_isFullscreen)
Positioned(
top: 80, // Position below compass
left: 16,
child: MapLegend(
teamMemberCount: contactsWithLocation.length,
foundPersonCount: messagesProvider.foundPersonMarkers.length,
fireCount: messagesProvider.fireMarkers.length,
stagingAreaCount: messagesProvider.stagingAreaMarkers.length,
objectCount: messagesProvider.objectMarkers.length,
),
),
// Measurement distance overlay
if (drawingProvider.drawingMode == DrawingMode.measure)
Positioned(
@@ -2017,6 +2371,82 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
return const DrawingToolbar();
},
),
// Hide other buttons when in drawing mode
if (!drawingProvider.isDrawing) ...[
// Current Location - always center to GPS
FloatingActionButton.small(
heroTag: 'center_map',
onPressed: !_isMapReady ? null : () async {
// Get current zoom to retain it
final currentZoom = _mapController.camera.zoom;
// Force update GPS location and jump to it
final position = await _locationService.getCurrentPosition();
if (position != null && mounted) {
setState(() {
// Position updated in service
});
_mapController.move(
LatLng(position.latitude, position.longitude),
currentZoom,
);
} else {
// Fallback to cached position or default center
final currentPosition = _locationService.currentPosition;
if (currentPosition != null) {
_mapController.move(
LatLng(
currentPosition.latitude,
currentPosition.longitude,
),
currentZoom,
);
} else {
_mapController.move(center, currentZoom);
}
}
},
child: const Icon(Icons.my_location),
),
const SizedBox(height: 8),
// Map Rotation Lock - toggle rotate with heading
FloatingActionButton.small(
heroTag: 'rotation_lock',
backgroundColor: _rotateMarkerWithHeading
? Theme.of(context).colorScheme.primary
: null,
onPressed: !_isMapReady ? null : () {
setState(() {
_rotateMarkerWithHeading = !_rotateMarkerWithHeading;
// Reset map rotation when disabling
if (_isMapReady) {
try {
final camera = _mapController.camera;
if (!_rotateMarkerWithHeading) {
// Disable: reset to north
_mapController.moveAndRotate(camera.center, camera.zoom, 0);
} else if (_currentHeading != null) {
// Enable: apply current heading rotation
_mapController.moveAndRotate(
camera.center,
camera.zoom,
-_currentHeading!,
);
}
} catch (e) {
debugPrint('Failed to toggle rotation lock: $e');
}
}
});
_saveSettings();
},
child: Icon(
Icons.screen_lock_rotation,
color: _rotateMarkerWithHeading ? Colors.white : null,
),
),
const SizedBox(height: 8),
],
// In simple mode: show ruler FAB directly
Consumer<AppProvider>(
builder: (context, appProvider, _) {
@@ -2053,52 +2483,10 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
);
},
),
// Hide other buttons when in drawing mode
// Continue with other buttons when not in drawing mode
if (!drawingProvider.isDrawing) ...[
FloatingActionButton.small(
heroTag: 'center_map',
onPressed: !_isMapReady ? null : () async {
// First tap: reset rotation if not 0
// Second tap (or if rotation is 0): jump to current location
if (_getMapRotation() != 0) {
_resetMapRotation();
return;
}
// Get current zoom to retain it
final currentZoom = _mapController.camera.zoom;
// Force update GPS location and jump to it
final position = await _locationService.getCurrentPosition();
if (position != null && mounted) {
setState(() {
// Position updated in service
});
_mapController.move(
LatLng(position.latitude, position.longitude),
currentZoom,
);
} else {
// Fallback to cached position or default center
final currentPosition = _locationService.currentPosition;
if (currentPosition != null) {
_mapController.move(
LatLng(
currentPosition.latitude,
currentPosition.longitude,
),
currentZoom,
);
} else {
_mapController.move(center, currentZoom);
}
}
},
child: const Icon(Icons.my_location),
),
const SizedBox(height: 8),
// Trail controls button
const TrailControls(),
// Trail controls button
const TrailControls(),
const SizedBox(height: 8),
FloatingActionButton.small(
heroTag: 'layer_selector',