diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8d6af6f..330ddf6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -41,7 +41,9 @@ "WebFetch(domain:github.com)", "Bash(find:*)", "WebFetch(domain:prostor.zgs.gov.si)", - "Bash(curl:*)" + "Bash(curl:*)", + "Read(//private/tmp/**)", + "Read(///**)" ], "deny": [], "ask": [] diff --git a/WMS_IMPLEMENTATION_PLAN.md b/WMS_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..3c091e7 --- /dev/null +++ b/WMS_IMPLEMENTATION_PLAN.md @@ -0,0 +1,463 @@ +# Implementation Plan: Generic WMS Server Support + +## Overview +Refactor the hardcoded Slovenian WMS implementation to support adding custom WMS servers while preserving existing functionality. + +## Goals +1. **Add WMS Server Management** - UI to add/edit/delete custom WMS servers via GetCapabilities URL +2. **Layer Selection** - Allow users to select base layers and overlays from any WMS server +3. **Preserve Slovenian Layers** - Keep existing EPSG:3794 support and language filtering +4. **Dynamic CRS** - Support common coordinate systems (EPSG:3857, 4326, 3794) +5. **Base Layer Switching** - Allow switching base layers on top of other layers + +## Implementation Phases + +### Phase 1: Core Models & Parsing (Foundation) + +**1.1 Create WMS Server Model** (`lib/models/wms_server.dart`) +- `WmsServer` class with id, name, capabilitiesUrl, version, layers list +- `WmsLayer` class with name, title, abstract, styles, CRS support, bounds +- JSON serialization for persistence + +**1.2 Implement WMS Capabilities Parser** (`lib/services/wms_capabilities_parser.dart`) +- Add `xml: ^7.0.0` dependency to pubspec.yaml +- Parse WMS 1.3.0 GetCapabilities XML +- Extract service metadata, supported formats, CRS list +- Recursively parse layer hierarchy (handle groups vs leaf layers) +- Extract bounding boxes, styles, and metadata + +**1.3 Create CRS Factory** (`lib/utils/crs_factory.dart`) +- Registry for common CRS: EPSG:3857 (Web Mercator), EPSG:4326 (WGS84), EPSG:3794 (Slovenian) +- Cache CRS instances to avoid re-creating projection objects +- Support for retrieving CRS by EPSG code + +**1.4 Create Storage Repository** (`lib/services/wms_server_repository.dart`) +- Store custom WMS servers in SharedPreferences as JSON +- CRUD operations: save, load, delete servers +- Persist layer visibility state per server/layer + +### Phase 2: UI Implementation + +**2.1 WMS Server Management Screen** (`lib/screens/wms_server_management_screen.dart`) +- List of saved WMS servers (edit/delete actions) +- "Add WMS Server" button → capabilities URL input dialog +- Show loading indicator while fetching GetCapabilities +- Parse and preview available layers +- Layer picker with checkboxes (base layers vs overlays) +- Validate CRS compatibility (warn if unsupported) +- Save button stores server + selected layers + +**2.2 Refactor Layer Selector** (`lib/screens/map_tab.dart`) +- Keep existing sections: Standard Online Layers, Built-in WMS (Slovenian - language filtered), MBTiles +- Add new section: "Custom WMS Servers" (expandable) +- Each custom server shows its base layers as selectable tiles +- "Manage WMS Servers" button at bottom → opens management screen + +**2.3 Overlay Management** +- Extend existing cadastral/forest roads overlay pattern to custom WMS layers +- Store overlay visibility per server/layer combination +- Add overlay toggles to layer selector when custom WMS base layer active + +### Phase 3: Integration & Testing + +**3.1 Refactor MapLayer Model** (`lib/models/map_layer.dart`) +- Add fields: `wmsServerId` (reference to custom server), `crsCode` (EPSG string) +- Add `isBuiltIn` flag to distinguish Slovenian layers from custom +- Factory method: `MapLayer.fromWmsServer(WmsServer, WmsLayer, Crs)` +- Keep existing Slovenian layer factories unchanged + +**3.2 Update MapProvider** (`lib/providers/map_provider.dart`) +- Load custom WMS servers on init +- Handle base layer switching with CRS changes +- Automatically clamp zoom level when switching to layers with lower maxZoom +- Persist selected custom WMS layer to SharedPreferences + +**3.3 Tile Caching Integration** +- Verify existing `getTileProviderForWms()` works with custom servers +- No changes needed (already uses `flutter_map`'s WMS tile URL generation) + +**3.4 Testing** +- Test with known WMS servers: NASA GIBS, USGS, OpenStreetMap WMS +- Test Slovenian layers still work (no regression) +- Test CRS switching (3857 ↔ 4326 ↔ 3794) +- Test offline caching with custom WMS tiles +- Test error cases: invalid URL, timeout, unsupported CRS, malformed XML + +### Phase 4: Polish & Documentation + +**4.1 Localization** +- Add strings to `lib/l10n/app_*.arb` files: + - "Add WMS Server", "Capabilities URL", "Custom WMS Servers" + - Error messages: "Invalid URL", "Parsing failed", "Unsupported CRS" +- Generate with `flutter gen-l10n` + +**4.2 Error Handling** +- Network timeout (30s) with user-friendly error +- XML parsing errors with "Invalid GetCapabilities response" +- Unsupported CRS warning with fallback suggestion +- Handle missing layer names, invalid bounds gracefully + +**4.3 Update Documentation** (`CLAUDE.md`) +- Document WMS server management workflow +- Add custom WMS section to "Common Tasks" +- Update architecture diagram with new models/services +- Add troubleshooting section for common WMS issues + +## Technical Decisions + +### Preservation of Slovenian Layers +- **Approach**: Keep existing code paths intact, add `isBuiltIn: true` flag +- **Language Filtering**: Remains unchanged (sl/hr only see Slovenian layers) +- **CRS**: EPSG:3794 remains as singleton in `slovenian_crs.dart` + +### Storage Strategy +- **Choice**: SharedPreferences (JSON serialization) +- **Rationale**: Estimated 700KB for 10 servers × 50 layers (under 1MB limit) +- **Migration Path**: Can move to SQLite if users need >10 servers + +### CRS Support (Initial Release) +- **Supported**: EPSG:3857 (Web Mercator), EPSG:4326 (WGS84), EPSG:3794 (Slovenian) +- **Unsupported**: Show warning, prevent selection +- **Future**: Add manual CRS configuration for advanced users + +### WMS Version Support +- **Phase 1**: WMS 1.3.0 only (most common) +- **Future**: Add WMS 1.1.1 (requires axis order handling) + +## Key Files to Modify + +**New Files**: +- `lib/models/wms_server.dart` +- `lib/services/wms_capabilities_parser.dart` +- `lib/services/wms_server_repository.dart` +- `lib/utils/crs_factory.dart` +- `lib/screens/wms_server_management_screen.dart` + +**Modified Files**: +- `lib/models/map_layer.dart` (add fields, factory method) +- `lib/screens/map_tab.dart` (layer selector UI) +- `lib/providers/map_provider.dart` (load custom servers, persistence) +- `lib/l10n/app_*.arb` (localized strings) +- `pubspec.yaml` (add `xml: ^7.0.0`) +- `CLAUDE.md` (documentation update) + +**Unchanged Files** (critical for backward compatibility): +- `lib/utils/slovenian_crs.dart` ✅ +- `lib/services/tile_cache_service.dart` ✅ + +## Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| Slow GetCapabilities responses (5-30s) | Show loading indicator, 30s timeout, cache for 24h | +| Unsupported CRS breaks map | Validate CRS before saving, show warning, fallback to 3857 | +| Tile matrix mismatch causes 400 errors | Document limitation, suggest WMTS if available | +| Breaking existing Slovenian functionality | Keep all existing code paths, add `isBuiltIn` flag, extensive testing | + +## Estimated Timeline +- **Phase 1**: 1-2 weeks (models, parser, storage) +- **Phase 2**: 1 week (UI screens) +- **Phase 3**: 1-2 weeks (integration, testing) +- **Phase 4**: 1 week (polish, docs) +- **Total**: 4-6 weeks (single developer, full-time) + +## Success Criteria +✅ Users can add custom WMS servers via GetCapabilities URL +✅ Users can select base layers and overlays from custom servers +✅ Slovenian layers continue to work with language filtering +✅ Base layer switching works (including CRS changes) +✅ Custom WMS tiles are cached for offline use +✅ Clear error messages for invalid/unsupported servers +✅ No regressions in existing map functionality + +--- + +## Detailed Implementation Notes + +### WMS Server Model Structure + +```dart +class WmsServer { + final String id; // UUID for persistence + final String name; // User-friendly name + final String capabilitiesUrl; // GetCapabilities endpoint + final String version; // WMS version (1.1.1, 1.3.0) + final List availableLayers; + final List supportedCrs; + final DateTime lastUpdated; // Cache invalidation + + Map toJson(); + factory WmsServer.fromJson(Map json); +} + +class WmsLayer { + final String name; // Layer identifier + final String title; // Human-readable title + final String? abstract; + final List styles; + final LatLngBounds? boundingBox; + final List supportedCrs; + final bool supportsTransparency; + final String? legendUrl; +} +``` + +### WMS GetCapabilities XML Parsing + +Key elements to extract: + +```xml + + + Server Name + + + + Root Layer + EPSG:4326 + EPSG:3857 + + layer_name + Layer Title + EPSG:3857 + + -180 + 180 + -90 + 90 + + + + + + +``` + +### CRS Factory Implementation + +```dart +class CrsFactory { + static final Map _crsCache = { + 'EPSG:3794': getSlovenianCrs(), + 'EPSG:3857': const Epsg3857(), + 'EPSG:4326': const Epsg4326(), + }; + + static Crs? getCrs(String epsgCode) { + return _crsCache[epsgCode]; + } + + static bool isSupported(String epsgCode) { + return _crsCache.containsKey(epsgCode); + } +} +``` + +### Storage JSON Schema + +```json +{ + "id": "uuid-here", + "name": "My WMS Server", + "capabilitiesUrl": "https://example.com/wms?SERVICE=WMS&REQUEST=GetCapabilities", + "version": "1.3.0", + "lastUpdated": "2025-10-28T12:00:00Z", + "layers": [ + { + "name": "layer_name", + "title": "Layer Title", + "abstract": "Description...", + "supportedCrs": ["EPSG:3857", "EPSG:4326"], + "boundingBox": { + "south": -90, "west": -180, + "north": 90, "east": 180 + }, + "styles": ["default"], + "supportsTransparency": true + } + ], + "supportedCrs": ["EPSG:3857", "EPSG:4326"] +} +``` + +### UI Flow Diagrams + +**Adding a WMS Server**: +1. User taps "Manage WMS Servers" in Map Management screen +2. User taps "Add WMS Server" button +3. Dialog appears with text field for capabilities URL +4. User enters URL (e.g., `https://prostor.zgs.gov.si/geoserver/wms?SERVICE=WMS&REQUEST=GetCapabilities`) +5. App fetches and parses GetCapabilities +6. Layer picker shows available layers with checkboxes +7. User selects layers to use as base layers or overlays +8. User taps "Save" → server stored in SharedPreferences +9. Layers appear in map layer selector + +**Using a Custom WMS Layer**: +1. User opens layer selector in Map tab +2. User scrolls to "Custom WMS Servers" section +3. User taps custom layer → map switches to that layer +4. If CRS differs from previous layer, map CRS updates +5. If zoom level exceeds layer's maxZoom, zoom is clamped +6. Overlay toggles appear if custom server has overlay layers + +### Testing Checklist + +**Functional Tests**: +- [ ] Add WMS server with valid GetCapabilities URL +- [ ] Parse layers with nested hierarchy (group layers) +- [ ] Select base layer from custom WMS server +- [ ] Switch between standard tile layer and custom WMS layer +- [ ] Switch between custom WMS layers with different CRS +- [ ] Toggle overlay layers from custom WMS server +- [ ] Delete custom WMS server +- [ ] Edit custom WMS server (re-fetch capabilities) +- [ ] Persist selected custom WMS layer across app restarts + +**Error Handling Tests**: +- [ ] Invalid URL (malformed) +- [ ] Network timeout (30s) +- [ ] Invalid XML (not a GetCapabilities response) +- [ ] Empty layer list +- [ ] Unsupported CRS (show warning, prevent selection) +- [ ] Missing required fields (layer name, title) + +**Regression Tests**: +- [ ] Slovenian aerial imagery still loads +- [ ] Slovenian overlays (cadastral, forest roads) still work +- [ ] Language filtering (sl/hr) still hides WMS layers for other locales +- [ ] EPSG:3794 CRS still works correctly +- [ ] Standard tile layers (OSM, OpenTopoMap) still work +- [ ] MBTiles offline layers still work +- [ ] Tile caching still works for WMS tiles + +**Performance Tests**: +- [ ] GetCapabilities fetch completes within 30s +- [ ] Large layer lists (>100 layers) render without lag +- [ ] Switching layers is smooth (no UI freeze) +- [ ] SharedPreferences storage under 1MB for 10 servers + +### Example WMS Servers for Testing + +**Public WMS Servers**: +1. **NASA GIBS** (satellite imagery): + - URL: `https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi?SERVICE=WMS&REQUEST=GetCapabilities` + - CRS: EPSG:4326 + - Layers: MODIS, VIIRS, Landsat + +2. **USGS National Map** (US topographic): + - URL: `https://basemap.nationalmap.gov/arcgis/services/USGSTopo/MapServer/WMSServer?SERVICE=WMS&REQUEST=GetCapabilities` + - CRS: EPSG:3857 + - Layers: US Topo + +3. **OpenStreetMap WMS** (reference): + - URL: `https://ows.terrestris.de/osm/service?SERVICE=WMS&REQUEST=GetCapabilities` + - CRS: EPSG:3857, EPSG:4326 + - Layers: OSM-WMS + +4. **Slovenian Government** (current built-in): + - URL: `https://prostor.zgs.gov.si/geowebcache/service/wms?SERVICE=WMS&REQUEST=GetCapabilities` + - CRS: EPSG:3794, EPSG:3857 + - Layers: DOF_2024, kn_parcele, gozdne_ceste + +### Critical Gotchas + +**WMS Version Differences**: +- WMS 1.1.1 uses ``, WMS 1.3.0 uses `` +- EPSG:4326 axis order differs (lon,lat vs lat,lon) +- bbox parameter order changes between versions + +**Layer Inheritance**: +- Child layers inherit CRS from parent if not specified +- Root layer CRS applies to all children unless overridden + +**Group vs Leaf Layers**: +- Only layers with `` can be requested in GetMap +- Layers without `` are groups (organizational only) + +**Namespace Prefixes**: +- Layer names may include workspace namespace (e.g., `pregledovalnik:DOF_2024`) +- Must be included in GetMap request exactly as in GetCapabilities + +**Tile Grid Alignment**: +- WMS uses arbitrary bounding boxes (not aligned tile grids) +- Works for dynamic rendering but may have caching issues +- WMTS is better for tile caching but requires separate implementation + +### Future Enhancements (Post-MVP) + +**Phase 5: Advanced Features** (not in initial scope): +- WMS 1.1.1 support (axis order handling) +- WMTS support (better tile caching) +- Custom CRS registration (manual proj4 definition input) +- Layer groups (hierarchical tree view) +- Legend display for overlay layers +- GetFeatureInfo support (tap on map to query layer attributes) +- Layer metadata viewer (abstract, attribution, keywords) +- Batch import/export of WMS server configurations + +**Phase 6: Performance Optimization**: +- Background GetCapabilities refresh (update layer list without blocking UI) +- Lazy loading of layer metadata (only fetch when user expands server) +- Thumbnail preview for layers (GetMap with small bbox) +- Server health check (ping before adding) + +--- + +## References + +- **WMS 1.3.0 Specification**: https://www.ogc.org/standards/wms +- **EPSG Registry**: https://epsg.io/ +- **Proj4 Definitions**: https://proj4.org/ +- **flutter_map WMS Docs**: https://docs.fleaflet.dev/layers/tile-layer/wms-tile-layer +- **xml package**: https://pub.dev/packages/xml +- **Slovenian WMS**: https://prostor.zgs.gov.si/geowebcache/service/wms?REQUEST=GetCapabilities&SERVICE=WMS +- **NASA GIBS**: https://wiki.earthdata.nasa.gov/display/GIBS/GIBS+API+for+Developers +- **OGC WMS Best Practices**: https://www.ogc.org/standards/wms + +--- + +## Implementation Checklist + +### Phase 1: Foundation +- [ ] Add `xml: ^7.0.0` to pubspec.yaml +- [ ] Create `lib/models/wms_server.dart` with JSON serialization +- [ ] Create `lib/services/wms_capabilities_parser.dart` with XML parsing +- [ ] Create `lib/utils/crs_factory.dart` with CRS registry +- [ ] Create `lib/services/wms_server_repository.dart` with SharedPreferences storage +- [ ] Write unit tests for capabilities parser + +### Phase 2: UI +- [ ] Create `lib/screens/wms_server_management_screen.dart` +- [ ] Add "Manage WMS Servers" button to Map Management screen +- [ ] Implement "Add WMS Server" dialog with URL input +- [ ] Implement layer picker with checkbox selection +- [ ] Add custom WMS section to layer selector in `map_tab.dart` +- [ ] Add "Manage WMS Servers" button to layer selector + +### Phase 3: Integration +- [ ] Add `wmsServerId`, `crsCode`, `isBuiltIn` fields to `MapLayer` +- [ ] Add `MapLayer.fromWmsServer()` factory method +- [ ] Update `MapProvider` to load custom WMS servers on init +- [ ] Update layer switching logic to handle CRS changes +- [ ] Update zoom clamping logic for custom layers +- [ ] Add overlay management for custom WMS layers +- [ ] Verify tile caching works with custom WMS + +### Phase 4: Polish +- [ ] Add localized strings to all `app_*.arb` files (en, hr, sl, de, es, fr, it) +- [ ] Add error handling with user-friendly messages +- [ ] Add loading indicators for GetCapabilities fetch +- [ ] Add CRS compatibility warnings +- [ ] Update `CLAUDE.md` with WMS documentation +- [ ] Test with multiple public WMS servers +- [ ] Regression test Slovenian layers +- [ ] Performance test with large layer lists + +--- + +**Document Version**: 1.0 +**Last Updated**: 2025-10-28 +**Status**: Ready for Implementation diff --git a/docs/wms_layer_analysis.md b/docs/wms_layer_analysis.md new file mode 100644 index 0000000..a642931 --- /dev/null +++ b/docs/wms_layer_analysis.md @@ -0,0 +1,304 @@ +# Slovenian WMS Layers - SAR Application Analysis + +**Total Layers Found: 108** + +## HIGH PRIORITY - Critical for SAR Operations + +### Aerial Imagery & Base Maps (5 layers) +1. **pregledovalnik:DOF_2024** - Digital Orthophoto 2024 (Latest aerial imagery) + - **USE CASE**: Primary visual reference, current terrain conditions + - **ALREADY IMPLEMENTED** in the app + +2. **pregledovalnik:DOF25** - Digital Orthophoto 25cm resolution + - **USE CASE**: High-resolution aerial imagery for detailed terrain analysis + +3. **pregledovalnik:DOF_IR** - Infrared Orthophoto + - **USE CASE**: Thermal/infrared imagery for detecting heat signatures, useful for night searches + +4. **pregledovalnik:DTK25** - Topographic Map 1:25,000 + - **USE CASE**: Traditional topographic reference with contours, trails, landmarks + +5. **pregledovalnik:dof025_2022_2024** - Combined orthophoto 2022-2024 + - **USE CASE**: Multi-year aerial imagery comparison + +### Administrative Boundaries (6 layers) +6. **pregledovalnik:NEP_RPE_OBCINE** - Municipalities (občine) + - **USE CASE**: Jurisdiction boundaries for coordinating with local authorities + - **RECOMMENDED FOR OVERLAY** + +7. **pregledovalnik:NEP_RPE_NASELJA** - Settlements + - **USE CASE**: Identify populated areas, evacuation points, staging areas + - **RECOMMENDED FOR OVERLAY** + +8. **pregledovalnik:NEP_HISNE_STEVILKE** - House Numbers + - **USE CASE**: Precise location identification for emergency response + +9. **pregledovalnik:NEP_RPE_UPRAVNE_ENOTE** - Administrative Units + - **USE CASE**: Regional administration boundaries + +10. **pregledovalnik:NEP_RPE_STATISTICNE_REGIJE** - Statistical Regions + - **USE CASE**: Broader regional planning + +11. **pregledovalnik:drzavna_meja** - State Border + - **USE CASE**: International coordination for cross-border operations + +### Roads & Transportation (4 layers) +12. **pregledovalnik:KGI_LINIJE_CESTE_G** - Roads + - **USE CASE**: Primary access routes, evacuation routes, vehicle navigation + - **HIGH PRIORITY OVERLAY** + +13. **pregledovalnik:gozdne_ceste** - Forest Roads + - **USE CASE**: Access to remote forest areas, critical for SAR vehicles + - **HIGH PRIORITY OVERLAY** + +14. **pregledovalnik:LINIJE_ZELEZNICE_G** - Railways + - **USE CASE**: Alternative access routes, landmarks, coordination with rail authorities + +15. **pregledovalnik:KGI_LINIJE_PLANINSKE_POTI_G** - Mountain/Hiking Trails + - **USE CASE**: Common search areas, lost hiker routes, access to remote areas + - **HIGH PRIORITY OVERLAY** + +### Fire & Emergency Hazards (5 layers) +16. **pregledovalnik:gozdni_pozari** - Forest Fires (historical) + - **USE CASE**: Historical fire locations, high-risk areas + - **RECOMMENDED FOR OVERLAY** + +17. **pregledovalnik:pozarna_ogrozenost** - Fire Hazard/Risk Areas + - **USE CASE**: Identify high fire risk zones, plan safe evacuation routes + - **HIGH PRIORITY OVERLAY** + +18. **pregledovalnik:pozarisce_goriski_kras** - Fire Site - Goriški Kras + - **USE CASE**: Specific fire hazard area in Karst region + +19. **pregledovalnik:pozarisce_kras** - Fire Site - Kras + - **USE CASE**: Karst region fire zones + +20. **pregledovalnik:protipozarne_preseke** - Firebreaks + - **USE CASE**: Fire containment lines, safe zones + +### Geographic Names & Navigation (1 layer) +21. **pregledovalnik:zemljepisna_imena** - Geographic Names + - **USE CASE**: Place names for communication, location identification + - **RECOMMENDED FOR OVERLAY** + +--- + +## MEDIUM PRIORITY - Useful for Planning & Context + +### Protected Areas & Natural Features (9 layers) +22. **pregledovalnik:natura2000** - Natura 2000 Protected Areas + - **USE CASE**: Environmental restrictions, sensitive areas + +23. **pregledovalnik:zavarovana_obmocja_poligoni** - Protected Areas (polygons) + - **USE CASE**: National parks, nature reserves, access restrictions + +24. **pregledovalnik:zavarovana_obmocja_tocke** - Protected Areas (points) + - **USE CASE**: Point-based protected sites + +25. **pregledovalnik:zavarovana_obmocja_conacija** - Protected Areas (zoning) + - **USE CASE**: Zoning within protected areas + +26. **pregledovalnik:naravne_vrednote_poligoni** - Natural Heritage (polygons) + - **USE CASE**: Notable natural features, landmarks + +27. **pregledovalnik:naravne_vrednote_tocke** - Natural Heritage (points) + - **USE CASE**: Specific natural landmarks (caves, waterfalls, etc.) + +28. **pregledovalnik:epo_poligoni** - Single Trees/Monuments (polygons) + - **USE CASE**: Notable landmark trees + +29. **pregledovalnik:epo_tocke** - Single Trees/Monuments (points) + - **USE CASE**: Point landmarks + +30. **pregledovalnik:gozdni_rezervati** - Forest Reserves + - **USE CASE**: Old-growth forests, restricted access areas + +### Cadastral & Land Parcels (2 layers) +31. **pregledovalnik:kn_parcele** - Cadastral Parcels + - **USE CASE**: Land ownership boundaries, legal jurisdictions + +32. **pregledovalnik:KN_KATASTRSKE_OBCINE** - Cadastral Municipalities + - **USE CASE**: Cadastral administrative units + +### Terrain & Elevation (2 layers) +33. **pregledovalnik:DMK** - Digital Cartographic Model + - **USE CASE**: Contours, terrain features, elevation data + +34. **pregledovalnik:DMR** - Digital Relief Model + - **USE CASE**: Shaded relief, terrain visualization + +### Forest Management Areas (10 layers) +35. **pregledovalnik:gge** - Forest Management Units (GGE) + - **USE CASE**: Forest administrative units, contact local foresters + +36. **pregledovalnik:ggo** - Forest Management Districts (GGO) + - **USE CASE**: Larger forest districts + +37. **pregledovalnik:revirji** - Forest Ranger Districts + - **USE CASE**: Local ranger contact areas + +38. **pregledovalnik:krajevne_enote** - Local Forest Units + - **USE CASE**: Smallest administrative forest units + +39. **pregledovalnik:odseki** - Forest Compartments + - **USE CASE**: Forest subdivisions for management + +40. **pregledovalnik:odseki_gozdni** - Forest Compartments (variant) + - **USE CASE**: Alternative compartment layer + +41. **pregledovalnik:sestoji** - Forest Stands + - **USE CASE**: Specific tree stand types, vegetation density + +42. **pregledovalnik:sestoji_druga_gozdna_zemljisca** - Other Forest Lands + - **USE CASE**: Non-productive forest areas + +43. **pregledovalnik:varovalni_gozdovi** - Protection Forests + - **USE CASE**: Forests with protective function (avalanche, erosion) + +44. **pregledovalnik:lovisca** - Hunting Grounds + - **USE CASE**: Contact with hunting associations for local knowledge + +### Historical Disasters (6 layers) +45. **pregledovalnik:vetrolom_2017** - Windthrow 2017 + - **USE CASE**: Storm damage areas, difficult terrain + +46. **pregledovalnik:vetrolom_2018** - Windthrow 2018 + - **USE CASE**: Storm damage areas from 2018 + +47. **pregledovalnik:zled_2014** - Ice Storm 2014 + - **USE CASE**: Ice damage areas + +48. **pregledovalnik:zled_drugi_gozdovi_2014** - Ice Storm 2014 (other forests) + - **USE CASE**: Ice damage in secondary forests + +49. **pregledovalnik:podlubniki_2015_2019** - Bark Beetle Damage 2015-2019 + - **USE CASE**: Dead wood areas, fire hazard, difficult terrain + +50. **pregledovalnik:krcitve** - Clearcuts + - **USE CASE**: Open areas, recent harvest sites, potential staging areas + +### Agricultural & Land Use (2 layers) +51. **pregledovalnik:povrsine_v_zarascanju** - Overgrown Areas + - **USE CASE**: Abandoned agricultural land, changing terrain + +52. **pregledovalnik:skupna_kmetijska_politika_2023_2027** - Common Agricultural Policy + - **USE CASE**: Agricultural land use planning + +--- + +## LOW PRIORITY - Technical/Specialized Layers + +### Forest Function Layers (ON21 series - 45 layers) +These are highly specialized forest function layers from the 2021 forest management plan. Each has variants for lines (_l), polygons (_p), and points (_t): + +**Categories:** +- **Biotska** (Biodiversity): on21_fun_biotska_l/p/t +- **Druge gozdne dobrine** (Other forest goods): on21_fun_druge_gozdne_dobrine_l/p/t +- **Estetska** (Aesthetic): on21_fun_estetska_l/p/t +- **Hidroloska** (Hydrological): on21_fun_hidroloska_l/p/t +- **Higiensko-zdravstvena** (Health/hygiene): on21_fun_higiensko_zdravstvena_p +- **Klimatska** (Climate): on21_fun_klimatska_l/p +- **Kulturna** (Cultural): on21_fun_kulturna_l/p/t +- **Lesnoproizvodna** (Timber production): on21_fun_lesnoproizvodna_p +- **Lovnogospodarska** (Hunting management): on21_fun_lovnogospodarska_p/t +- **Obrambna** (Defense): on21_fun_obrambna_p/t +- **Poucna** (Educational): on21_fun_poucna_l/p/t +- **Raziskovalna** (Research): on21_fun_raziskovalna_p/t +- **Rekreacijska** (Recreation): on21_fun_rekreacijska_l/p/t +- **Skupaj** (Combined): on21_fun_skupaj_l/p/t +- **Turisticna** (Tourism): on21_fun_turisticna_l/p/t +- **Varovalna** (Protection): on21_fun_varovalna_p +- **Varovanja naravnih vrednot** (Natural heritage protection): on21_fun_varovanja_naravnih_vrednot_l/p/t +- **Zascitna** (Conservation): on21_fun_zascitna_l/p + +**USE CASE**: Very specialized forest planning data. May be useful for: +- **Rekreacijska**: Popular recreation areas (lost hikers) +- **Turisticna**: Tourist areas (search priority) +- **Varovalna**: Avalanche/erosion protection forests (hazard awareness) + +### Miscellaneous Technical (6 layers) +- **pregledovalnik:conacija_gp** - Zonation (technical) +- **pregledovalnik:evrd** - Single-tree selection forests +- **pregledovalnik:koridorji** - Corridors (ecological) +- **pregledovalnik:luo** - Forest landscape units +- **pregledovalnik:pobude_gge** - GGE initiatives +- **pregledovalnik:provenience** - Seed provenance areas +- **pregledovalnik:uvhvvr** - High conservation value forests +- **pregledovalnik:gozdni_sklad_ekocelice** - Forest fund eco-cells +- **pregledovalnik:gozdni_sklad_habitatna_drevesa** - Habitat trees + +### Layer Groups (2 layers) +- **pregledovalnik:ttn_group** - Group layer (container) +- **pregledovalnik:zemljevid_group** - Map group layer (container) + +--- + +## RECOMMENDED IMPLEMENTATION PLAN + +### Phase 1: Critical Overlays (Immediate) +Add these layers as toggleable overlays in the Map Options screen: + +1. **Forest Roads** (`gozdne_ceste`) - PNG, transparent + - Critical for vehicle access in remote areas + +2. **Hiking/Mountain Trails** (`KGI_LINIJE_PLANINSKE_POTI_G`) - PNG, transparent + - Common search areas for lost hikers + +3. **Fire Hazard Zones** (`pozarna_ogrozenost`) - PNG, transparent + - Safety planning, risk assessment + +4. **Settlements** (`NEP_RPE_NASELJA`) - PNG, transparent + - Populated areas, staging areas + +5. **Municipalities** (`NEP_RPE_OBCINE`) - PNG, transparent + - Administrative boundaries + +### Phase 2: Additional Useful Layers +6. **Geographic Names** (`zemljepisna_imena`) +7. **Forest Fires Historical** (`gozdni_pozari`) +8. **Protected Areas** (`zavarovana_obmocja_poligoni`) +9. **Topographic Map** (`DTK25`) - Alternative base layer +10. **Infrared Imagery** (`DOF_IR`) - Alternative base layer + +### Phase 3: Specialized Layers (On Demand) +11. **Windthrow/Disaster Areas** (for post-disaster operations) +12. **Recreation/Tourism Areas** (for prioritizing search areas) +13. **Protection Forests** (avalanche/erosion hazard awareness) + +--- + +## TECHNICAL NOTES + +### CRS Compatibility +- All layers from `prostor.zgs.gov.si` support **EPSG:3794** (Slovenian National Grid) +- The app already has the correct CRS implementation in `lib/utils/slovenian_crs.dart` + +### Recommended Formats +- **Base Layers**: JPEG (better compression for imagery) +- **Overlays**: PNG with transparency=true (for stacking) + +### Caching Strategy +- All layers should use the existing FMTC caching infrastructure +- 30-day validity is appropriate for most layers +- Consider longer validity for static layers (administrative boundaries) + +### Performance Considerations +- Limit active overlays to 3-4 simultaneously to avoid performance issues +- Use appropriate zoom level restrictions (some layers only useful at close zoom) +- Consider pre-downloading critical layers for offline SAR operations + +--- + +## LAYER NAMING CONVENTIONS + +**Slovenian Terms Reference:** +- **DOF** = Digitalni Ortofoto (Digital Orthophoto) +- **DTK** = Državna Topografska Karta (State Topographic Map) +- **DMK** = Digitalni Kartografski Model (Digital Cartographic Model) +- **DMR** = Digitalni Model Reliefa (Digital Relief Model) +- **NEP** = Nacionalni Evidenčni Portal (National Registry Portal) +- **RPE** = Register Prostorskih Enot (Spatial Units Register) +- **GGE** = Gozdnogospodarska Enota (Forest Management Unit) +- **GGO** = Gozdnogospodarska Območje (Forest Management District) +- **ON21** = Območni Načrt 2021 (Regional Plan 2021) + diff --git a/ios/Runner.app.dSYM.zip b/ios/Runner.app.dSYM.zip index 338e17e..6266eda 100644 Binary files a/ios/Runner.app.dSYM.zip and b/ios/Runner.app.dSYM.zip differ diff --git a/ios/Runner.ipa b/ios/Runner.ipa index a5c0bd2..ea16e39 100644 Binary files a/ios/Runner.ipa and b/ios/Runner.ipa differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 55d0c5d..108fcde 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -489,7 +489,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 45; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -511,7 +511,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 45; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_TEAM = JND55328G8; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -530,7 +530,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 45; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_TEAM = JND55328G8; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -547,7 +547,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 45; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_TEAM = JND55328G8; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -679,7 +679,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 45; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -702,7 +702,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 45; + CURRENT_PROJECT_VERSION = 46; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index a894bbf..deab66f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -43,7 +43,7 @@ CFBundleSignature ???? CFBundleVersion - 45 + 46 LSRequiresIPhoneOS NSBluetoothAlwaysUsageDescription diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index c2dff83..5316700 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,22 @@ - + - + - + - + diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8634009..2864153 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5277cfc..02c4eb4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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" diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index efe2888..daa2ebb 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -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", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 4fc448a..753d8f4 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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" diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb index b5d27b6..e40fdab 100644 --- a/lib/l10n/app_hr.arb +++ b/lib/l10n/app_hr.arb @@ -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", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 45190f7..dc2cd65 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -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" diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2ca7381..b5214b2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1600466..fbd5008 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a669f26..54c5f8c 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 945fae9..0be0372 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -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'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 94f1825..2aea8ca 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index e87f121..b71752e 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 25d5134..52b3686 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -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'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e91919e..361b802 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -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'; diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index eab77dd..1ff69e0 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -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", diff --git a/lib/models/map_layer.dart b/lib/models/map_layer.dart index 75e2e25..14733d8 100644 --- a/lib/models/map_layer.dart +++ b/lib/models/map_layer.dart @@ -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 allLayers = [ openStreetMap, openTopoMap, diff --git a/lib/providers/map_provider.dart b/lib/providers/map_provider.dart index 9c86a42..d1047f1 100644 --- a/lib/providers/map_provider.dart +++ b/lib/providers/map_provider.dart @@ -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 toggleHikingTrailsOverlay() async { + _showHikingTrailsOverlay = !_showHikingTrailsOverlay; + notifyListeners(); + await _saveOverlayState(); + } + + /// Toggle main roads overlay + Future toggleMainRoadsOverlay() async { + _showMainRoadsOverlay = !_showMainRoadsOverlay; + notifyListeners(); + await _saveOverlayState(); + } + + /// Toggle house numbers overlay + Future toggleHouseNumbersOverlay() async { + _showHouseNumbersOverlay = !_showHouseNumbersOverlay; + notifyListeners(); + await _saveOverlayState(); + } + + /// Toggle fire hazard zones overlay + Future toggleFireHazardZonesOverlay() async { + _showFireHazardZonesOverlay = !_showFireHazardZonesOverlay; + notifyListeners(); + await _saveOverlayState(); + } + + /// Toggle historical fires overlay + Future toggleHistoricalFiresOverlay() async { + _showHistoricalFiresOverlay = !_showHistoricalFiresOverlay; + notifyListeners(); + await _saveOverlayState(); + } + + /// Toggle firebreaks overlay + Future toggleFirebreaksOverlay() async { + _showFirebreaksOverlay = !_showFirebreaksOverlay; + notifyListeners(); + await _saveOverlayState(); + } + + /// Toggle Kras fire zones overlay + Future toggleKrasFireZonesOverlay() async { + _showKrasFireZonesOverlay = !_showKrasFireZonesOverlay; + notifyListeners(); + await _saveOverlayState(); + } + + /// Toggle place names overlay + Future togglePlaceNamesOverlay() async { + _showPlaceNamesOverlay = !_showPlaceNamesOverlay; + notifyListeners(); + await _saveOverlayState(); + } + + /// Toggle municipality borders overlay + Future toggleMunicipalityBordersOverlay() async { + _showMunicipalityBordersOverlay = !_showMunicipalityBordersOverlay; + notifyListeners(); + await _saveOverlayState(); + } + /// Load overlay state from SharedPreferences Future 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 diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index 7de5f5a..eb5121b 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -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 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 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 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 with AutomaticKeepAliveClientMixin { } /// Get all available layers (default + WMS + MBTiles) - List get _allLayers => [...MapLayer.allLayers, _slovenianAerialLayer, ..._mbtilesLayers]; + List get _allLayers => [...MapLayer.allLayers, _slovenianAerialLayer, _dtk25Layer, ..._mbtilesLayers]; Future _loadSettings() async { final prefs = await SharedPreferences.getInstance(); @@ -275,7 +275,6 @@ class _MapTabState extends State 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 with AutomaticKeepAliveClientMixin { Future _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 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 contacts, List sarMarkers) { return _markerService.calculateCenter( contacts: contacts, @@ -592,9 +578,9 @@ class _MapTabState extends State 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 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 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 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 with AutomaticKeepAliveClientMixin { ); }, ), + // Hiking trails overlay + Consumer( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( builder: (context, mapProvider, _) { @@ -1904,19 +2271,6 @@ class _MapTabState extends State 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 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( builder: (context, appProvider, _) { @@ -2053,52 +2483,10 @@ class _MapTabState extends State 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',