Skip to content

Commit

Permalink
feat: allow polylines & polygons to cross world boundary (#1969)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
monsieurtanuki authored Dec 3, 2024
1 parent aca8aed commit b81d6db
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 68 deletions.
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
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';
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

0 comments on commit b81d6db

Please sign in to comment.