Skip to content

Commit

Permalink
perf!: cache projection of polygon points & CRS improvements (#1801)
Browse files Browse the repository at this point in the history
  • Loading branch information
ignatz authored Jan 21, 2024
1 parent 0724cf4 commit 89cd3df
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 208 deletions.
2 changes: 1 addition & 1 deletion lib/flutter_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/// * discord.gg: <https://discord.gg/BwpEsjqMAH>
library flutter_map;

export 'package:flutter_map/src/geo/crs.dart';
export 'package:flutter_map/src/geo/crs.dart' hide CrsWithStaticTransformation;
export 'package:flutter_map/src/geo/latlng_bounds.dart';
export 'package:flutter_map/src/gestures/interactive_flag.dart';
export 'package:flutter_map/src/gestures/latlng_tween.dart';
Expand Down
64 changes: 44 additions & 20 deletions lib/src/geo/crs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@ abstract class Crs {
this.wrapLat,
});

/// Project a spherical LatLng coordinate into planar space (unscaled).
Projection get projection;

/// Converts a point on the sphere surface (with a certain zoom) in a
/// map point.
/// Scale planar coordinate to scaled map point.
(double, double) transform(double x, double y, double scale);
(double, double) untransform(double x, double y, double scale);

/// Converts a point on the sphere surface (with a certain zoom) to a
/// scaled map point.
(double, double) latLngToXY(LatLng latlng, double scale);

Point<double> latLngToPoint(LatLng latlng, double zoom) {
Expand All @@ -53,33 +58,42 @@ abstract class Crs {
Bounds<double>? getProjectedBounds(double zoom);
}

/// Internal base class for CRS with a single zoom-level independent transformation.
@immutable
abstract class _CrsWithStaticTransformation extends Crs {
@internal
abstract class CrsWithStaticTransformation extends Crs {
@nonVirtual
@protected
final _Transformation transformation;
final _Transformation _transformation;

@override
final Projection projection;

const _CrsWithStaticTransformation({
required this.transformation,
const CrsWithStaticTransformation._({
required _Transformation transformation,
required this.projection,
required super.code,
required super.infinite,
super.wrapLng,
super.wrapLat,
});
}) : _transformation = transformation;

@override
(double, double) transform(double x, double y, double scale) =>
_transformation.transform(x, y, scale);
@override
(double, double) untransform(double x, double y, double scale) =>
_transformation.untransform(x, y, scale);

@override
(double, double) latLngToXY(LatLng latlng, double scale) {
final (x, y) = projection.projectXY(latlng);
return transformation.transform(x, y, scale);
return _transformation.transform(x, y, scale);
}

@override
LatLng pointToLatLng(Point point, double zoom) {
final (x, y) = transformation.untransform(
final (x, y) = _transformation.untransform(
point.x.toDouble(),
point.y.toDouble(),
scale(zoom),
Expand All @@ -93,8 +107,8 @@ abstract class _CrsWithStaticTransformation extends Crs {

final b = projection.bounds!;
final s = scale(zoom);
final (minx, miny) = transformation.transform(b.min.x, b.min.y, s);
final (maxx, maxy) = transformation.transform(b.max.x, b.max.y, s);
final (minx, miny) = _transformation.transform(b.min.x, b.min.y, s);
final (maxx, maxy) = _transformation.transform(b.max.x, b.max.y, s);
return Bounds<double>(
Point<double>(minx, miny),
Point<double>(maxx, maxy),
Expand All @@ -104,9 +118,9 @@ abstract class _CrsWithStaticTransformation extends Crs {

// Custom CRS for non geographical maps
@immutable
class CrsSimple extends _CrsWithStaticTransformation {
class CrsSimple extends CrsWithStaticTransformation {
const CrsSimple()
: super(
: super._(
code: 'CRS.SIMPLE',
transformation: const _Transformation(1, 0, -1, 0),
projection: const _LonLat(),
Expand All @@ -118,11 +132,11 @@ class CrsSimple extends _CrsWithStaticTransformation {

/// The most common CRS used for rendering maps.
@immutable
class Epsg3857 extends _CrsWithStaticTransformation {
class Epsg3857 extends CrsWithStaticTransformation {
static const double _scale = 0.5 / (math.pi * SphericalMercator.r);

const Epsg3857()
: super(
: super._(
code: 'EPSG:3857',
transformation: const _Transformation(_scale, 0.5, -_scale, 0.5),
projection: const SphericalMercator(),
Expand All @@ -132,12 +146,15 @@ class Epsg3857 extends _CrsWithStaticTransformation {

@override
(double, double) latLngToXY(LatLng latlng, double scale) =>
transformation.transform(SphericalMercator.projectLng(latlng.longitude),
SphericalMercator.projectLat(latlng.latitude), scale);
_transformation.transform(
SphericalMercator.projectLng(latlng.longitude),
SphericalMercator.projectLat(latlng.latitude),
scale,
);

@override
Point<double> latLngToPoint(LatLng latlng, double zoom) {
final (x, y) = transformation.transform(
final (x, y) = _transformation.transform(
SphericalMercator.projectLng(latlng.longitude),
SphericalMercator.projectLat(latlng.latitude),
scale(zoom),
Expand All @@ -148,9 +165,9 @@ class Epsg3857 extends _CrsWithStaticTransformation {

/// A common CRS among GIS enthusiasts. Uses simple Equirectangular projection.
@immutable
class Epsg4326 extends _CrsWithStaticTransformation {
class Epsg4326 extends CrsWithStaticTransformation {
const Epsg4326()
: super(
: super._(
projection: const _LonLat(),
transformation: const _Transformation(1 / 180, 1, -1 / 180, 0.5),
code: 'EPSG:4326',
Expand Down Expand Up @@ -222,6 +239,13 @@ class Proj4Crs extends Crs {
);
}

@override
(double, double) transform(double x, double y, double scale) =>
_getTransformationByZoom(zoom(scale)).transform(x, y, scale);
@override
(double, double) untransform(double x, double y, double scale) =>
_getTransformationByZoom(zoom(scale)).untransform(x, y, scale);

/// Converts a point on the sphere surface (with a certain zoom) in a
/// map point.
@override
Expand Down
50 changes: 23 additions & 27 deletions lib/src/layer/polygon_layer/painter.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
part of 'polygon_layer.dart';

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

PolygonPainter({
_PolygonPainter({
required this.polygons,
required this.camera,
required this.polygonLabels,
Expand All @@ -17,23 +17,11 @@ class PolygonPainter extends CustomPainter {
({Offset min, Offset max}) getBounds(Offset origin, Polygon polygon) {
final bbox = polygon.boundingBox;
return (
min: getOffset(origin, bbox.southWest),
max: getOffset(origin, bbox.northEast),
min: getOffset(camera, origin, bbox.southWest),
max: getOffset(camera, origin, bbox.northEast),
);
}

Offset getOffset(Offset origin, LatLng point) {
// Critically create as little garbage as possible. This is called on every frame.
final projected = camera.project(point);
return Offset(projected.x - origin.dx, projected.y - origin.dy);
}

List<Offset> getOffsets(Offset origin, List<LatLng> points) => List.generate(
points.length,
(index) => getOffset(origin, points[index]),
growable: false,
);

@override
void paint(Canvas canvas, Size size) {
var filledPath = ui.Path();
Expand Down Expand Up @@ -73,11 +61,12 @@ class PolygonPainter extends CustomPainter {
final origin = (camera.project(camera.center) - camera.size / 2).toOffset();

// Main loop constructing batched fill and border paths from given polygons.
for (final polygon in polygons) {
if (polygon.points.isEmpty) {
for (final projectedPolygon in polygons) {
if (projectedPolygon.points.isEmpty) {
continue;
}
final offsets = getOffsets(origin, polygon.points);
final polygon = projectedPolygon.polygon;
final offsets = getOffsetsXY(camera, origin, projectedPolygon.points);

// The hash is based on the polygons visual properties. If the hash from
// the current and the previous polygon no longer match, we need to flush
Expand Down Expand Up @@ -110,7 +99,7 @@ class PolygonPainter extends CustomPainter {

final holeOffsetsList = List<List<Offset>>.generate(
holePointsList.length,
(i) => getOffsets(origin, holePointsList[i]),
(i) => getOffsets(camera, origin, holePointsList[i]),
growable: false,
);

Expand Down Expand Up @@ -155,10 +144,11 @@ class PolygonPainter extends CustomPainter {
drawPaths();

if (polygonLabels && drawLabelsLast) {
for (final polygon in polygons) {
if (polygon.points.isEmpty) {
for (final projectedPolygon in polygons) {
if (projectedPolygon.points.isEmpty) {
continue;
}
final polygon = projectedPolygon.polygon;
final textPainter = polygon.textPainter;
if (textPainter != null) {
final painter = _buildLabelTextPainter(
Expand Down Expand Up @@ -221,26 +211,32 @@ class PolygonPainter extends CustomPainter {
}

void _addDottedLineToPath(
ui.Path path, List<Offset> offsets, double radius, double stepLength) {
ui.Path path,
List<Offset> offsets,
double radius,
double stepLength,
) {
if (offsets.isEmpty) {
return;
}

double startDistance = 0;
for (var i = 0; i < offsets.length; i++) {
for (int i = 0; i < offsets.length; i++) {
final o0 = offsets[i % offsets.length];
final o1 = offsets[(i + 1) % offsets.length];
final totalDistance = (o0 - o1).distance;

double distance = startDistance;
for (; distance < totalDistance; distance += stepLength) {
while (distance < totalDistance) {
final done = distance / totalDistance;
final remain = 1.0 - done;
final offset = Offset(
o0.dx * remain + o1.dx * done,
o0.dy * remain + o1.dy * done,
);
path.addOval(Rect.fromCircle(center: offset, radius: radius));

distance += stepLength;
}

startDistance = distance < totalDistance
Expand All @@ -256,5 +252,5 @@ class PolygonPainter extends CustomPainter {
}

@override
bool shouldRepaint(PolygonPainter oldDelegate) => false;
bool shouldRepaint(_PolygonPainter oldDelegate) => false;
}
18 changes: 0 additions & 18 deletions lib/src/layer/polygon_layer/polygon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,6 @@ class Polygon {
}) : _filledAndClockwise =
(isFilled ?? (color != null)) && isClockwise(points);

Polygon copyWithNewPoints(List<LatLng> points) => Polygon(
points: points,
holePointsList: holePointsList,
color: color,
borderStrokeWidth: borderStrokeWidth,
borderColor: borderColor,
disableHolesBorder: disableHolesBorder,
isDotted: isDotted,
// ignore: deprecated_member_use_from_same_package
isFilled: isFilled,
strokeCap: strokeCap,
strokeJoin: strokeJoin,
label: label,
labelStyle: labelStyle,
labelPlacement: labelPlacement,
rotateLabel: rotateLabel,
);

static bool isClockwise(List<LatLng> points) {
double sum = 0;
for (int i = 0; i < points.length; ++i) {
Expand Down
Loading

0 comments on commit 89cd3df

Please sign in to comment.