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

perf!: cache projection of polygon points & CRS improvements #1801

Merged
merged 11 commits into from
Jan 21, 2024
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);
ignatz marked this conversation as resolved.
Show resolved Hide resolved
(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 {
ignatz marked this conversation as resolved.
Show resolved Hide resolved
@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 {
JaffaKetchup marked this conversation as resolved.
Show resolved Hide resolved
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
Loading