diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index 1abdd3f3e..5ec3d7277 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -24,13 +24,8 @@ abstract class Crs { /// Converts a point on the sphere surface (with a certain zoom) in a /// map point. Point latLngToPoint(LatLng latlng, double zoom) { - try { - final projectedPoint = projection.project(latlng); - final scale = this.scale(zoom); - return transformation.transform(projectedPoint, scale); - } catch (_) { - return const Point(0, 0); - } + final projectedPoint = projection.project(latlng); + return transformation.transform(projectedPoint, scale(zoom)); } /// Converts a map point to the sphere coordinate (at a certain zoom). @@ -114,7 +109,7 @@ class Epsg3857 extends Earth { @override final Transformation transformation; - static const num _scale = 0.5 / (math.pi * SphericalMercator.r); + static const double _scale = 0.5 / (math.pi * SphericalMercator.r); const Epsg3857() : projection = const SphericalMercator(), @@ -183,7 +178,7 @@ class Proj4Crs extends Crs { required String code, required proj4.Projection proj4Projection, Transformation? transformation, - List? origins, + List>? origins, Bounds? bounds, List? scales, List? resolutions, @@ -230,15 +225,11 @@ class Proj4Crs extends Crs { /// map point. @override Point latLngToPoint(LatLng latlng, double zoom) { - try { - final projectedPoint = projection.project(latlng); - final scale = this.scale(zoom); - final transformation = _getTransformationByZoom(zoom); - - return transformation.transform(projectedPoint, scale); - } catch (_) { - return const Point(0, 0); - } + final projectedPoint = projection.project(latlng); + final scale = this.scale(zoom); + final transformation = _getTransformationByZoom(zoom); + + return transformation.transform(projectedPoint, scale); } /// Converts a map point to the sphere coordinate (at a certain zoom). @@ -313,14 +304,15 @@ class Proj4Crs extends Crs { /// returns Transformation object based on zoom Transformation _getTransformationByZoom(double zoom) { - if (null == _transformations) { + final transformations = _transformations; + if (transformations == null || transformations.isEmpty) { return transformation; } final iZoom = zoom.round(); - final lastIdx = _transformations!.length - 1; + final lastIdx = transformations.length - 1; - return _transformations![iZoom > lastIdx ? lastIdx : iZoom]; + return transformations[iZoom > lastIdx ? lastIdx : iZoom]; } } @@ -369,7 +361,7 @@ class _LonLat extends Projection { @override LatLng unproject(Point point) { return LatLng( - inclusiveLat(point.y as double), inclusiveLng(point.x as double)); + inclusiveLat(point.y.toDouble()), inclusiveLng(point.x.toDouble())); } } @@ -391,12 +383,13 @@ class SphericalMercator extends Projection { @override Point project(LatLng latlng) { const d = math.pi / 180; - const max = maxLatitude; - final lat = math.max(math.min(max, latlng.latitude), -max); + final lat = latlng.latitude.clamp(-maxLatitude, maxLatitude); final sin = math.sin(lat * d); return Point( - r * latlng.longitude * d, r * math.log((1 + sin) / (1 - sin)) / 2); + r * d * latlng.longitude, + r / 2 * math.log((1 + sin) / (1 - sin)), + ); } @override @@ -434,7 +427,7 @@ class _Proj4Projection extends Projection { @override LatLng unproject(Point point) { final point2 = proj4Projection.transform( - epsg4326, proj4.Point(x: point.x as double, y: point.y as double)); + epsg4326, proj4.Point(x: point.x.toDouble(), y: point.y.toDouble())); return LatLng(inclusiveLat(point2.y), inclusiveLng(point2.x)); } @@ -442,10 +435,10 @@ class _Proj4Projection extends Projection { @immutable class Transformation { - final num a; - final num b; - final num c; - final num d; + final double a; + final double b; + final double c; + final double d; const Transformation(this.a, this.b, this.c, this.d); diff --git a/lib/src/layer/polygon_layer/label.dart b/lib/src/layer/polygon_layer/label.dart index 3f3684470..6cd8d139c 100644 --- a/lib/src/layer/polygon_layer/label.dart +++ b/lib/src/layer/polygon_layer/label.dart @@ -6,42 +6,60 @@ import 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; import 'package:latlong2/latlong.dart'; import 'package:polylabel/polylabel.dart'; +class PolygonBounds { + final Offset min; + final Offset max; + + const PolygonBounds(this.min, this.max); +} + void Function(Canvas canvas)? buildLabelTextPainter({ + required math.Point mapSize, required Offset placementPoint, - required List points, + required PolygonBounds bounds, required String labelText, required double rotationRad, required bool rotate, required TextStyle labelStyle, required double padding, }) { - final textSpan = TextSpan(text: labelText, style: labelStyle); final textPainter = TextPainter( - text: textSpan, + text: TextSpan(text: labelText, style: labelStyle), textAlign: TextAlign.center, textDirection: TextDirection.ltr, )..layout(); - final dx = placementPoint.dx - textPainter.width / 2; - final dy = placementPoint.dy - textPainter.height / 2; + final dx = placementPoint.dx; + final dy = placementPoint.dy; + final width = textPainter.width; + final height = textPainter.height; - double maxDx = 0; - var minDx = double.infinity; - for (final point in points) { - maxDx = math.max(maxDx, point.dx); - minDx = math.min(minDx, point.dx); + // Cull labels where the polygon is still on the map but the label wouldn't be. + if (dx + width / 2 < 0 || dx - width / 2 > mapSize.x) { + return null; + } + if (dy + height / 2 < 0 || dy - height / 2 > mapSize.x) { + return null; } - if (maxDx - minDx - padding > textPainter.width) { + // Note: I'm pretty sure this doesn't work for concave shapes. + if (bounds.max.dx - bounds.min.dx - padding > width) { return (canvas) { if (rotate) { canvas.save(); - canvas.translate(placementPoint.dx, placementPoint.dy); + canvas.translate(dx, dy); canvas.rotate(-rotationRad); - canvas.translate(-placementPoint.dx, -placementPoint.dy); + canvas.translate(-dx, -dy); } - textPainter.paint(canvas, Offset(dx, dy)); + textPainter.paint( + canvas, + Offset( + dx - width / 2, + dy - height / 2, + ), + ); + if (rotate) { canvas.restore(); } diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 7b1382808..fb3f083e8 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -5,6 +5,8 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/polygon_layer/label.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/misc/bounds.dart'; +import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI enum PolygonLabelPlacement { @@ -148,16 +150,27 @@ class PolygonPainter extends CustomPainter { int? _hash; - List getOffsets(List points) { + Offset getOffset(Offset origin, LatLng point) { + // Critically create as little garbage as possible. This is called on every frame. + final projected = map.project(point); + return Offset(projected.x - origin.dx, projected.y - origin.dy); + } + + List getOffsets(Offset origin, List points) { return List.generate( points.length, - (index) { - return map.getOffsetFromOrigin(points[index]); - }, + (index) => getOffset(origin, points[index]), growable: false, ); } + PolygonBounds getBounds(Offset origin, Polygon polygon) { + final bbox = polygon.boundingBox; + final min = getOffset(origin, bbox.southWest); + final max = getOffset(origin, bbox.northEast); + return PolygonBounds(min, max); + } + @override void paint(Canvas canvas, Size size) { var filledPath = ui.Path(); @@ -193,12 +206,14 @@ class PolygonPainter extends CustomPainter { lastHash = null; } + final origin = (map.project(map.center) - map.size / 2).toOffset(); + // Main loop constructing batched fill and border paths from given polygons. for (final polygon in polygons) { if (polygon.points.isEmpty) { continue; } - final offsets = getOffsets(polygon.points); + final offsets = getOffsets(origin, polygon.points); // The hash is based on the polygons visual properties. If the hash from // the current and the previous polygon no longer match, we need to flush @@ -228,7 +243,7 @@ class PolygonPainter extends CustomPainter { final holeOffsetsList = List>.generate( holePointsList.length, - (i) => getOffsets(holePointsList[i]), + (i) => getOffsets(origin, holePointsList[i]), growable: false, ); @@ -252,8 +267,9 @@ class PolygonPainter extends CustomPainter { // The painter will be null if the layouting algorithm determined that // there isn't enough space. final painter = buildLabelTextPainter( - placementPoint: map.getOffsetFromOrigin(polygon.labelPosition), - points: offsets, + mapSize: map.size, + placementPoint: getOffset(origin, polygon.labelPosition), + bounds: getBounds(origin, polygon), labelText: polygon.label!, labelStyle: polygon.labelStyle, rotationRad: map.rotationRad, @@ -277,12 +293,13 @@ class PolygonPainter extends CustomPainter { if (polygon.points.isEmpty) { continue; } - final offsets = getOffsets(polygon.points); if (polygon.label != null) { final painter = buildLabelTextPainter( - placementPoint: map.getOffsetFromOrigin(polygon.labelPosition), - points: offsets, + mapSize: map.size, + placementPoint: + map.project(polygon.labelPosition).toOffset() - origin, + bounds: getBounds(origin, polygon), labelText: polygon.label!, labelStyle: polygon.labelStyle, rotationRad: map.rotationRad, diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index cc245a647..b20de99f1 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:latlong2/latlong.dart'; class Polyline { @@ -97,13 +98,19 @@ class PolylinePainter extends CustomPainter { int? _hash; - List getOffsets(List points) { - return List.generate(points.length, (index) { - return getOffset(points[index]); - }, growable: false); + Offset getOffset(Offset origin, LatLng point) { + // Critically create as little garbage as possible. This is called on every frame. + final projected = map.project(point); + return Offset(projected.x - origin.dx, projected.y - origin.dy); } - Offset getOffset(LatLng point) => map.getOffsetFromOrigin(point); + List getOffsets(Offset origin, List points) { + return List.generate( + points.length, + (index) => getOffset(origin, points[index]), + growable: false, + ); + } @override void paint(Canvas canvas, Size size) { @@ -144,8 +151,10 @@ class PolylinePainter extends CustomPainter { paint = Paint(); } + final origin = map.project(map.center).toOffset() - map.size.toOffset() / 2; + for (final polyline in polylines) { - final offsets = getOffsets(polyline.points); + final offsets = getOffsets(origin, polyline.points); if (offsets.isEmpty) { continue; } @@ -167,7 +176,7 @@ class PolylinePainter extends CustomPainter { polyline.strokeWidth, 180, ); - final delta = firstOffset - getOffset(r); + final delta = firstOffset - getOffset(origin, r); strokeWidth = delta.distance; } else { diff --git a/lib/src/map/camera/camera.dart b/lib/src/map/camera/camera.dart index 2875c2db1..6cdc2cbcb 100644 --- a/lib/src/map/camera/camera.dart +++ b/lib/src/map/camera/camera.dart @@ -58,7 +58,7 @@ class MapCamera { LatLngBounds? _bounds; /// Lazily calculated field - Point? _pixelOrigin; + Point? _pixelOrigin; /// Lazily calculated field double? _rotationRad; @@ -89,8 +89,8 @@ class MapCamera { /// The offset of the top-left corner of the bounding rectangle of this /// camera. This will not equal the offset of the top-left visible pixel when /// the map is rotated. - Point get pixelOrigin => - _pixelOrigin ??= (project(center, zoom) - size / 2.0).round(); + Point get pixelOrigin => + _pixelOrigin ??= project(center, zoom) - size / 2.0; /// The camera of the closest [FlutterMap] ancestor. If this is called from a /// context with no [FlutterMap] ancestor null, is returned. @@ -118,7 +118,7 @@ class MapCamera { Point? size, Bounds? pixelBounds, LatLngBounds? bounds, - Point? pixelOrigin, + Point? pixelOrigin, double? rotationRad, }) : _cameraSize = size, _pixelBounds = pixelBounds, @@ -254,7 +254,7 @@ class MapCamera { /// Calculates the [Offset] from the [pos] to this camera's [pixelOrigin]. Offset getOffsetFromOrigin(LatLng pos) => - project(pos).subtract(pixelOrigin).toOffset(); + (project(pos) - pixelOrigin).toOffset(); /// Calculates the pixel origin of this [MapCamera] at the given /// [center]/[zoom].