Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow polylines & polygons to cross world boundary #1969

Merged
merged 16 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions example/lib/pages/polygon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,35 @@ class _PolygonPageState extends State<PolygonPage> {
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),
Expand Down
17 changes: 17 additions & 0 deletions example/lib/pages/polyline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ class _PolylinePageState extends State<PolylinePage> {
List<Polyline<HitValue>>? _hoverLines;

final _polylinesRaw = <Polyline<HitValue>>[
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),
Expand Down
1 change: 1 addition & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
proj4dart: ^2.1.0
shared_preferences: ^2.3.2
url_launcher: ^6.3.0
url_launcher_android: 6.3.2
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved
url_strategy: ^0.3.0
vector_math: ^2.1.4

Expand Down
47 changes: 47 additions & 0 deletions lib/src/geo/crs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
mootw marked this conversation as resolved.
Show resolved Hide resolved
import 'package:latlong2/latlong.dart';
import 'package:meta/meta.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;
Expand Down Expand Up @@ -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<DoublePoint> projectList(List<LatLng> points, {LatLng? referencePoint}) {
late double previousX;
final worldWidth = getWorldWidth();
return List<DoublePoint>.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 {
Expand Down
61 changes: 23 additions & 38 deletions lib/src/layer/polygon_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,12 @@ base class _PolygonPainter<R extends Object>
// and the normal points are the same
filledPath.fillType = PathFillType.evenOdd;

final holeOffsetsList = List<List<Offset>>.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
Expand All @@ -307,15 +306,23 @@ base class _PolygonPainter<R extends Object>
}

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,
);
}
}
}

Expand Down Expand Up @@ -434,28 +441,6 @@ base class _PolygonPainter<R extends Object>
}
}

void _addHoleBordersToPath(
Path path,
Polygon polygon,
List<List<Offset>> 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 (
Expand Down
25 changes: 6 additions & 19 deletions lib/src/layer/polygon_layer/projected_polygon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,22 @@ class _ProjectedPolygon<R extends Object> with HitDetectableElement<R> {
_ProjectedPolygon._fromPolygon(Projection projection, Polygon<R> polygon)
: this._(
polygon: polygon,
points: List<DoublePoint>.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 <List<DoublePoint>>[];
}

return List<List<DoublePoint>>.generate(
holes.length,
(j) {
final points = holes[j];
return List<DoublePoint>.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,
);
}(),
Expand Down
8 changes: 7 additions & 1 deletion lib/src/layer/polyline_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,13 @@ base class _PolylinePainter<R extends Object>
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;
}

Expand Down
18 changes: 18 additions & 0 deletions lib/src/layer/polyline_layer/polyline_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ class _PolylineLayerState<R extends Object> extends State<PolylineLayer<R>>
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;

Expand All @@ -149,6 +151,22 @@ class _PolylineLayerState<R extends Object> extends State<PolylineLayer<R>>
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;
Expand Down
9 changes: 1 addition & 8 deletions lib/src/layer/polyline_layer/projected_polyline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@ class _ProjectedPolyline<R extends Object> with HitDetectableElement<R> {
_ProjectedPolyline._fromPolyline(Projection projection, Polyline<R> polyline)
: this._(
polyline: polyline,
points: List<DoublePoint>.generate(
polyline.points.length,
(j) {
final (x, y) = projection.projectXY(polyline.points[j]);
return DoublePoint(x, y);
},
growable: false,
),
points: projection.projectList(polyline.points),
);
}
35 changes: 33 additions & 2 deletions lib/src/misc/offsets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,44 @@ List<Offset> 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<double> 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<Offset>.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;
Expand All @@ -75,7 +106,7 @@ List<Offset> getOffsetsXY({
final v = List<Offset>.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;
Expand Down
Loading