From 38b4522e0e513a2ecb2041d1dfbddd12a308c92e Mon Sep 17 00:00:00 2001 From: awoni Date: Mon, 25 Aug 2025 19:58:29 +0900 Subject: [PATCH 1/2] feat: add nativeZooms property for custom zoom level support - Add nativeZooms property to specify available discrete zoom levels - Add NativeZoomStrategy enum with multiple selection strategies - Implement intelligent zoom clamping based on distance and strategy - Maintain backward compatibility with existing zoom behavior --- lib/src/layer/tile_layer/tile_layer.dart | 86 +++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 3dcba4c0f..e275e74c5 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -28,6 +28,24 @@ part 'wms_tile_layer_options.dart'; /// You should read up about the options by exploring each one, or visiting /// https://docs.fleaflet.dev/usage/layers/tile-layer. Some are important to /// avoid issues. + +/// Strategy for selecting native zoom level when the target zoom is equidistant +/// from two available native zoom levels. +enum NativeZoomStrategy { + /// Prefer lower zoom level (better performance, lower quality) + preferLower, + + /// Prefer higher zoom level (better quality, more bandwidth) + preferHigher, + + /// Always round down to the nearest native zoom + alwaysLower, + + /// Always round up to the nearest native zoom + alwaysHigher, +} + + @immutable class TileLayer extends StatefulWidget { /// The URL template is a string that contains placeholders, which, when filled @@ -113,6 +131,15 @@ class TileLayer extends StatefulWidget { /// Otherwise, this should be specified. late final int maxNativeZoom; + /// Native zoom levels (optional). + /// If specified, only these zoom levels will be used for fetching tiles. + /// For example: [4, 6, 8, 10] for JMA weather tiles. + late final List? nativeZooms; + + /// Strategy for selecting native zoom level when equidistant. + /// Defaults to [NativeZoomStrategy.preferLower] for better performance. + final NativeZoomStrategy nativeZoomStrategy; + /// If set to true, the zoom number used in tile URLs will be reversed /// (`maxZoom - zoom` instead of `zoom`) final bool zoomReverse; @@ -241,6 +268,8 @@ class TileLayer extends StatefulWidget { double maxZoom = double.infinity, int minNativeZoom = 0, int maxNativeZoom = 19, + this.nativeZooms, + this.nativeZoomStrategy = NativeZoomStrategy.preferLower, this.zoomReverse = false, double zoomOffset = 0.0, this.additionalOptions = const {}, @@ -745,8 +774,61 @@ See: /// Rounds the zoom to the nearest int and clamps it to the native zoom limits /// if there are any. - int _clampToNativeZoom(double zoom) => - zoom.round().clamp(widget.minNativeZoom, widget.maxNativeZoom); + int _clampToNativeZoom(double zoom) { + if (widget.nativeZooms != null && widget.nativeZooms!.isNotEmpty) { + final targetZoom = zoom.round(); + final sortedZooms = List.from(widget.nativeZooms!)..sort(); + + // Return as-is if exact match is found + if (sortedZooms.contains(targetZoom)) { + return targetZoom; + } + + // Find the maximum value <= targetZoom and minimum value >= targetZoom + int? lowerZoom; + int? upperZoom; + + for (final z in sortedZooms) { + if (z < targetZoom) { + lowerZoom = z; + } else if (z > targetZoom) { + upperZoom = z; + break; + } + } + + // Handle boundary cases + if (lowerZoom == null) { + return sortedZooms.first; + } else if (upperZoom == null) { + return sortedZooms.last; + } + + // Calculate distances + final lowerDiff = targetZoom - lowerZoom; + final upperDiff = upperZoom - targetZoom; + + // Select based on strategy + switch (widget.nativeZoomStrategy) { + case NativeZoomStrategy.preferLower: + // Choose lower when distances are equal (default) + return lowerDiff <= upperDiff ? lowerZoom : upperZoom; + + case NativeZoomStrategy.preferHigher: + // Choose higher when distances are equal + return lowerDiff < upperDiff ? lowerZoom : upperZoom; + + case NativeZoomStrategy.alwaysLower: + // Always choose lower + return lowerZoom; + + case NativeZoomStrategy.alwaysHigher: + // Always choose higher + return upperZoom; + } + } + return zoom.round().clamp(widget.minNativeZoom, widget.maxNativeZoom); + } void _onTileLoadError(TileImage tile, Object error, StackTrace? stackTrace) { debugPrint(error.toString()); From 3dbf12b8fa5cf2187205aa2686009b713353887b Mon Sep 17 00:00:00 2001 From: awoni Date: Thu, 28 Aug 2025 07:28:43 +0900 Subject: [PATCH 2/2] Fix formatting issues --- lib/src/layer/tile_layer/tile_layer.dart | 29 ++++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index e275e74c5..2b64860c7 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -29,23 +29,22 @@ part 'wms_tile_layer_options.dart'; /// https://docs.fleaflet.dev/usage/layers/tile-layer. Some are important to /// avoid issues. -/// Strategy for selecting native zoom level when the target zoom is equidistant +/// Strategy for selecting native zoom level when the target zoom is equidistant /// from two available native zoom levels. enum NativeZoomStrategy { /// Prefer lower zoom level (better performance, lower quality) preferLower, - + /// Prefer higher zoom level (better quality, more bandwidth) preferHigher, - + /// Always round down to the nearest native zoom alwaysLower, - + /// Always round up to the nearest native zoom alwaysHigher, } - @immutable class TileLayer extends StatefulWidget { /// The URL template is a string that contains placeholders, which, when filled @@ -135,7 +134,7 @@ class TileLayer extends StatefulWidget { /// If specified, only these zoom levels will be used for fetching tiles. /// For example: [4, 6, 8, 10] for JMA weather tiles. late final List? nativeZooms; - + /// Strategy for selecting native zoom level when equidistant. /// Defaults to [NativeZoomStrategy.preferLower] for better performance. final NativeZoomStrategy nativeZoomStrategy; @@ -778,16 +777,16 @@ See: if (widget.nativeZooms != null && widget.nativeZooms!.isNotEmpty) { final targetZoom = zoom.round(); final sortedZooms = List.from(widget.nativeZooms!)..sort(); - + // Return as-is if exact match is found if (sortedZooms.contains(targetZoom)) { return targetZoom; } - + // Find the maximum value <= targetZoom and minimum value >= targetZoom int? lowerZoom; int? upperZoom; - + for (final z in sortedZooms) { if (z < targetZoom) { lowerZoom = z; @@ -796,32 +795,32 @@ See: break; } } - + // Handle boundary cases if (lowerZoom == null) { return sortedZooms.first; } else if (upperZoom == null) { return sortedZooms.last; } - + // Calculate distances final lowerDiff = targetZoom - lowerZoom; final upperDiff = upperZoom - targetZoom; - + // Select based on strategy switch (widget.nativeZoomStrategy) { case NativeZoomStrategy.preferLower: // Choose lower when distances are equal (default) return lowerDiff <= upperDiff ? lowerZoom : upperZoom; - + case NativeZoomStrategy.preferHigher: // Choose higher when distances are equal return lowerDiff < upperDiff ? lowerZoom : upperZoom; - + case NativeZoomStrategy.alwaysLower: // Always choose lower return lowerZoom; - + case NativeZoomStrategy.alwaysHigher: // Always choose higher return upperZoom;