diff --git a/lib/localizations/en.json b/lib/localizations/en.json index f5ed7e3..b55a10f 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -191,6 +191,11 @@ "attribution": "Attribution", "attributionHint": "© Map Provider", "attributionRequired": "Attribution is required", + "maxZoom": "Max Zoom Level", + "maxZoomHint": "Maximum zoom level (1-19)", + "maxZoomRequired": "Max zoom is required", + "maxZoomInvalid": "Max zoom must be a number", + "maxZoomRange": "Max zoom must be between {} and {}", "fetchPreview": "Fetch Preview", "previewTileLoaded": "Preview tile loaded successfully", "previewTileFailed": "Failed to fetch preview: {}", diff --git a/lib/models/tile_provider.dart b/lib/models/tile_provider.dart index 5348396..d563991 100644 --- a/lib/models/tile_provider.dart +++ b/lib/models/tile_provider.dart @@ -8,6 +8,7 @@ class TileType { final String urlTemplate; final String attribution; final Uint8List? previewTile; // Single tile image data for preview + final int maxZoom; // Maximum zoom level for this tile type const TileType({ required this.id, @@ -15,6 +16,7 @@ class TileType { required this.urlTemplate, required this.attribution, this.previewTile, + this.maxZoom = 18, // Default max zoom level }); /// Create URL for a specific tile, replacing template variables @@ -40,6 +42,7 @@ class TileType { 'urlTemplate': urlTemplate, 'attribution': attribution, 'previewTile': previewTile != null ? base64Encode(previewTile!) : null, + 'maxZoom': maxZoom, }; static TileType fromJson(Map json) => TileType( @@ -50,6 +53,7 @@ class TileType { previewTile: json['previewTile'] != null ? base64Decode(json['previewTile']) : null, + maxZoom: json['maxZoom'] ?? 18, // Default to 18 if not specified ); TileType copyWith({ @@ -58,12 +62,14 @@ class TileType { String? urlTemplate, String? attribution, Uint8List? previewTile, + int? maxZoom, }) => TileType( id: id ?? this.id, name: name ?? this.name, urlTemplate: urlTemplate ?? this.urlTemplate, attribution: attribution ?? this.attribution, previewTile: previewTile ?? this.previewTile, + maxZoom: maxZoom ?? this.maxZoom, ); @override @@ -151,6 +157,7 @@ class DefaultTileProviders { name: 'Street Map', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap contributors', + maxZoom: 19, ), ], ), diff --git a/lib/screens/tile_provider_editor_screen.dart b/lib/screens/tile_provider_editor_screen.dart index a3fb243..2cb9ea7 100644 --- a/lib/screens/tile_provider_editor_screen.dart +++ b/lib/screens/tile_provider_editor_screen.dart @@ -256,6 +256,7 @@ class _TileTypeDialogState extends State<_TileTypeDialog> { late final TextEditingController _nameController; late final TextEditingController _urlController; late final TextEditingController _attributionController; + late final TextEditingController _maxZoomController; Uint8List? _previewTile; bool _isLoadingPreview = false; @@ -266,6 +267,7 @@ class _TileTypeDialogState extends State<_TileTypeDialog> { _nameController = TextEditingController(text: tileType?.name ?? ''); _urlController = TextEditingController(text: tileType?.urlTemplate ?? ''); _attributionController = TextEditingController(text: tileType?.attribution ?? ''); + _maxZoomController = TextEditingController(text: (tileType?.maxZoom ?? 18).toString()); _previewTile = tileType?.previewTile; } @@ -274,6 +276,7 @@ class _TileTypeDialogState extends State<_TileTypeDialog> { _nameController.dispose(); _urlController.dispose(); _attributionController.dispose(); + _maxZoomController.dispose(); super.dispose(); } @@ -326,6 +329,22 @@ class _TileTypeDialogState extends State<_TileTypeDialog> { validator: (value) => value?.trim().isEmpty == true ? locService.t('tileTypeEditor.attributionRequired') : null, ), const SizedBox(height: 16), + TextFormField( + controller: _maxZoomController, + decoration: InputDecoration( + labelText: locService.t('tileTypeEditor.maxZoom'), + hintText: locService.t('tileTypeEditor.maxZoomHint'), + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value?.trim().isEmpty == true) return locService.t('tileTypeEditor.maxZoomRequired'); + final zoom = int.tryParse(value!); + if (zoom == null) return locService.t('tileTypeEditor.maxZoomInvalid'); + if (zoom < 1 || zoom > kAbsoluteMaxZoom) return locService.t('tileTypeEditor.maxZoomRange', params: ['1', kAbsoluteMaxZoom.toString()]); + return null; + }, + ), + const SizedBox(height: 16), Row( children: [ TextButton.icon( @@ -425,6 +444,7 @@ class _TileTypeDialogState extends State<_TileTypeDialog> { urlTemplate: _urlController.text.trim(), attribution: _attributionController.text.trim(), previewTile: _previewTile, + maxZoom: int.parse(_maxZoomController.text.trim()), ); widget.onSave(tileType); diff --git a/lib/widgets/map/tile_layer_manager.dart b/lib/widgets/map/tile_layer_manager.dart index 2411c96..e12b8d8 100644 --- a/lib/widgets/map/tile_layer_manager.dart +++ b/lib/widgets/map/tile_layer_manager.dart @@ -78,6 +78,7 @@ class TileLayerManager { return TileLayer( urlTemplate: urlTemplate, userAgentPackageName: 'me.deflock.deflockapp', + maxZoom: selectedTileType?.maxZoom?.toDouble() ?? 18.0, tileProvider: NetworkTileProvider( httpClient: _tileHttpClient, // Enable flutter_map caching - cache busting handled by URL changes and FlutterMap key diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 93d6942..d02c532 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -452,7 +452,7 @@ class MapViewState extends State { options: MapOptions( initialCenter: _gpsController.currentLocation ?? _positionManager.initialLocation ?? LatLng(37.7749, -122.4194), initialZoom: _positionManager.initialZoom ?? 15, - maxZoom: 19, + maxZoom: (appState.selectedTileType?.maxZoom ?? 18).toDouble(), onPositionChanged: (pos, gesture) { setState(() {}); // Instant UI update for zoom, etc. if (gesture) widget.onUserGesture();