diff --git a/example/pubspec.lock b/example/pubspec.lock index 4db9e9b..38cf1e3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -191,10 +191,10 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da" + sha256: bbf145e8220531f2f727608c431871c7457f3b134e513543913afd00fdc1cd47 url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "8.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -328,7 +328,7 @@ packages: source: hosted version: "2.5.0" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 @@ -662,4 +662,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.0-91.0.dev <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.27.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9bc1af3..8433f88 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: "A new Flutter project." -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" version: 1.0.0+1 environment: @@ -12,11 +12,11 @@ dependencies: flutter_gpu: sdk: flutter - + gpu_vector_tile_renderer: path: .. - flutter_map: ^7.0.2 + flutter_map: ^8.1.0 latlong2: ^0.9.1 # Ffi / Native assets @@ -24,6 +24,7 @@ dependencies: flutter_gpu_shaders: ^0.1.3 native_assets_cli: ^0.8.0 native_toolchain_c: ^0.5.4 + logging: ^1.3.0 dev_dependencies: flutter_test: diff --git a/lib/src/controller/controller.dart b/lib/src/controller/controller.dart index d04de42..c4abda8 100644 --- a/lib/src/controller/controller.dart +++ b/lib/src/controller/controller.dart @@ -65,7 +65,7 @@ class VectorTileLayerController with ChangeNotifier { notifyListeners(); // Trigger camera changed to set the initial visible tiles - if (_lastCamera != null) onCameraChanged(_lastCamera!, _lastTileSize!); + if (_lastCamera != null) onCameraChanged(_lastCamera!, _lastDimension!); } catch (e) { _logger.severe('Failed to load style', e); rethrow; @@ -91,7 +91,7 @@ class VectorTileLayerController with ChangeNotifier { } fm.MapCamera? _lastCamera; - double? _lastTileSize; + int? _lastDimension; TileRangeCalculator? _tileRangeCalculator; Map? _tileBoundsForSources; @@ -101,15 +101,16 @@ class VectorTileLayerController with ChangeNotifier { /// /// If the style hasn't been loaded yet, it will trigger updates once it's loaded, using the latest state of the /// camera passed to this method. - void onCameraChanged(fm.MapCamera camera, double tileSize) { + void onCameraChanged(fm.MapCamera camera, int tileDimension) { _lastCamera = camera; - _lastTileSize = tileSize; + _lastDimension = tileDimension; if (!isLoaded) return; final crs = camera.crs; // Check if we need to update the tile range calculator - if (_tileRangeCalculator?.tileSize != tileSize) _tileRangeCalculator = TileRangeCalculator(tileSize: tileSize); + if (_tileRangeCalculator?.tileDimension != tileDimension) + _tileRangeCalculator = TileRangeCalculator(tileDimension: tileDimension); // Check if we need to update the tile bounds for sources _tileBoundsForSources ??= {}; @@ -118,7 +119,7 @@ class VectorTileLayerController with ChangeNotifier { final source = entry.value; final existingBounds = _tileBoundsForSources![key]; - if (existingBounds != null && !existingBounds.shouldReplace(crs, tileSize, null)) continue; + if (existingBounds != null && !existingBounds.shouldReplace(crs, tileDimension, null)) continue; if (source is spec.SourceVector && source.tiles != null) { final bounds = fm.LatLngBounds.unsafe( @@ -128,7 +129,7 @@ class VectorTileLayerController with ChangeNotifier { west: source.bounds[0].toDouble(), ); - _tileBoundsForSources![key] = TileBounds(crs: crs, tileSize: tileSize, latLngBounds: bounds); + _tileBoundsForSources![key] = TileBounds(crs: crs, tileDimension: tileDimension, latLngBounds: bounds); } } @@ -143,7 +144,7 @@ class VectorTileLayerController with ChangeNotifier { final sourceKey = entry.key; final source = style.sources[sourceKey]! as spec.SourceVector; final bounds = entry.value; - + // todo: clean this up final zoomForSource = tileZoom.clamp(source.minzoom, source.maxzoom).toInt(); final boundsAtZoom = bounds.atZoom(zoomForSource); diff --git a/lib/src/debug/debug_painter.dart b/lib/src/debug/debug_painter.dart index 798cfda..0897a4a 100644 --- a/lib/src/debug/debug_painter.dart +++ b/lib/src/debug/debug_painter.dart @@ -1,42 +1,40 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:gpu_vector_tile_renderer/_controller.dart'; -import 'package:gpu_vector_tile_renderer/_renderer.dart'; import 'package:gpu_vector_tile_renderer/_vector_tile.dart' as vt; import 'package:gpu_vector_tile_renderer/src/utils/flutter_map/tile_scale_calculator.dart'; class MapDebugPainter extends CustomPainter { - MapDebugPainter({required this.camera, required this.controller, required this.tileSize}) + MapDebugPainter({required this.camera, required this.controller, required this.tileDimension}) : super(repaint: controller); final MapCamera camera; - final double tileSize; + final int tileDimension; final VectorTileLayerController controller; @override void paint(Canvas canvas, Size size) { - final tileScaleCalculator = TileScaleCalculator(crs: camera.crs, tileSize: tileSize); + final tileScaleCalculator = TileScaleCalculator(crs: camera.crs, tileDimension: tileDimension); tileScaleCalculator.clearCacheUnlessZoomMatches(camera.zoom); for (final tile in controller.tiles) { if (tile.isLoaded) { final vt = tile.vectorTiles.values.first; - final tileSize = tileScaleCalculator.scaledTileSize(camera.zoom, tile.coordinates.z); + final tileDimension = tileScaleCalculator.scaledTileDimension(camera.zoom, tile.coordinates.z); final origin = Offset( - tile.coordinates.x * tileSize - camera.pixelOrigin.x, - tile.coordinates.y * tileSize - camera.pixelOrigin.y, + tile.coordinates.x * tileDimension - camera.pixelOrigin.dx, + tile.coordinates.y * tileDimension - camera.pixelOrigin.dy, ); final transform = Matrix4.identity() ..translate(origin.dx, origin.dy) - ..scale(tileSize / vt.layers.first.extent, tileSize / vt.layers.first.extent); + ..scale(tileDimension / vt.layers.first.extent, tileDimension / vt.layers.first.extent); canvas.drawRect( - Rect.fromLTWH(origin.dx, origin.dy, tileSize, tileSize), + Rect.fromLTWH(origin.dx, origin.dy, tileDimension, tileDimension), Paint() ..color = Colors.black.withValues(alpha: 0.2) ..style = PaintingStyle.stroke, diff --git a/lib/src/renderer/context.dart b/lib/src/renderer/context.dart index cc61424..a3b602a 100644 --- a/lib/src/renderer/context.dart +++ b/lib/src/renderer/context.dart @@ -16,7 +16,6 @@ class RenderContext { required this.size, required this.pixelRatio, required this.camera, - required this.unscaledTileSize, required this.tileScaleCalculator, required this.eval, }); @@ -25,12 +24,11 @@ class RenderContext { final Size size; final double pixelRatio; final MapCamera camera; - final double unscaledTileSize; final EvaluationContext eval; final TileScaleCalculator tileScaleCalculator; - double getScaledTileSize(TileCoordinates coordinates) => - tileScaleCalculator.scaledTileSize(camera.zoom, coordinates.z); + double getScaledTileDimension(TileCoordinates coordinates) => + tileScaleCalculator.scaledTileDimension(camera.zoom, coordinates.z); Size get scaledSize => size * pixelRatio; @@ -40,23 +38,23 @@ class RenderContext { worldToGl.translate(-1.0, 1.0); worldToGl.scale(pixelRatio); worldToGl.scale(2.0 / scaledSize.width, -2.0 / scaledSize.height); - worldToGl.translate(-camera.pixelOrigin.x, -camera.pixelOrigin.y); + worldToGl.translate(-camera.pixelOrigin.dx, -camera.pixelOrigin.dy); return worldToGl; } void setTileScissor(RenderPass pass, TileCoordinates coordinates) { - final scaledTileSize = getScaledTileSize(coordinates); + final scaledTileDimension = getScaledTileDimension(coordinates); final origin = Offset( - coordinates.x * scaledTileSize - camera.pixelOrigin.x, - coordinates.y * scaledTileSize - camera.pixelOrigin.y, + coordinates.x * scaledTileDimension - camera.pixelOrigin.dx, + coordinates.y * scaledTileDimension - camera.pixelOrigin.dy, ); var _x = (origin.dx * pixelRatio).ceil(); var _y = (origin.dy * pixelRatio).ceil(); - var _width = (scaledTileSize * pixelRatio).ceil(); - var _height = (scaledTileSize * pixelRatio).ceil(); + var _width = (scaledTileDimension * pixelRatio).ceil(); + var _height = (scaledTileDimension * pixelRatio).ceil(); if (_x < 0) { _width += _x; diff --git a/lib/src/renderer/layers/_generator.dart b/lib/src/renderer/layers/_generator.dart index 3d97c02..7e6481e 100644 --- a/lib/src/renderer/layers/_generator.dart +++ b/lib/src/renderer/layers/_generator.dart @@ -31,7 +31,7 @@ List setUniformsGenerator(List uniformEval, List uniform ' double cameraZoom,', ' double pixelRatio,', ' Matrix4 tileLocalToWorld,', - ' double tileSize,', + ' int tileDimension,', ' double tileExtent,', ' double tileOpacity,', ') {', @@ -42,8 +42,8 @@ List setUniformsGenerator(List uniformEval, List uniform ' cameraWorldToGl: cameraWorldToGl,', ' cameraZoom: cameraZoom,', ' cameraPixelRatio: pixelRatio,' - ' tileLocalToWorld: tileLocalToWorld,', - ' tileSize: tileSize,', + ' tileLocalToWorld: tileLocalToWorld,', + ' tileDimension: tileDimension,', ' tileExtent: tileExtent,', ' tileOpacity: tileOpacity,', ...uniformSetters.map((v) => ' $v,'), diff --git a/lib/src/renderer/layers/fill_layer_renderer.dart b/lib/src/renderer/layers/fill_layer_renderer.dart index 5021253..4fb015c 100644 --- a/lib/src/renderer/layers/fill_layer_renderer.dart +++ b/lib/src/renderer/layers/fill_layer_renderer.dart @@ -75,8 +75,8 @@ abstract class FillLayerRenderer extends SingleTileLayerRenderer void draw(RenderContext context) { if (!pipeline.isReady) return; - final tileSize = context.getScaledTileSize(coordinates); - final origin = ui.Offset(coordinates.x * tileSize, coordinates.y * tileSize); + final tileDimension = context.getScaledTileDimension(coordinates); + final origin = ui.Offset(coordinates.x * tileDimension, coordinates.y * tileDimension); final tileLocalToWorld = Matrix4.identity()..translate(origin.dx, origin.dy); setUniforms( @@ -85,7 +85,7 @@ abstract class FillLayerRenderer extends SingleTileLayerRenderer context.camera.zoom, context.pixelRatio, tileLocalToWorld, - tileSize, + tileDimension, vtLayer.extent.toDouble(), container.opacityAnimation.value, ); diff --git a/lib/src/renderer/layers/line_layer_renderer.dart b/lib/src/renderer/layers/line_layer_renderer.dart index 10fd689..b9dc49b 100644 --- a/lib/src/renderer/layers/line_layer_renderer.dart +++ b/lib/src/renderer/layers/line_layer_renderer.dart @@ -188,8 +188,8 @@ abstract class LineLayerRenderer extends SingleTileLayerRenderer void draw(RenderContext context) { if (!pipeline.isReady) return; - final tileSize = context.getScaledTileSize(coordinates); - final origin = ui.Offset(coordinates.x * tileSize, coordinates.y * tileSize); + final tileDimension = context.getScaledTileDimension(coordinates); + final origin = ui.Offset(coordinates.x * tileDimension, coordinates.y * tileDimension); final tileLocalToWorld = Matrix4.identity()..translate(origin.dx, origin.dy); setUniforms( @@ -198,7 +198,7 @@ abstract class LineLayerRenderer extends SingleTileLayerRenderer context.camera.zoom, context.pixelRatio, tileLocalToWorld, - tileSize, + tileDimension, vtLayer.extent.toDouble(), container.opacityAnimation.value, ); diff --git a/lib/src/renderer/render_orchestrator.dart b/lib/src/renderer/render_orchestrator.dart index d7583dc..af0c4bb 100644 --- a/lib/src/renderer/render_orchestrator.dart +++ b/lib/src/renderer/render_orchestrator.dart @@ -81,16 +81,16 @@ class VectorTileLayerRenderOrchestrator with ChangeNotifier { } fm.MapCamera? _lastCamera; - double? _lastTileSize; + int? _lastTileDimension; TileScaleCalculator? _tileScaleCalculator; - void onCameraChanged(fm.MapCamera camera, double tileSize) { - if (_tileScaleCalculator == null || tileSize != _lastTileSize) { - _tileScaleCalculator = TileScaleCalculator(crs: camera.crs, tileSize: tileSize); + void onCameraChanged(fm.MapCamera camera, int tileDimension) { + if (_tileScaleCalculator == null || tileDimension != _lastTileDimension) { + _tileScaleCalculator = TileScaleCalculator(crs: camera.crs, tileDimension: tileDimension); } _lastCamera = camera; - _lastTileSize = tileSize; + _lastTileDimension = tileDimension; _tileScaleCalculator!.clearCacheUnlessZoomMatches(camera.zoom); } @@ -134,7 +134,7 @@ class VectorTileLayerRenderOrchestrator with ChangeNotifier { required ui.Size size, required double pixelRatio, required fm.MapCamera camera, - required double tileSize, + required int tileDimension, }) { if (_layers == null) return null; _setupTextures(size, pixelRatio); @@ -171,7 +171,6 @@ class VectorTileLayerRenderOrchestrator with ChangeNotifier { size: size, pixelRatio: pixelRatio, camera: camera, - unscaledTileSize: tileSize, tileScaleCalculator: _tileScaleCalculator!, eval: spec.EvaluationContext(geometryType: '', zoom: camera.zoom, locale: spec.Locale(languageCode: 'en')), ); diff --git a/lib/src/renderer/render_orchestrator_painter.dart b/lib/src/renderer/render_orchestrator_painter.dart index 2ec7fa4..c2bb3fd 100644 --- a/lib/src/renderer/render_orchestrator_painter.dart +++ b/lib/src/renderer/render_orchestrator_painter.dart @@ -6,18 +6,18 @@ class RenderOrchestratorPainter extends CustomPainter { RenderOrchestratorPainter({ required this.camera, required this.pixelRatio, - required this.tileSize, + required this.tileDimension, required this.orchestrator, }) : super(repaint: orchestrator); final MapCamera camera; final double pixelRatio; - final double tileSize; + final int tileDimension; final VectorTileLayerRenderOrchestrator orchestrator; @override void paint(Canvas canvas, Size size) { - final image = orchestrator.draw(camera: camera, pixelRatio: pixelRatio, size: size, tileSize: tileSize); + final image = orchestrator.draw(camera: camera, pixelRatio: pixelRatio, size: size, tileDimension: tileDimension); if (image == null) return; canvas.scale(1 / pixelRatio); diff --git a/lib/src/utils/flutter_map/LICENSE b/lib/src/utils/flutter_map/LICENSE index 70a0f04..c66e13d 100644 --- a/lib/src/utils/flutter_map/LICENSE +++ b/lib/src/utils/flutter_map/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2018-2024, the 'flutter_map' authors and maintainers +Copyright (c) 2018-2025, the 'flutter_map' authors and maintainers All rights reserved. diff --git a/lib/src/utils/flutter_map/README.md b/lib/src/utils/flutter_map/README.md index 7e8e9ba..57c7ca7 100644 --- a/lib/src/utils/flutter_map/README.md +++ b/lib/src/utils/flutter_map/README.md @@ -1 +1,3 @@ -Files here are copied from the flutter_map repository (https://github.com/fleaflet/flutter_map). +Files here are copied from the flutter_map repository (). + +The future intention is to make the FM repo more extendable to prevent this. diff --git a/lib/src/utils/flutter_map/extensions.dart b/lib/src/utils/flutter_map/extensions.dart new file mode 100644 index 0000000..b6284ef --- /dev/null +++ b/lib/src/utils/flutter_map/extensions.dart @@ -0,0 +1,91 @@ +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:meta/meta.dart'; + +/// Extension methods for the [math.Point] class +@internal +extension PointExtension on math.Point { + /// Create a new [math.Point] where the [x] and [y] values are divided by [factor]. + math.Point operator /(num factor) { + return math.Point(x / factor, y / factor); + } + + /// Converts to offset + Offset toOffset() => Offset(x.toDouble(), y.toDouble()); +} + +/// Extension methods for [Offset] +@internal +extension OffsetExtension on Offset { + /// Creates a [math.Point] representation of this offset. + math.Point toPoint() => math.Point(dx, dy); + + /// Create a new [Offset] whose [dx] and [dy] values are rotated clockwise by + /// [radians]. + Offset rotate(num radians) { + final cosTheta = math.cos(radians); + final sinTheta = math.sin(radians); + final nx = (cosTheta * dx) + (sinTheta * dy); + final ny = (cosTheta * dy) - (sinTheta * dx); + return Offset(nx, ny); + } + + /// returns new [Offset] where floorToDouble() is called on [dx] and [dy] independently + Offset floor() => Offset(dx.floorToDouble(), dy.floorToDouble()); + + /// returns new [Offset] where roundToDouble() is called on [dx] and [dy] independently + Offset round() => Offset(dx.roundToDouble(), dy.roundToDouble()); +} + +@internal +extension RectExtension on Rect { + /// Create a [Rect] as bounding box of a list of points. + static Rect containing(List points) { + var maxX = double.negativeInfinity; + var maxY = double.negativeInfinity; + var minX = double.infinity; + var minY = double.infinity; + + for (final point in points) { + maxX = math.max(point.dx, maxX); + minX = math.min(point.dx, minX); + maxY = math.max(point.dy, maxY); + minY = math.min(point.dy, minY); + } + + return Rect.fromPoints(Offset(minX, minY), Offset(maxX, maxY)); + } + + /// Checks if the line between the two coordinates is contained within the + /// [Rect]. + bool aabbContainsLine(double x1, double y1, double x2, double y2) { + // Completely outside. + if ((x1 <= left && x2 <= left) || + (y1 <= top && y2 <= top) || + (x1 >= right && x2 >= right) || + (y1 >= bottom && y2 >= bottom)) { + return false; + } + + final m = (y2 - y1) / (x2 - x1); + + double y = m * (left - x1) + y1; + if (y > top && y < bottom) return true; + + y = m * (right - x1) + y1; + if (y > top && y < bottom) return true; + + double x = (top - y1) / m + x1; + if (x > left && x < right) return true; + + x = (bottom - y1) / m + x1; + if (x > left && x < right) return true; + + return false; + } + + Offset get min => topLeft; + + Offset get max => bottomRight; +} diff --git a/lib/src/utils/flutter_map/integer_bounds.dart b/lib/src/utils/flutter_map/integer_bounds.dart new file mode 100644 index 0000000..e623e08 --- /dev/null +++ b/lib/src/utils/flutter_map/integer_bounds.dart @@ -0,0 +1,98 @@ +import 'dart:math'; +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:gpu_vector_tile_renderer/src/utils/flutter_map/extensions.dart'; +import 'package:meta/meta.dart'; + +/// Rectangular bound delimited by orthogonal lines passing through two +/// points. +@immutable +@internal +class IntegerBounds { + /// inthe edge of the bounds with the minimum x and y coordinate + final Point min; + + /// inthe edge of the bounds with the maximum x and y coordinate + final Point max; + + /// Create a [IntegerBounds] instance in a safe way. + factory IntegerBounds(Point a, Point b) { + final int minX; + final int maxX; + if (a.x > b.x) { + minX = b.x; + maxX = a.x; + } else { + minX = a.x; + maxX = b.x; + } + final int minY; + final int maxY; + if (a.y > b.y) { + minY = b.y; + maxY = a.y; + } else { + minY = a.y; + maxY = b.y; + } + return IntegerBounds.unsafe(Point(minX, minY), Point(maxX, maxY)); + } + + /// Create a [IntegerBounds] instance **without** checking if [min] is actually the + /// minimum and [max] is actually the maximum. + const IntegerBounds.unsafe(this.min, this.max); + + /// Creates a new [IntegerBounds] obtained by expanding the current ones with a new + /// point. + IntegerBounds extend(Point point) { + return IntegerBounds.unsafe( + Point(math.min(point.x, min.x), math.min(point.y, min.y)), + Point(math.max(point.x, max.x), math.max(point.y, max.y)), + ); + } + + /// inthis [IntegerBounds] central point. + Offset get center => (min + max).toOffset() / 2; + + /// Bottom-Left corner's point. + Point get bottomLeft => Point(min.x, max.y); + + /// intop-Right corner's point. + Point get topRight => Point(max.x, min.y); + + /// intop-Left corner's point. + Point get topLeft => min; + + /// Bottom-Right corner's point. + Point get bottomRight => max; + + /// A point that contains the difference between the point's axis projections. + Point get size { + return max - min; + } + + /// Check if a [Point] is inside of the bounds. + bool contains(Point point) { + return (point.x >= min.x) && (point.x <= max.x) && (point.y >= min.y) && (point.y <= max.y); + } + + /// Calculates the intersection of two Bounds. inthe return value will be null + /// if there is no intersection. inthe returned bounds may be zero size + /// (bottomLeft == topRight). + IntegerBounds? intersect(IntegerBounds b) { + final leftX = math.max(min.x, b.min.x); + final rightX = math.min(max.x, b.max.x); + final topY = math.max(min.y, b.min.y); + final bottomY = math.min(max.y, b.max.y); + + if (leftX <= rightX && topY <= bottomY) { + return IntegerBounds.unsafe(Point(leftX, topY), Point(rightX, bottomY)); + } + + return null; + } + + @override + String toString() => 'Bounds($min, $max)'; +} diff --git a/lib/src/utils/flutter_map/tile_bounds/tile_bounds.dart b/lib/src/utils/flutter_map/tile_bounds/tile_bounds.dart index 9b679b6..a65be41 100644 --- a/lib/src/utils/flutter_map/tile_bounds/tile_bounds.dart +++ b/lib/src/utils/flutter_map/tile_bounds/tile_bounds.dart @@ -1,62 +1,48 @@ -import 'package:flutter/foundation.dart'; +import 'dart:ui'; + import 'package:flutter_map/flutter_map.dart'; import 'package:gpu_vector_tile_renderer/src/utils/flutter_map/tile_bounds/tile_bounds_at_zoom.dart'; import 'package:gpu_vector_tile_renderer/src/utils/flutter_map/tile_range.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; /// The bounding box of a tile. @immutable abstract class TileBounds { /// Reference to the coordinate reference system. final Crs crs; - final double _tileSize; + final int _tileDimension; final LatLngBounds? _latLngBounds; /// Constructor that creates an instance of a subclass of [TileBounds]: /// [InfiniteTileBounds] if the CRS is infinite. /// [DiscreteTileBounds] if the CRS has hard borders. /// [WrappedTileBounds] if the CRS is wrapped. - factory TileBounds({ - required Crs crs, - required double tileSize, - LatLngBounds? latLngBounds, - }) { + factory TileBounds({required Crs crs, required int tileDimension, LatLngBounds? latLngBounds}) { if (crs.infinite && latLngBounds == null) { - return InfiniteTileBounds._(crs, tileSize, latLngBounds); + return InfiniteTileBounds._(crs, tileDimension, latLngBounds); } else if (crs.wrapLat == null && crs.wrapLng == null) { - return DiscreteTileBounds._(crs, tileSize, latLngBounds); + return DiscreteTileBounds._(crs, tileDimension, latLngBounds); } else { - return WrappedTileBounds._(crs, tileSize, latLngBounds); + return WrappedTileBounds._(crs, tileDimension, latLngBounds); } } - const TileBounds._( - this.crs, - this._tileSize, - this._latLngBounds, - ); + const TileBounds._(this.crs, this._tileDimension, this._latLngBounds); /// Create a [TileBoundsAtZoom] for a given [zoom] level. TileBoundsAtZoom atZoom(int zoom); /// Returns true if these bounds may no longer be valid for the given /// parameters. - bool shouldReplace( - Crs crs, - double tileSize, - LatLngBounds? latLngBounds, - ) => - crs != this.crs || tileSize != _tileSize || latLngBounds != _latLngBounds; + bool shouldReplace(Crs crs, int tileDimension, LatLngBounds? latLngBounds) => + crs != this.crs || tileDimension != _tileDimension || latLngBounds != _latLngBounds; } /// [TileBounds] that have no limits. @immutable class InfiniteTileBounds extends TileBounds { - const InfiniteTileBounds._( - super.crs, - super._tileSize, - super._latLngBounds, - ) : super._(); + const InfiniteTileBounds._(super.crs, super._tileDimension, super._latLngBounds) : super._(); @override TileBoundsAtZoom atZoom(int zoom) => const InfiniteTileBoundsAtZoom(); @@ -67,11 +53,7 @@ class InfiniteTileBounds extends TileBounds { class DiscreteTileBounds extends TileBounds { final Map _tileBoundsAtZoomCache = {}; - DiscreteTileBounds._( - super.crs, - super._tileSize, - super._latLngBounds, - ) : super._(); + DiscreteTileBounds._(super.crs, super._tileDimension, super._latLngBounds) : super._(); /// Return the [TileBoundsAtZoom] for the given zoom level (cached). @override @@ -83,22 +65,18 @@ class DiscreteTileBounds extends TileBounds { TileBoundsAtZoom _tileBoundsAtZoomImpl(int zoom) { final zoomDouble = zoom.toDouble(); - final Bounds pixelBounds; + final Rect pixelBounds; if (_latLngBounds == null) { pixelBounds = crs.getProjectedBounds(zoomDouble)!; } else { - pixelBounds = Bounds( - crs.latLngToPoint(_latLngBounds.southWest, zoomDouble), - crs.latLngToPoint(_latLngBounds.northEast, zoomDouble), + pixelBounds = Rect.fromPoints( + crs.latLngToOffset(_latLngBounds.southWest, zoomDouble), + crs.latLngToOffset(_latLngBounds.northEast, zoomDouble), ); } return DiscreteTileBoundsAtZoom( - DiscreteTileRange.fromPixelBounds( - zoom: zoom, - tileSize: _tileSize, - pixelBounds: pixelBounds, - ), + DiscreteTileRange.fromPixelBounds(zoom: zoom, tileDimension: _tileDimension, pixelBounds: pixelBounds), ); } } @@ -108,11 +86,7 @@ class DiscreteTileBounds extends TileBounds { class WrappedTileBounds extends TileBounds { final Map _tileBoundsAtZoomCache = {}; - WrappedTileBounds._( - super.crs, - super._tileSize, - super._latLngBounds, - ) : super._(); + WrappedTileBounds._(super.crs, super._tileDimension, super._latLngBounds) : super._(); @override TileBoundsAtZoom atZoom(int zoom) { @@ -122,36 +96,32 @@ class WrappedTileBounds extends TileBounds { WrappedTileBoundsAtZoom _tileBoundsAtZoomImpl(int zoom) { final zoomDouble = zoom.toDouble(); - final Bounds pixelBounds; + final Rect pixelBounds; if (_latLngBounds == null) { pixelBounds = crs.getProjectedBounds(zoomDouble)!; } else { - pixelBounds = Bounds( - crs.latLngToPoint(_latLngBounds.southWest, zoomDouble), - crs.latLngToPoint(_latLngBounds.northEast, zoomDouble), + pixelBounds = Rect.fromPoints( + crs.latLngToOffset(_latLngBounds.southWest, zoomDouble), + crs.latLngToOffset(_latLngBounds.northEast, zoomDouble), ); } (int, int)? wrapX; if (crs.wrapLng case final wrapLng?) { - final wrapXMin = (crs.latLngToPoint(LatLng(0, wrapLng.$1), zoomDouble).x / _tileSize).floor(); - final wrapXMax = (crs.latLngToPoint(LatLng(0, wrapLng.$2), zoomDouble).x / _tileSize).ceil(); + final wrapXMin = (crs.latLngToOffset(LatLng(0, wrapLng.$1), zoomDouble).dx / _tileDimension).floor(); + final wrapXMax = (crs.latLngToOffset(LatLng(0, wrapLng.$2), zoomDouble).dx / _tileDimension).ceil(); wrapX = (wrapXMin, wrapXMax - 1); } (int, int)? wrapY; if (crs.wrapLat case final wrapLat?) { - final wrapYMin = (crs.latLngToPoint(LatLng(wrapLat.$1, 0), zoomDouble).y / _tileSize).floor(); - final wrapYMax = (crs.latLngToPoint(LatLng(wrapLat.$2, 0), zoomDouble).y / _tileSize).ceil(); + final wrapYMin = (crs.latLngToOffset(LatLng(wrapLat.$1, 0), zoomDouble).dy / _tileDimension).floor(); + final wrapYMax = (crs.latLngToOffset(LatLng(wrapLat.$2, 0), zoomDouble).dy / _tileDimension).ceil(); wrapY = (wrapYMin, wrapYMax - 1); } return WrappedTileBoundsAtZoom( - tileRange: DiscreteTileRange.fromPixelBounds( - zoom: zoom, - tileSize: _tileSize, - pixelBounds: pixelBounds, - ), + tileRange: DiscreteTileRange.fromPixelBounds(zoom: zoom, tileDimension: _tileDimension, pixelBounds: pixelBounds), wrappedAxisIsAlwaysInBounds: _latLngBounds == null, wrapX: wrapX, wrapY: wrapY, diff --git a/lib/src/utils/flutter_map/tile_range.dart b/lib/src/utils/flutter_map/tile_range.dart index e39200b..df11e43 100644 --- a/lib/src/utils/flutter_map/tile_range.dart +++ b/lib/src/utils/flutter_map/tile_range.dart @@ -1,7 +1,10 @@ import 'dart:math' as math hide Point; import 'dart:math' show Point; +import 'dart:ui'; import 'package:flutter_map/flutter_map.dart'; +import 'package:gpu_vector_tile_renderer/src/utils/flutter_map/extensions.dart'; +import 'package:gpu_vector_tile_renderer/src/utils/flutter_map/integer_bounds.dart'; import 'package:meta/meta.dart'; /// A range of tiles, this is normally a [DiscreteTileRange] and sometimes @@ -25,15 +28,18 @@ class EmptyTileRange extends TileRange { const EmptyTileRange._(super.zoom); @override - Iterable get coordinates => - const Iterable.empty(); + Iterable get coordinates => const Iterable.empty(); } +Point _floor(Offset point) => Point(point.dx.floor(), point.dy.floor()); + +Point _ceil(Offset point) => Point(point.dx.ceil(), point.dy.ceil()); + /// Every [TileRange] is a [DiscreteTileRange] if it's not an [EmptyTileRange]. @immutable class DiscreteTileRange extends TileRange { /// Bounds are inclusive - final Bounds _bounds; + final IntegerBounds _bounds; /// Create a new [DiscreteTileRange] by setting it's values. const DiscreteTileRange(super.zoom, this._bounds); @@ -41,17 +47,17 @@ class DiscreteTileRange extends TileRange { /// Calculate a [DiscreteTileRange] by using the pixel bounds. factory DiscreteTileRange.fromPixelBounds({ required int zoom, - required double tileSize, - required Bounds pixelBounds, + required int tileDimension, + required Rect pixelBounds, }) { - final Bounds bounds; - if (pixelBounds.min == pixelBounds.max) { - final minAndMax = (pixelBounds.min / tileSize).floor(); - bounds = Bounds(minAndMax, minAndMax); + final IntegerBounds bounds; + if (pixelBounds.isEmpty) { + final minAndMax = _floor(pixelBounds.min / tileDimension.toDouble()); + bounds = IntegerBounds(minAndMax, minAndMax); } else { - bounds = Bounds( - (pixelBounds.min / tileSize).floor(), - (pixelBounds.max / tileSize).ceil() - const Point(1, 1), + bounds = IntegerBounds( + _floor(pixelBounds.min / tileDimension.toDouble()), + _ceil(pixelBounds.max / tileDimension.toDouble()) - const Point(1, 1), ); } @@ -88,10 +94,7 @@ class DiscreteTileRange extends TileRange { return DiscreteTileRange( zoom, - Bounds( - Point(math.max(min.x, minX), min.y), - Point(math.min(max.x, maxX), max.y), - ), + IntegerBounds(Point(math.max(min.x, minX), min.y), Point(math.min(max.x, maxX), max.y)), ); } @@ -103,16 +106,32 @@ class DiscreteTileRange extends TileRange { return DiscreteTileRange( zoom, - Bounds( - Point(min.x, math.max(min.y, minY)), - Point(max.x, math.min(max.y, maxY)), - ), + IntegerBounds(Point(min.x, math.max(min.y, minY)), Point(max.x, math.min(max.y, maxY))), ); } /// Check if a [Point] is inside of the bounds of the [DiscreteTileRange]. - bool contains(Point point) { - return _bounds.contains(point); + /// + /// We use a modulo in order to prevent side-effects at the end of the world. + bool contains(Point point, {bool replicatesWorldLongitude = false}) { + if (!replicatesWorldLongitude) { + return _bounds.contains(point); + } + + final int modulo = 1 << zoom; + + bool containsCoordinate(int value, int min, int max) { + int tmp = value; + while (tmp < min) { + tmp += modulo; + } + while (tmp > max) { + tmp -= modulo; + } + return tmp >= min && tmp <= max; + } + + return containsCoordinate(point.x, min.x, max.x) && containsCoordinate(point.y, min.y, max.y); } /// The minimum [Point] of the [DiscreteTileRange] @@ -122,7 +141,7 @@ class DiscreteTileRange extends TileRange { Point get max => _bounds.max; /// The center [Point] of the [DiscreteTileRange] - Point get center => _bounds.center; + Offset get center => _bounds.center; /// Get a list of [TileCoordinates] for the [DiscreteTileRange]. @override diff --git a/lib/src/utils/flutter_map/tile_range_calculator.dart b/lib/src/utils/flutter_map/tile_range_calculator.dart index ef51f7b..8af1d22 100644 --- a/lib/src/utils/flutter_map/tile_range_calculator.dart +++ b/lib/src/utils/flutter_map/tile_range_calculator.dart @@ -1,16 +1,19 @@ -import 'package:flutter/foundation.dart'; +import 'dart:ui'; + import 'package:flutter_map/flutter_map.dart'; +import 'package:gpu_vector_tile_renderer/src/utils/flutter_map/extensions.dart'; import 'package:gpu_vector_tile_renderer/src/utils/flutter_map/tile_range.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; /// The [TileRangeCalculator] helps to calculate the bounds in pixel. @immutable class TileRangeCalculator { /// The tile size in pixels. - final double tileSize; + final int tileDimension; /// Create a new [TileRangeCalculator] instance. - const TileRangeCalculator({required this.tileSize}); + const TileRangeCalculator({required this.tileDimension}); /// Calculates the visible pixel bounds at the [tileZoom] zoom level when /// viewing the map from the [viewingZoom] centered at the [center]. The @@ -27,28 +30,20 @@ class TileRangeCalculator { }) { return DiscreteTileRange.fromPixelBounds( zoom: tileZoom, - tileSize: tileSize, - pixelBounds: _calculatePixelBounds( - camera, - center ?? camera.center, - viewingZoom ?? camera.zoom, - tileZoom, - ), + tileDimension: tileDimension, + pixelBounds: _calculatePixelBounds(camera, center ?? camera.center, viewingZoom ?? camera.zoom, tileZoom), ); } - Bounds _calculatePixelBounds( - MapCamera camera, - LatLng center, - double viewingZoom, - int tileZoom, - ) { + Rect _calculatePixelBounds(MapCamera camera, LatLng center, double viewingZoom, int tileZoom) { final tileZoomDouble = tileZoom.toDouble(); final scale = camera.getZoomScale(viewingZoom, tileZoomDouble); - final pixelCenter = - camera.project(center, tileZoomDouble).floor().toDoublePoint(); + final pixelCenter = camera.projectAtZoom(center, tileZoomDouble).floor(); final halfSize = camera.size / (scale * 2); - return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); + return Rect.fromPoints( + pixelCenter - halfSize.bottomRight(Offset.zero), + pixelCenter + halfSize.bottomRight(Offset.zero), + ); } } diff --git a/lib/src/utils/flutter_map/tile_scale_calculator.dart b/lib/src/utils/flutter_map/tile_scale_calculator.dart index 01df1d3..93c142b 100644 --- a/lib/src/utils/flutter_map/tile_scale_calculator.dart +++ b/lib/src/utils/flutter_map/tile_scale_calculator.dart @@ -6,43 +6,37 @@ class TileScaleCalculator { final Crs crs; /// The size in pixel of tiles. - final double tileSize; + final int tileDimension; double? _cachedCurrentZoom; final Map _cache = {}; /// Create a new [TileScaleCalculator] instance. - TileScaleCalculator({ - required this.crs, - required this.tileSize, - }); + TileScaleCalculator({required this.crs, required this.tileDimension}); - /// Returns true to indicate that the TileSizeCache should get replaced. - bool shouldReplace(Crs crs, double tileSize) => - this.crs != crs || this.tileSize != tileSize; + /// Returns true to indicate that the TileDimensionCache should get replaced. + bool shouldReplace(Crs crs, int tileDimension) => this.crs != crs || this.tileDimension != tileDimension; /// Clears the cache if the zoom level does not match the current cached one - /// and sets [currentZoom] as the new zoom to cache for. Must be called - /// before calling scaledTileSize with a [currentZoom] different than the - /// last time scaledTileSize was called. + /// and sets [currentZoom] as the new zoom to cache for. + /// + /// Must be called before calling [scaledTileDimension] with a [currentZoom] + /// different than the last time [scaledTileDimension] was called. void clearCacheUnlessZoomMatches(double currentZoom) { if (_cachedCurrentZoom != currentZoom) _cache.clear(); _cachedCurrentZoom = currentZoom; } /// Returns a scale value to transform a Tile coordinate to a Tile position. - double scaledTileSize(double currentZoom, int tileZoom) { + double scaledTileDimension(double currentZoom, int tileZoom) { assert( _cachedCurrentZoom == currentZoom, 'The cachedCurrentZoom value and the provided currentZoom need to be equal', ); - return _cache.putIfAbsent( - tileZoom, - () => _scaledTileSizeImpl(currentZoom, tileZoom), - ); + return _cache.putIfAbsent(tileZoom, () => _scaledTileDimensionImpl(currentZoom, tileZoom)); } - double _scaledTileSizeImpl(double currentZoom, int tileZoom) { - return tileSize * (crs.scale(currentZoom) / crs.scale(tileZoom.toDouble())); + double _scaledTileDimensionImpl(double currentZoom, int tileZoom) { + return tileDimension * (crs.scale(currentZoom) / crs.scale(tileZoom.toDouble())); } } diff --git a/lib/src/utils/tessellator.dart b/lib/src/utils/tessellator.dart index bf424ea..d61fd2f 100644 --- a/lib/src/utils/tessellator.dart +++ b/lib/src/utils/tessellator.dart @@ -1,10 +1,10 @@ import 'dart:ffi'; -import 'dart:ui' as ui; +import 'dart:ui'; import 'package:ffi/ffi.dart'; import 'package:flutter/scheduler.dart'; import 'package:dart_earcut/dart_earcut.dart' as earcut; -import 'package:flutter_map/flutter_map.dart'; + import 'package:gpu_vector_tile_renderer/_vector_tile.dart' as vt; import 'package:gpu_vector_tile_renderer/src/ffi/bindings.gen.dart'; import 'package:gpu_vector_tile_renderer/src/ffi/earcut.dart'; @@ -24,17 +24,22 @@ class Tessellator { } static List tessellatePolygon(vt.Polygon polygon) { - final vertices = []; + final vertices = List.of(polygon.exterior.points); final holeIndices = []; - vertices.addAll(polygon.exterior.points); - for (final interiorRing in polygon.interiors) { holeIndices.add(vertices.length); vertices.addAll(interiorRing.points); } - return earcut.Earcut.triangulateFromPoints(vertices.map((v) => v.toPoint()), holeIndices: holeIndices); + return earcut.Earcut.triangulateRaw( + List.generate( + vertices.length * 2, + (i) => i.isEven ? vertices.elementAt(i ~/ 2).dx : vertices.elementAt(i ~/ 2).dy, + growable: false, + ), + holeIndices: holeIndices, + ); } static Future> tessellatePolygonAsync(vt.Polygon polygon) async { diff --git a/lib/src/widgets/flutter_gpu_vector_tile_layer.dart b/lib/src/widgets/flutter_gpu_vector_tile_layer.dart index 8428d9d..907e711 100644 --- a/lib/src/widgets/flutter_gpu_vector_tile_layer.dart +++ b/lib/src/widgets/flutter_gpu_vector_tile_layer.dart @@ -9,7 +9,7 @@ class FlutterGpuVectorTileLayer extends StatefulWidget { const FlutterGpuVectorTileLayer({ super.key, required this.styleProvider, - this.tileSize = 256.0, + this.tileDimension = 256, required this.shaderLibrary, required this.createSingleTileLayerRenderer, this.enableRender = true, @@ -19,7 +19,7 @@ class FlutterGpuVectorTileLayer extends StatefulWidget { final StyleProviderFn styleProvider; final ShaderLibrary shaderLibrary; final CreateSingleTileLayerRendererFn createSingleTileLayerRenderer; - final double tileSize; + final int tileDimension; final bool enableRender; // Temporary! final bool debug; // Temporary! @@ -64,8 +64,8 @@ class FlutterGpuVectorTileLayerState extends State wi super.didChangeDependencies(); final camera = MapCamera.of(context); - _controller.onCameraChanged(camera, widget.tileSize); - _orchestrator.onCameraChanged(camera, widget.tileSize); + _controller.onCameraChanged(camera, widget.tileDimension); + _orchestrator.onCameraChanged(camera, widget.tileDimension); } @override @@ -82,12 +82,14 @@ class FlutterGpuVectorTileLayerState extends State wi painter: RenderOrchestratorPainter( camera: camera, pixelRatio: pixelRatio, - tileSize: widget.tileSize, + tileDimension: widget.tileDimension, orchestrator: _orchestrator, ), ), if (widget.debug) - CustomPaint(painter: MapDebugPainter(camera: camera, controller: _controller, tileSize: widget.tileSize)), + CustomPaint( + painter: MapDebugPainter(camera: camera, controller: _controller, tileDimension: widget.tileDimension), + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 5a9a258..eb05fa4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -199,10 +199,10 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da" + sha256: "82786b8e1ffbff079487eeeed59a34e8a0b09896dd7713d8e1dc193d673496b5" url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "8.0.0" frontend_server_client: dependency: transitive description: @@ -324,7 +324,7 @@ packages: source: hosted version: "0.11.1" meta: - dependency: transitive + dependency: "direct main" description: name: meta sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c @@ -650,4 +650,4 @@ packages: version: "2.2.2" sdks: dart: ">=3.8.0-91.0.dev <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index a624722..5295179 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: gpu_vector_tile_renderer -description: 'A MapLibre style spec (v8) compatible vector tile renderer for Flutter, using flutter_gpu' +description: "A MapLibre style spec (v8) compatible vector tile renderer for Flutter, using flutter_gpu" version: 0.0.1 environment: @@ -21,9 +21,10 @@ dependencies: intl: ^0.20.2 vector_math: ^2.1.4 logging: ^1.2.0 - + meta: ^1.16.0 + # Map - flutter_map: ^7.0.2 + flutter_map: ^8.0.0 latlong2: ^0.9.1 dart_earcut: ^1.2.0 @@ -52,4 +53,4 @@ flutter: plugin: platforms: android: - ffiPlugin: true \ No newline at end of file + ffiPlugin: true