17 KiB
Implementation Plan: Generic WMS Server Support
Overview
Refactor the hardcoded Slovenian WMS implementation to support adding custom WMS servers while preserving existing functionality.
Goals
- Add WMS Server Management - UI to add/edit/delete custom WMS servers via GetCapabilities URL
- Layer Selection - Allow users to select base layers and overlays from any WMS server
- Preserve Slovenian Layers - Keep existing EPSG:3794 support and language filtering
- Dynamic CRS - Support common coordinate systems (EPSG:3857, 4326, 3794)
- 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)
WmsServerclass with id, name, capabilitiesUrl, version, layers listWmsLayerclass 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.0dependency 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
isBuiltInflag 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_*.arbfiles:- "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: trueflag - 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.dartlib/services/wms_capabilities_parser.dartlib/services/wms_server_repository.dartlib/utils/crs_factory.dartlib/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(addxml: ^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
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<WmsLayer> availableLayers;
final List<String> supportedCrs;
final DateTime lastUpdated; // Cache invalidation
Map<String, dynamic> toJson();
factory WmsServer.fromJson(Map<String, dynamic> json);
}
class WmsLayer {
final String name; // Layer identifier
final String title; // Human-readable title
final String? abstract;
final List<String> styles;
final LatLngBounds? boundingBox;
final List<String> supportedCrs;
final bool supportsTransparency;
final String? legendUrl;
}
WMS GetCapabilities XML Parsing
Key elements to extract:
<WMS_Capabilities version="1.3.0">
<Service>
<Title>Server Name</Title>
</Service>
<Capability>
<Layer>
<Title>Root Layer</Title>
<CRS>EPSG:4326</CRS>
<CRS>EPSG:3857</CRS>
<Layer queryable="1">
<Name>layer_name</Name>
<Title>Layer Title</Title>
<CRS>EPSG:3857</CRS>
<EX_GeographicBoundingBox>
<westBoundLongitude>-180</westBoundLongitude>
<eastBoundLongitude>180</eastBoundLongitude>
<southBoundLatitude>-90</southBoundLatitude>
<northBoundLatitude>90</northBoundLatitude>
</EX_GeographicBoundingBox>
<Style>
<Name>default</Name>
</Style>
</Layer>
</Layer>
</Capability>
</WMS_Capabilities>
CRS Factory Implementation
class CrsFactory {
static final Map<String, Crs> _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
{
"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:
- User taps "Manage WMS Servers" in Map Management screen
- User taps "Add WMS Server" button
- Dialog appears with text field for capabilities URL
- User enters URL (e.g.,
https://prostor.zgs.gov.si/geoserver/wms?SERVICE=WMS&REQUEST=GetCapabilities) - App fetches and parses GetCapabilities
- Layer picker shows available layers with checkboxes
- User selects layers to use as base layers or overlays
- User taps "Save" → server stored in SharedPreferences
- Layers appear in map layer selector
Using a Custom WMS Layer:
- User opens layer selector in Map tab
- User scrolls to "Custom WMS Servers" section
- User taps custom layer → map switches to that layer
- If CRS differs from previous layer, map CRS updates
- If zoom level exceeds layer's maxZoom, zoom is clamped
- 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:
-
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
- URL:
-
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
- URL:
-
OpenStreetMap WMS (reference):
- URL:
https://ows.terrestris.de/osm/service?SERVICE=WMS&REQUEST=GetCapabilities - CRS: EPSG:3857, EPSG:4326
- Layers: OSM-WMS
- URL:
-
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
- URL:
Critical Gotchas
WMS Version Differences:
- WMS 1.1.1 uses
<SRS>, WMS 1.3.0 uses<CRS> - 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
<Name>can be requested in GetMap - Layers without
<Name>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.0to pubspec.yaml - Create
lib/models/wms_server.dartwith JSON serialization - Create
lib/services/wms_capabilities_parser.dartwith XML parsing - Create
lib/utils/crs_factory.dartwith CRS registry - Create
lib/services/wms_server_repository.dartwith 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,isBuiltInfields toMapLayer - Add
MapLayer.fromWmsServer()factory method - Update
MapProviderto 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_*.arbfiles (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.mdwith 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