diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 1489e49a1..98d50b3e4 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -328,6 +328,35 @@ class _PolygonPageState extends State { simplificationTolerance: 0, useAltRendering: true, polygons: [ + Polygon( + points: const [ + LatLng(40, 150), + LatLng(45, 160), + LatLng(50, 170), + LatLng(55, 180), + LatLng(50, -170), + LatLng(45, -160), + LatLng(40, -150), + LatLng(35, -160), + LatLng(30, -170), + LatLng(25, -180), + LatLng(30, 170), + LatLng(35, 160), + ], + holePointsList: const [ + [ + LatLng(45, 175), + LatLng(45, -175), + LatLng(35, -175), + LatLng(35, 175), + ], + ], + color: const Color(0xFFFF0000), + hitValue: ( + title: 'Red Line', + subtitle: 'Across the universe...', + ), + ), Polygon( points: const [ LatLng(50, -18), diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 60ef904a8..499481017 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -22,6 +22,23 @@ class _PolylinePageState extends State { List>? _hoverLines; final _polylinesRaw = >[ + Polyline( + points: const [ + LatLng(40, 150), + LatLng(45, 160), + LatLng(50, 170), + LatLng(55, 180), + LatLng(50, -170), + LatLng(45, -160), + LatLng(40, -150), + ], + strokeWidth: 8, + color: const Color(0xFFFF0000), + hitValue: ( + title: 'Red Line', + subtitle: 'Across the universe...', + ), + ), Polyline( points: [ const LatLng(51.5, -0.09), diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index 446ab371e..41eda28f9 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -2,6 +2,7 @@ import 'dart:math' as math hide Point; import 'dart:math' show Point; import 'package:flutter_map/src/misc/bounds.dart'; +import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; import 'package:proj4dart/proj4dart.dart' as proj4; @@ -394,6 +395,52 @@ abstract class Projection { /// unproject cartesian x,y coordinates to [LatLng]. LatLng unprojectXY(double x, double y); + + /// Returns the width of the world in geometry coordinates. + /// + /// Is used at least in 2 cases: + /// * my polyline crosses longitude 180, and I somehow need to "add a world" + /// to the coordinates in order to display a continuous polyline + /// * when my map scrolls around longitude 180 and I have a marker in this + /// area, the marker may be projected a world away, depending on the map being + /// centered either in the 179 or the -179 part - again, we can "add a world" + double getWorldWidth() { + final (x0, _) = projectXY(const LatLng(0, 0)); + final (x180, _) = projectXY(const LatLng(0, 180)); + return 2 * (x0 > x180 ? x0 - x180 : x180 - x0); + } + + /// Projects a list of [LatLng]s into geometry coordinates. + /// + /// All resulting points gather somehow around the first point, or the + /// optional [referencePoint] if provided. + /// The typical use-case is when you display the whole world: you don't want + /// longitudes -179 and 179 to be projected each on one side. + /// [referencePoint] is used for polygon holes: we want the holes to be + /// displayed close to the polygon, not on the other side of the world. + List projectList(List points, {LatLng? referencePoint}) { + late double previousX; + final worldWidth = getWorldWidth(); + return List.generate( + points.length, + (j) { + if (j == 0 && referencePoint != null) { + (previousX, _) = projectXY(referencePoint); + } + var (x, y) = projectXY(points[j]); + if (j > 0 || referencePoint != null) { + if (x - previousX > worldWidth / 2) { + x -= worldWidth; + } else if (x - previousX < -worldWidth / 2) { + x += worldWidth; + } + } + previousX = x; + return DoublePoint(x, y); + }, + growable: false, + ); + } } class _LonLat extends Projection { diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 44aff4b8b..91ed21a74 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -286,13 +286,12 @@ base class _PolygonPainter // and the normal points are the same filledPath.fillType = PathFillType.evenOdd; - final holeOffsetsList = List>.generate( - holePointsList.length, - (i) => getOffsets(camera, origin, holePointsList[i]), - growable: false, - ); - - for (final holeOffsets in holeOffsetsList) { + for (final singleHolePoints in projectedPolygon.holePoints) { + final holeOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: singleHolePoints, + ); filledPath.addPolygon(holeOffsets, true); // TODO: Potentially more efficient and may change the need to do @@ -307,15 +306,23 @@ base class _PolygonPainter } if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { - _addHoleBordersToPath( - borderPath, - polygon, - holeOffsetsList, - size, - canvas, - _getBorderPaint(polygon), - polygon.borderStrokeWidth, - ); + final borderPaint = _getBorderPaint(polygon); + for (final singleHolePoints in projectedPolygon.holePoints) { + final holeOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: singleHolePoints, + ); + _addBorderToPath( + borderPath, + polygon, + holeOffsets, + size, + canvas, + borderPaint, + polygon.borderStrokeWidth, + ); + } } } @@ -434,28 +441,6 @@ base class _PolygonPainter } } - void _addHoleBordersToPath( - Path path, - Polygon polygon, - List> holeOffsetsList, - Size canvasSize, - Canvas canvas, - Paint paint, - double strokeWidth, - ) { - for (final offsets in holeOffsetsList) { - _addBorderToPath( - path, - polygon, - offsets, - canvasSize, - canvas, - paint, - strokeWidth, - ); - } - } - ({Offset min, Offset max}) _getBounds(Offset origin, Polygon polygon) { final bBox = polygon.boundingBox; return ( diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index 1c25ef0f5..af18c74be 100644 --- a/lib/src/layer/polygon_layer/projected_polygon.dart +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -18,35 +18,22 @@ class _ProjectedPolygon with HitDetectableElement { _ProjectedPolygon._fromPolygon(Projection projection, Polygon polygon) : this._( polygon: polygon, - points: List.generate( - polygon.points.length, - (j) { - final (x, y) = projection.projectXY(polygon.points[j]); - return DoublePoint(x, y); - }, - growable: false, - ), + points: projection.projectList(polygon.points), holePoints: () { final holes = polygon.holePointsList; if (holes == null || holes.isEmpty || + polygon.points.isEmpty || holes.every((e) => e.isEmpty)) { return >[]; } return List>.generate( holes.length, - (j) { - final points = holes[j]; - return List.generate( - points.length, - (k) { - final (x, y) = projection.projectXY(points[k]); - return DoublePoint(x, y); - }, - growable: false, - ); - }, + (j) => projection.projectList( + holes[j], + referencePoint: polygon.points[0], + ), growable: false, ); }(), diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index 1f60aee39..1d85b57df 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -274,7 +274,13 @@ base class _PolylinePainter double strokeWidthInMeters, ) { final r = _distance.offset(p0, strokeWidthInMeters, 180); - final delta = o0 - getOffset(camera, origin, r); + var delta = o0 - getOffset(camera, origin, r); + final worldSize = camera.crs.scale(camera.zoom); + if (delta.dx < 0) { + delta = delta.translate(worldSize, 0); + } else if (delta.dx >= worldSize) { + delta = delta.translate(-worldSize, 0); + } return delta.distance; } diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index 47da085ff..0aae00f7f 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -140,6 +140,8 @@ class _PolylineLayerState extends State> projection.project(boundsAdjusted.northEast), ); + final (xWest, _) = projection.projectXY(const LatLng(0, -180)); + final (xEast, _) = projection.projectXY(const LatLng(0, 180)); for (final projectedPolyline in polylines) { final polyline = projectedPolyline.polyline; @@ -149,6 +151,22 @@ class _PolylineLayerState extends State> continue; } + /// Returns true if the points stretch on different versions of the world. + bool stretchesBeyondTheLimits() { + for (final point in projectedPolyline.points) { + if (point.x > xEast || point.x < xWest) { + return true; + } + } + return false; + } + + // TODO: think about how to cull polylines that go beyond -180/180. + if (stretchesBeyondTheLimits()) { + yield projectedPolyline; + continue; + } + // pointer that indicates the start of the visible polyline segment int start = -1; bool containsSegment = false; diff --git a/lib/src/layer/polyline_layer/projected_polyline.dart b/lib/src/layer/polyline_layer/projected_polyline.dart index eca98465a..9ba15e4f5 100644 --- a/lib/src/layer/polyline_layer/projected_polyline.dart +++ b/lib/src/layer/polyline_layer/projected_polyline.dart @@ -16,13 +16,6 @@ class _ProjectedPolyline with HitDetectableElement { _ProjectedPolyline._fromPolyline(Projection projection, Polyline polyline) : this._( polyline: polyline, - points: List.generate( - polyline.points.length, - (j) { - final (x, y) = projection.projectXY(polyline.points[j]); - return DoublePoint(x, y); - }, - growable: false, - ), + points: projection.projectList(polyline.points), ); } diff --git a/lib/src/misc/offsets.dart b/lib/src/misc/offsets.dart index c22c216de..0e6aa1771 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -60,13 +60,44 @@ List getOffsetsXY({ final oy = -origin.dy; final len = realPoints.length; + /// Returns additional world width in order to have visible points. + double getAddedWorldWidth() { + final worldWidth = crs.projection.getWorldWidth(); + final List addedWidths = [ + 0, + worldWidth, + -worldWidth, + ]; + final halfScreenWidth = camera.size.x / 2; + final p = realPoints.elementAt(0); + late double result; + late double bestX; + for (int i = 0; i < addedWidths.length; i++) { + final addedWidth = addedWidths[i]; + final (x, _) = crs.transform(p.x + addedWidth, p.y, zoomScale); + if (i == 0) { + result = addedWidth; + bestX = x; + continue; + } + if ((bestX + ox - halfScreenWidth).abs() > + (x + ox - halfScreenWidth).abs()) { + result = addedWidth; + bestX = x; + } + } + return result; + } + + final double addedWorldWidth = getAddedWorldWidth(); + // Optimization: monomorphize the CrsWithStaticTransformation-case to avoid // the virtual function overhead. if (crs case final CrsWithStaticTransformation crs) { final v = List.filled(len, Offset.zero, growable: true); for (int i = 0; i < len; ++i) { final p = realPoints.elementAt(i); - final (x, y) = crs.transform(p.x, p.y, zoomScale); + final (x, y) = crs.transform(p.x + addedWorldWidth, p.y, zoomScale); v[i] = Offset(x + ox, y + oy); } return v; @@ -75,7 +106,7 @@ List getOffsetsXY({ final v = List.filled(len, Offset.zero, growable: true); for (int i = 0; i < len; ++i) { final p = realPoints.elementAt(i); - final (x, y) = crs.transform(p.x, p.y, zoomScale); + final (x, y) = crs.transform(p.x + addedWorldWidth, p.y, zoomScale); v[i] = Offset(x + ox, y + oy); } return v;