Skip to content

Commit

Permalink
implementation of polyline and polygon simplification
Browse files Browse the repository at this point in the history
  • Loading branch information
mootw committed Oct 25, 2023
1 parent 6e8cc20 commit 8bb6b0d
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 21 deletions.
41 changes: 32 additions & 9 deletions lib/src/layer/polygon_layer/polygon_layer.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'dart:math';
import 'dart:ui' as ui;

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/layer/polygon_layer/label.dart';
import 'package:flutter_map/src/map/camera/camera.dart';
import 'package:flutter_map/src/misc/simplify.dart';
import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI

enum PolygonLabelPlacement {
Expand Down Expand Up @@ -95,13 +97,21 @@ class PolygonLayer extends StatelessWidget {
/// screen space culling of polygons based on bounding box
final bool polygonCulling;

/// how much to simplify the polygons, in decimal degrees scaled to floored zoom
final double? simplificationTolerance;

/// use high quality simplification
final bool simplificationHighQuality;

// Turn on/off per-polygon label drawing on the layer-level.
final bool polygonLabels;

const PolygonLayer({
super.key,
required this.polygons,
this.polygonCulling = false,
this.simplificationTolerance = 1,
this.simplificationHighQuality = false,
this.polygonLabels = true,
});

Expand All @@ -110,15 +120,16 @@ class PolygonLayer extends StatelessWidget {
final map = MapCamera.of(context);
final size = Size(map.size.x, map.size.y);

final pgons = polygonCulling
final polygonsToRender = polygonCulling
? polygons.where((p) {
return p.boundingBox.isOverlapping(map.visibleBounds);
}).toList()
: polygons;

return MobileLayerTransformer(
child: CustomPaint(
painter: PolygonPainter(pgons, map, polygonLabels),
painter: PolygonPainter(polygonsToRender, map, polygonLabels,
simplificationTolerance, simplificationHighQuality),
size: size,
isComplex: true,
),
Expand All @@ -128,12 +139,16 @@ class PolygonLayer extends StatelessWidget {

class PolygonPainter extends CustomPainter {
final List<Polygon> polygons;
final MapCamera map;
final MapCamera camera;
final LatLngBounds bounds;
final bool polygonLabels;

PolygonPainter(this.polygons, this.map, this.polygonLabels)
: bounds = map.visibleBounds;
final double? simplificationTolerance;
final bool simplificationHighQuality;

PolygonPainter(this.polygons, this.camera, this.polygonLabels,
this.simplificationTolerance, this.simplificationHighQuality)
: bounds = camera.visibleBounds;

int get hash {
_hash ??= Object.hashAll(polygons);
Expand All @@ -143,10 +158,18 @@ class PolygonPainter extends CustomPainter {
int? _hash;

List<Offset> getOffsets(List<LatLng> points) {
final List<LatLng> simplifiedPoints;
if (simplificationTolerance != null) {
simplifiedPoints = simplify(points,
simplificationTolerance! / pow(2, camera.zoom.floorToDouble()),
highestQuality: simplificationHighQuality);
} else {
simplifiedPoints = points;
}
return List.generate(
points.length,
simplifiedPoints.length,
(index) {
return map.getOffsetFromOrigin(points[index]);
return camera.getOffsetFromOrigin(simplifiedPoints[index]);
},
growable: false,
);
Expand Down Expand Up @@ -248,11 +271,11 @@ class PolygonPainter extends CustomPainter {
final painter = buildLabelTextPainter(
polygon.points,
polygon.labelPosition,
placementPoint: map.getOffsetFromOrigin(polygon.labelPosition),
placementPoint: camera.getOffsetFromOrigin(polygon.labelPosition),
points: offsets,
labelText: polygon.label!,
labelStyle: polygon.labelStyle,
rotationRad: map.rotationRad,
rotationRad: camera.rotationRad,
rotate: polygon.rotateLabel,
padding: 10,
);
Expand Down
49 changes: 37 additions & 12 deletions lib/src/layer/polyline_layer.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'dart:core';
import 'dart:math';
import 'dart:ui' as ui;

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/simplify.dart';
import 'package:latlong2/latlong.dart';

class Polyline {
Expand Down Expand Up @@ -58,10 +60,18 @@ class PolylineLayer extends StatelessWidget {
final List<Polyline> polylines;
final bool polylineCulling;

/// how much to simplify the polygons, in decimal degrees scaled to floored zoom
final double? simplificationTolerance;

/// use high quality simplification
final bool simplificationHighQuality;

const PolylineLayer({
super.key,
required this.polylines,
this.polylineCulling = false,
this.simplificationTolerance = 1,
this.simplificationHighQuality = false,
});

@override
Expand All @@ -71,13 +81,15 @@ class PolylineLayer extends StatelessWidget {
return MobileLayerTransformer(
child: CustomPaint(
painter: PolylinePainter(
polylineCulling
? polylines
.where((p) => p.boundingBox.isOverlapping(map.visibleBounds))
.toList()
: polylines,
map,
),
polylineCulling
? polylines
.where(
(p) => p.boundingBox.isOverlapping(map.visibleBounds))
.toList()
: polylines,
map,
simplificationTolerance,
simplificationHighQuality),
size: Size(map.size.x, map.size.y),
isComplex: true,
),
Expand All @@ -88,22 +100,35 @@ class PolylineLayer extends StatelessWidget {
class PolylinePainter extends CustomPainter {
final List<Polyline> polylines;

final MapCamera map;
final MapCamera camera;
final LatLngBounds bounds;

PolylinePainter(this.polylines, this.map) : bounds = map.visibleBounds;
final double? simplificationTolerance;
final bool simplificationHighQuality;

PolylinePainter(this.polylines, this.camera, this.simplificationTolerance,
this.simplificationHighQuality)
: bounds = camera.visibleBounds;

int get hash => _hash ??= Object.hashAll(polylines);

int? _hash;

List<Offset> getOffsets(List<LatLng> points) {
return List.generate(points.length, (index) {
return getOffset(points[index]);
final List<LatLng> simplifiedPoints;
if (simplificationTolerance != null) {
simplifiedPoints = simplify(points,
simplificationTolerance! / pow(2, camera.zoom.floorToDouble()),
highestQuality: simplificationHighQuality);
} else {
simplifiedPoints = points;
}
return List.generate(simplifiedPoints.length, (index) {
return getOffset(simplifiedPoints[index]);
}, growable: false);
}

Offset getOffset(LatLng point) => map.getOffsetFromOrigin(point);
Offset getOffset(LatLng point) => camera.getOffsetFromOrigin(point);

@override
void paint(Canvas canvas, Size size) {
Expand Down
120 changes: 120 additions & 0 deletions lib/src/misc/simplify.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// implementation based on
// https://github.com/mourner/simplify-js/blob/master/simplify.js


import 'package:latlong2/latlong.dart';

double _getSqDist(
LatLng p1,
LatLng p2,
) {
final double dx = p1.longitude - p2.longitude;
final double dy = p1.latitude - p2.latitude;
return dx * dx + dy * dy;
}

// square distance from a point to a segment
double _getSqSegDist(
LatLng p,
LatLng p1,
LatLng p2,
) {
double x = p1.longitude;
double y = p1.latitude;
double dx = p2.longitude - x;
double dy = p2.latitude - y;
if (dx != 0 || dy != 0) {
final double t =
((p.longitude - x) * dx + (p.latitude - y) * dy) /
(dx * dx + dy * dy);
if (t > 1) {
x = p2.longitude;
y = p2.latitude;
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}

dx = p.longitude - x;
dy = p.latitude - y;

return dx * dx + dy * dy;
}

List<LatLng> _simplifyRadialDist(
List<LatLng> points,
double sqTolerance,
) {
LatLng prevPoint = points[0];
final List<LatLng> newPoints = [prevPoint];
late LatLng point;
for (int i = 1, len = points.length; i < len; i++) {
point = points[i];
if (_getSqDist(point, prevPoint) > sqTolerance) {
newPoints.add(point);
prevPoint = point;
}
}
if (prevPoint != point) {
newPoints.add(point);
}
return newPoints;
}

void _simplifyDPStep(
List<LatLng> points,
int first,
int last,
double sqTolerance,
List<LatLng> simplified,
) {
double maxSqDist = sqTolerance;
late int index;
for (int i = first + 1; i < last; i++) {
final double sqDist = _getSqSegDist(points[i], points[first], points[last]);

if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
}
if (maxSqDist > sqTolerance) {
if (index - first > 1) {
_simplifyDPStep(points, first, index, sqTolerance, simplified);
}
simplified.add(points[index]);
if (last - index > 1) {
_simplifyDPStep(points, index, last, sqTolerance, simplified);
}
}
}

// simplification using Ramer-Douglas-Peucker algorithm
List<LatLng> _simplifyDouglasPeucker(
List<LatLng> points,
double sqTolerance,
) {
final int last = points.length - 1;
final List<LatLng> simplified = [points[0]];
_simplifyDPStep(points, 0, last, sqTolerance, simplified);
simplified.add(points[last]);
return simplified;
}

// both algorithms combined for awesome performance
List<LatLng> simplify(
List<LatLng> points,
double tolerance, {
bool highestQuality = false,
}) {
if (points.length <= 2) {
return points;
}
List<LatLng> nextPoints = points;
final double sqTolerance = tolerance * tolerance;
nextPoints =
highestQuality ? points : _simplifyRadialDist(nextPoints, sqTolerance);
nextPoints = _simplifyDouglasPeucker(nextPoints, sqTolerance);
return nextPoints;
}

0 comments on commit 8bb6b0d

Please sign in to comment.