Skip to content

Commit b81d6db

Browse files
feat: allow polylines & polygons to cross world boundary (#1969)
* fix: 1338 - longitude +-180 with correct polylines and polygons Impacted files * `crs.dart`: new methods `getHalfWorldWidth` and `projectList` * `painter.dart`: refactored using pre-computed `List<Double>` * `polygon.dart`: added an example around longitude 180 * `polyline.dart`: added an example around longitude 180 * `polyline_layer.dart`: we don't cull polylines that go beyond longitude 180 * `projected_polygon.dart`: using new method `Projection.projectList` * `projected_polyline.dart`: using new method `Projection.projectList` * Typo fix. * fix: always display at least one instance of the polyline/polygon Impacted files: * `offsets.dart`: new method `getAddedWorldWidth`, used to add/subtract a world width in order to display visible polylines * `painter.dart`: minor fix, as now we may unproject coordinates from the wrong world * refactoring Impacted files: * `crs.dart`: replaced "half world width" with "world width", in order to avoid answering to the question "why HALF?" * `offsets.dart`: now we display the occurrence closer to the screen center; minor refactoring * `painter.dart`: minor fix regarding side-effects on `_metersToStrokeWidth` * `polyline_layer.dart`: now computes the limits projected from -180 and 180 instead of "half world width" * `projected_polyline.dart`: moved code to `polyline_layer.dart` * "example" build fix * "example" build fix, just trying * "example" build fix, just trying * minor fix
1 parent aca8aed commit b81d6db

File tree

9 files changed

+181
-68
lines changed

9 files changed

+181
-68
lines changed

example/lib/pages/polygon.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,35 @@ class _PolygonPageState extends State<PolygonPage> {
328328
simplificationTolerance: 0,
329329
useAltRendering: true,
330330
polygons: [
331+
Polygon(
332+
points: const [
333+
LatLng(40, 150),
334+
LatLng(45, 160),
335+
LatLng(50, 170),
336+
LatLng(55, 180),
337+
LatLng(50, -170),
338+
LatLng(45, -160),
339+
LatLng(40, -150),
340+
LatLng(35, -160),
341+
LatLng(30, -170),
342+
LatLng(25, -180),
343+
LatLng(30, 170),
344+
LatLng(35, 160),
345+
],
346+
holePointsList: const [
347+
[
348+
LatLng(45, 175),
349+
LatLng(45, -175),
350+
LatLng(35, -175),
351+
LatLng(35, 175),
352+
],
353+
],
354+
color: const Color(0xFFFF0000),
355+
hitValue: (
356+
title: 'Red Line',
357+
subtitle: 'Across the universe...',
358+
),
359+
),
331360
Polygon(
332361
points: const [
333362
LatLng(50, -18),

example/lib/pages/polyline.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ class _PolylinePageState extends State<PolylinePage> {
2222
List<Polyline<HitValue>>? _hoverLines;
2323

2424
final _polylinesRaw = <Polyline<HitValue>>[
25+
Polyline(
26+
points: const [
27+
LatLng(40, 150),
28+
LatLng(45, 160),
29+
LatLng(50, 170),
30+
LatLng(55, 180),
31+
LatLng(50, -170),
32+
LatLng(45, -160),
33+
LatLng(40, -150),
34+
],
35+
strokeWidth: 8,
36+
color: const Color(0xFFFF0000),
37+
hitValue: (
38+
title: 'Red Line',
39+
subtitle: 'Across the universe...',
40+
),
41+
),
2542
Polyline(
2643
points: [
2744
const LatLng(51.5, -0.09),

lib/src/geo/crs.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:math' as math hide Point;
22
import 'dart:math' show Point;
33

44
import 'package:flutter_map/src/misc/bounds.dart';
5+
import 'package:flutter_map/src/misc/simplify.dart';
56
import 'package:latlong2/latlong.dart';
67
import 'package:meta/meta.dart';
78
import 'package:proj4dart/proj4dart.dart' as proj4;
@@ -394,6 +395,52 @@ abstract class Projection {
394395

395396
/// unproject cartesian x,y coordinates to [LatLng].
396397
LatLng unprojectXY(double x, double y);
398+
399+
/// Returns the width of the world in geometry coordinates.
400+
///
401+
/// Is used at least in 2 cases:
402+
/// * my polyline crosses longitude 180, and I somehow need to "add a world"
403+
/// to the coordinates in order to display a continuous polyline
404+
/// * when my map scrolls around longitude 180 and I have a marker in this
405+
/// area, the marker may be projected a world away, depending on the map being
406+
/// centered either in the 179 or the -179 part - again, we can "add a world"
407+
double getWorldWidth() {
408+
final (x0, _) = projectXY(const LatLng(0, 0));
409+
final (x180, _) = projectXY(const LatLng(0, 180));
410+
return 2 * (x0 > x180 ? x0 - x180 : x180 - x0);
411+
}
412+
413+
/// Projects a list of [LatLng]s into geometry coordinates.
414+
///
415+
/// All resulting points gather somehow around the first point, or the
416+
/// optional [referencePoint] if provided.
417+
/// The typical use-case is when you display the whole world: you don't want
418+
/// longitudes -179 and 179 to be projected each on one side.
419+
/// [referencePoint] is used for polygon holes: we want the holes to be
420+
/// displayed close to the polygon, not on the other side of the world.
421+
List<DoublePoint> projectList(List<LatLng> points, {LatLng? referencePoint}) {
422+
late double previousX;
423+
final worldWidth = getWorldWidth();
424+
return List<DoublePoint>.generate(
425+
points.length,
426+
(j) {
427+
if (j == 0 && referencePoint != null) {
428+
(previousX, _) = projectXY(referencePoint);
429+
}
430+
var (x, y) = projectXY(points[j]);
431+
if (j > 0 || referencePoint != null) {
432+
if (x - previousX > worldWidth / 2) {
433+
x -= worldWidth;
434+
} else if (x - previousX < -worldWidth / 2) {
435+
x += worldWidth;
436+
}
437+
}
438+
previousX = x;
439+
return DoublePoint(x, y);
440+
},
441+
growable: false,
442+
);
443+
}
397444
}
398445

399446
class _LonLat extends Projection {

lib/src/layer/polygon_layer/painter.dart

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,12 @@ base class _PolygonPainter<R extends Object>
286286
// and the normal points are the same
287287
filledPath.fillType = PathFillType.evenOdd;
288288

289-
final holeOffsetsList = List<List<Offset>>.generate(
290-
holePointsList.length,
291-
(i) => getOffsets(camera, origin, holePointsList[i]),
292-
growable: false,
293-
);
294-
295-
for (final holeOffsets in holeOffsetsList) {
289+
for (final singleHolePoints in projectedPolygon.holePoints) {
290+
final holeOffsets = getOffsetsXY(
291+
camera: camera,
292+
origin: origin,
293+
points: singleHolePoints,
294+
);
296295
filledPath.addPolygon(holeOffsets, true);
297296

298297
// TODO: Potentially more efficient and may change the need to do
@@ -307,15 +306,23 @@ base class _PolygonPainter<R extends Object>
307306
}
308307

309308
if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) {
310-
_addHoleBordersToPath(
311-
borderPath,
312-
polygon,
313-
holeOffsetsList,
314-
size,
315-
canvas,
316-
_getBorderPaint(polygon),
317-
polygon.borderStrokeWidth,
318-
);
309+
final borderPaint = _getBorderPaint(polygon);
310+
for (final singleHolePoints in projectedPolygon.holePoints) {
311+
final holeOffsets = getOffsetsXY(
312+
camera: camera,
313+
origin: origin,
314+
points: singleHolePoints,
315+
);
316+
_addBorderToPath(
317+
borderPath,
318+
polygon,
319+
holeOffsets,
320+
size,
321+
canvas,
322+
borderPaint,
323+
polygon.borderStrokeWidth,
324+
);
325+
}
319326
}
320327
}
321328

@@ -434,28 +441,6 @@ base class _PolygonPainter<R extends Object>
434441
}
435442
}
436443

437-
void _addHoleBordersToPath(
438-
Path path,
439-
Polygon polygon,
440-
List<List<Offset>> holeOffsetsList,
441-
Size canvasSize,
442-
Canvas canvas,
443-
Paint paint,
444-
double strokeWidth,
445-
) {
446-
for (final offsets in holeOffsetsList) {
447-
_addBorderToPath(
448-
path,
449-
polygon,
450-
offsets,
451-
canvasSize,
452-
canvas,
453-
paint,
454-
strokeWidth,
455-
);
456-
}
457-
}
458-
459444
({Offset min, Offset max}) _getBounds(Offset origin, Polygon polygon) {
460445
final bBox = polygon.boundingBox;
461446
return (

lib/src/layer/polygon_layer/projected_polygon.dart

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,22 @@ class _ProjectedPolygon<R extends Object> with HitDetectableElement<R> {
1818
_ProjectedPolygon._fromPolygon(Projection projection, Polygon<R> polygon)
1919
: this._(
2020
polygon: polygon,
21-
points: List<DoublePoint>.generate(
22-
polygon.points.length,
23-
(j) {
24-
final (x, y) = projection.projectXY(polygon.points[j]);
25-
return DoublePoint(x, y);
26-
},
27-
growable: false,
28-
),
21+
points: projection.projectList(polygon.points),
2922
holePoints: () {
3023
final holes = polygon.holePointsList;
3124
if (holes == null ||
3225
holes.isEmpty ||
26+
polygon.points.isEmpty ||
3327
holes.every((e) => e.isEmpty)) {
3428
return <List<DoublePoint>>[];
3529
}
3630

3731
return List<List<DoublePoint>>.generate(
3832
holes.length,
39-
(j) {
40-
final points = holes[j];
41-
return List<DoublePoint>.generate(
42-
points.length,
43-
(k) {
44-
final (x, y) = projection.projectXY(points[k]);
45-
return DoublePoint(x, y);
46-
},
47-
growable: false,
48-
);
49-
},
33+
(j) => projection.projectList(
34+
holes[j],
35+
referencePoint: polygon.points[0],
36+
),
5037
growable: false,
5138
);
5239
}(),

lib/src/layer/polyline_layer/painter.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,13 @@ base class _PolylinePainter<R extends Object>
274274
double strokeWidthInMeters,
275275
) {
276276
final r = _distance.offset(p0, strokeWidthInMeters, 180);
277-
final delta = o0 - getOffset(camera, origin, r);
277+
var delta = o0 - getOffset(camera, origin, r);
278+
final worldSize = camera.crs.scale(camera.zoom);
279+
if (delta.dx < 0) {
280+
delta = delta.translate(worldSize, 0);
281+
} else if (delta.dx >= worldSize) {
282+
delta = delta.translate(-worldSize, 0);
283+
}
278284
return delta.distance;
279285
}
280286

lib/src/layer/polyline_layer/polyline_layer.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ class _PolylineLayerState<R extends Object> extends State<PolylineLayer<R>>
140140
projection.project(boundsAdjusted.northEast),
141141
);
142142

143+
final (xWest, _) = projection.projectXY(const LatLng(0, -180));
144+
final (xEast, _) = projection.projectXY(const LatLng(0, 180));
143145
for (final projectedPolyline in polylines) {
144146
final polyline = projectedPolyline.polyline;
145147

@@ -149,6 +151,22 @@ class _PolylineLayerState<R extends Object> extends State<PolylineLayer<R>>
149151
continue;
150152
}
151153

154+
/// Returns true if the points stretch on different versions of the world.
155+
bool stretchesBeyondTheLimits() {
156+
for (final point in projectedPolyline.points) {
157+
if (point.x > xEast || point.x < xWest) {
158+
return true;
159+
}
160+
}
161+
return false;
162+
}
163+
164+
// TODO: think about how to cull polylines that go beyond -180/180.
165+
if (stretchesBeyondTheLimits()) {
166+
yield projectedPolyline;
167+
continue;
168+
}
169+
152170
// pointer that indicates the start of the visible polyline segment
153171
int start = -1;
154172
bool containsSegment = false;

lib/src/layer/polyline_layer/projected_polyline.dart

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,6 @@ class _ProjectedPolyline<R extends Object> with HitDetectableElement<R> {
1616
_ProjectedPolyline._fromPolyline(Projection projection, Polyline<R> polyline)
1717
: this._(
1818
polyline: polyline,
19-
points: List<DoublePoint>.generate(
20-
polyline.points.length,
21-
(j) {
22-
final (x, y) = projection.projectXY(polyline.points[j]);
23-
return DoublePoint(x, y);
24-
},
25-
growable: false,
26-
),
19+
points: projection.projectList(polyline.points),
2720
);
2821
}

lib/src/misc/offsets.dart

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,44 @@ List<Offset> getOffsetsXY({
6060
final oy = -origin.dy;
6161
final len = realPoints.length;
6262

63+
/// Returns additional world width in order to have visible points.
64+
double getAddedWorldWidth() {
65+
final worldWidth = crs.projection.getWorldWidth();
66+
final List<double> addedWidths = [
67+
0,
68+
worldWidth,
69+
-worldWidth,
70+
];
71+
final halfScreenWidth = camera.size.x / 2;
72+
final p = realPoints.elementAt(0);
73+
late double result;
74+
late double bestX;
75+
for (int i = 0; i < addedWidths.length; i++) {
76+
final addedWidth = addedWidths[i];
77+
final (x, _) = crs.transform(p.x + addedWidth, p.y, zoomScale);
78+
if (i == 0) {
79+
result = addedWidth;
80+
bestX = x;
81+
continue;
82+
}
83+
if ((bestX + ox - halfScreenWidth).abs() >
84+
(x + ox - halfScreenWidth).abs()) {
85+
result = addedWidth;
86+
bestX = x;
87+
}
88+
}
89+
return result;
90+
}
91+
92+
final double addedWorldWidth = getAddedWorldWidth();
93+
6394
// Optimization: monomorphize the CrsWithStaticTransformation-case to avoid
6495
// the virtual function overhead.
6596
if (crs case final CrsWithStaticTransformation crs) {
6697
final v = List<Offset>.filled(len, Offset.zero, growable: true);
6798
for (int i = 0; i < len; ++i) {
6899
final p = realPoints.elementAt(i);
69-
final (x, y) = crs.transform(p.x, p.y, zoomScale);
100+
final (x, y) = crs.transform(p.x + addedWorldWidth, p.y, zoomScale);
70101
v[i] = Offset(x + ox, y + oy);
71102
}
72103
return v;
@@ -75,7 +106,7 @@ List<Offset> getOffsetsXY({
75106
final v = List<Offset>.filled(len, Offset.zero, growable: true);
76107
for (int i = 0; i < len; ++i) {
77108
final p = realPoints.elementAt(i);
78-
final (x, y) = crs.transform(p.x, p.y, zoomScale);
109+
final (x, y) = crs.transform(p.x + addedWorldWidth, p.y, zoomScale);
79110
v[i] = Offset(x + ox, y + oy);
80111
}
81112
return v;

0 commit comments

Comments
 (0)