Skip to content

Commit

Permalink
Cache projection of polygon points.
Browse files Browse the repository at this point in the history
  • Loading branch information
ignatz committed Jan 16, 2024
1 parent 202e320 commit 5986b39
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 53 deletions.
62 changes: 42 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) {
final (x, y) = latLngToXY(latlng, scale(zoom));
Expand All @@ -53,32 +58,39 @@ abstract class Crs {
}

@immutable
abstract class _CrsWithStaticTransformation extends Crs {
abstract class CrsWithStaticTransformation extends Crs {
@nonVirtual
@protected
final _Transformation transformation;
final _Transformation _transformation;

@override
final Projection projection;

const _CrsWithStaticTransformation({
required this.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);

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

@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 @@ -92,8 +104,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 @@ -103,9 +115,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 @@ -117,11 +129,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 @@ -131,12 +143,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 @@ -147,9 +162,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 @@ -221,6 +236,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
21 changes: 5 additions & 16 deletions lib/src/layer/polygon_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -77,7 +65,8 @@ class PolygonPainter extends CustomPainter {
if (polygon.points.isEmpty) {
continue;
}
final offsets = getOffsets(origin, polygon.points);
final offsets = getOffsetsXY(
camera, origin, polygon.getProjectedPoints(camera.crs.projection));

// 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
6 changes: 6 additions & 0 deletions lib/src/layer/polygon_layer/polygon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class Polygon {
LatLngBounds get boundingBox =>
_boundingBox ??= LatLngBounds.fromPoints(points);

List<(double, double)>? _projectedPoints;
List<(double, double)> getProjectedPoints(Projection projection) =>
_projectedPoints ??= List<(double, double)>.generate(
points.length, (i) => projection.projectXY(points[i]),
growable: false);

TextPainter? _textPainter;
TextPainter? get textPainter {
if (label != null) {
Expand Down
27 changes: 17 additions & 10 deletions lib/src/layer/polygon_layer/polygon_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import 'dart:ui' as ui;
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_map/src/geo/crs.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/offsets.dart';
import 'package:flutter_map/src/misc/point_extensions.dart';
import 'package:flutter_map/src/misc/simplify.dart';
import 'package:latlong2/latlong.dart' hide Path;
Expand Down Expand Up @@ -119,15 +121,20 @@ class _PolygonLayerState extends State<PolygonLayer> {
}

List<Polygon> _computeZoomLevelSimplification(int zoom) =>
_cachedSimplifiedPolygons[zoom] ??= widget.polygons
.map(
(polygon) => polygon.copyWithNewPoints(
simplify(
polygon.points,
widget.simplificationTolerance / math.pow(2, zoom),
highestQuality: true,
),
_cachedSimplifiedPolygons[zoom] ??= List<Polygon>.generate(
widget.polygons.length,
(i) {
final polygon = widget.polygons[i];
return polygon.copyWithNewPoints(
// TODO: Ideally we'd simplify in projected space to minimize issues with map distortion.
// TODO: Simplify polygon holes as well.
simplify(
polygon.points,
widget.simplificationTolerance / math.pow(2, zoom),
highestQuality: true,
),
)
.toList();
);
},
growable: false,
);
}
34 changes: 33 additions & 1 deletion lib/src/misc/offsets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ List<Offset> getOffsets(MapCamera camera, Offset origin, List<LatLng> points) {
final oy = -origin.dy;
final len = points.length;

// Optimization: monomorphize the Epsg3857-case to save the virtual function overhead.
// Optimization: monomorphize the Epsg3857-case to avoid the virtual function overhead.
if (crs is Epsg3857) {
final Epsg3857 epsg3857 = crs;
final v = List<Offset>.filled(len, Offset.zero);
Expand All @@ -38,3 +38,35 @@ List<Offset> getOffsets(MapCamera camera, Offset origin, List<LatLng> points) {
}
return v;
}

List<Offset> getOffsetsXY(
MapCamera camera, Offset origin, List<(double, double)> points) {
// Critically create as little garbage as possible. This is called on every frame.
final crs = camera.crs;
final zoomScale = crs.scale(camera.zoom);

final ox = -origin.dx;
final oy = -origin.dy;
final len = points.length;

// Optimization: monomorphize the CrsWithStaticTransformation-case to avoid
// the virtual function overhead.
if (crs is CrsWithStaticTransformation) {
final CrsWithStaticTransformation mcrs = crs;
final v = List<Offset>.filled(len, Offset.zero);
for (int i = 0; i < len; ++i) {
final (px, py) = points[i];
final (x, y) = mcrs.transform(px, py, zoomScale);
v[i] = Offset(x + ox, y + oy);
}
return v;
}

final v = List<Offset>.filled(len, Offset.zero);
for (int i = 0; i < len; ++i) {
final (px, py) = points[i];
final (x, y) = crs.transform(px, py, zoomScale);
v[i] = Offset(x + ox, y + oy);
}
return v;
}
14 changes: 8 additions & 6 deletions test/layer/tile_layer/tile_bounds/crs_fakes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ class FakeInfiniteCrs extends Crs {

/// Any projection just to get non-zero coordiantes.
@override
Point<double> latLngToPoint(LatLng latlng, double zoom) {
return const Epsg3857().latLngToPoint(latlng, zoom);
}
(double, double) latLngToXY(LatLng latlng, double scale) =>
const Epsg3857().latLngToXY(latlng, scale);

@override
(double, double) latLngToXY(LatLng latlng, double scale) {
return const Epsg3857().latLngToXY(latlng, scale);
}
(double, double) transform(double x, double y, double scale) =>
const Epsg3857().transform(x, y, scale);

@override
(double, double) untransform(double x, double y, double scale) =>
const Epsg3857().untransform(x, y, scale);

@override
LatLng pointToLatLng(Point point, double zoom) => throw UnimplementedError();
Expand Down

0 comments on commit 5986b39

Please sign in to comment.