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: expose isPointInPolygon and make 40% faster (at least in JIT mode) #1907

Merged
merged 4 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions benchmark/point_in_polygon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui';

import 'package:flutter_map/src/misc/point_in_polygon.dart';
import 'package:logger/logger.dart';

class NoFilter extends LogFilter {
@override
bool shouldLog(LogEvent event) => true;
}

typedef Result = ({
String name,
Duration duration,
});

Future<Result> timedRun(String name, dynamic Function() body) async {
Logger().i('running $name...');
final watch = Stopwatch()..start();
await body();
watch.stop();

return (name: name, duration: watch.elapsed);
}

List<Offset> makeCircle(int points, double radius, double phase) {
final slice = math.pi * 2 / (points - 1);
return List.generate(points, (i) {
// Note the modulo is only there to deal with floating point imprecision
// and ensure first == last.
final angle = slice * (i % (points - 1)) + phase;
return Offset(radius * math.cos(angle), radius * math.sin(angle));
}, growable: false);
}

// NOTE: to have a more prod like comparison, run with:
// $ dart compile exe benchmark/crs.dart && ./benchmark/crs.exe
//
// If you run in JIT mode, the resulting execution times will be a lot more similar.
Future<void> main() async {
Logger.level = Level.all;
Logger.defaultFilter = NoFilter.new;
Logger.defaultPrinter = SimplePrinter.new;

final results = <Result>[];
const N = 3000000;

final circle = makeCircle(1000, 1, 0);

results.add(await timedRun('In circle', () {
const point = math.Point(0, 0);

bool yesPlease = true;
for (int i = 0; i < N; ++i) {
yesPlease = yesPlease && isPointInPolygon(point, circle);
}

assert(yesPlease, 'should be in circle');
return yesPlease;
}));

results.add(await timedRun('Not in circle', () {
const point = math.Point(4, 4);

bool noSir = false;
for (int i = 0; i < N; ++i) {
noSir = noSir || isPointInPolygon(point, circle);
}

assert(!noSir, 'should not be in circle');
return noSir;
}));

Logger().i('Results:\n${results.map((r) => r.toString()).join('\n')}');
}
2 changes: 1 addition & 1 deletion lib/src/geo/crs.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:math' as math hide Point;
import 'dart:math' show Point;

import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/src/misc/bounds.dart';
import 'package:latlong2/latlong.dart';
import 'package:meta/meta.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;
Expand Down
57 changes: 19 additions & 38 deletions lib/src/layer/polygon_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,42 +39,41 @@ base class _PolygonPainter<R extends Object>
required LatLng coordinate,
}) {
final polygon = projectedPolygon.polygon;

if (!polygon.boundingBox.contains(coordinate)) return false;
if (!polygon.boundingBox.contains(coordinate)) {
return false;
}

final projectedCoords = getOffsetsXY(
camera: camera,
origin: hitTestCameraOrigin,
points: projectedPolygon.points,
).toList();
);

ignatz marked this conversation as resolved.
Show resolved Hide resolved
if (projectedCoords.first != projectedCoords.last) {
projectedCoords.add(projectedCoords.first);
}
final isInPolygon = isPointInPolygon(point, projectedCoords);

final hasHoles = projectedPolygon.holePoints.isNotEmpty;
late final List<List<Offset>> projectedHoleCoords;
if (hasHoles) {
projectedHoleCoords = projectedPolygon.holePoints
.map(
(points) => getOffsetsXY(
final isInHole = hasHoles &&
() {
for (final points in projectedPolygon.holePoints) {
final projectedHoleCoords = getOffsetsXY(
camera: camera,
origin: hitTestCameraOrigin,
points: points,
).toList(),
ignatz marked this conversation as resolved.
Show resolved Hide resolved
)
.toList();
);

if (projectedHoleCoords.firstOrNull != projectedHoleCoords.lastOrNull) {
ignatz marked this conversation as resolved.
Show resolved Hide resolved
projectedHoleCoords.add(projectedHoleCoords.first);
}
}
if (projectedHoleCoords.first != projectedHoleCoords.last) {
projectedHoleCoords.add(projectedHoleCoords.first);
}

final isInPolygon = _isPointInPolygon(point, projectedCoords);
final isInHole = hasHoles &&
projectedHoleCoords
.map((c) => _isPointInPolygon(point, c))
.any((e) => e);
JaffaKetchup marked this conversation as resolved.
Show resolved Hide resolved
if (isPointInPolygon(point, projectedHoleCoords)) {
return true;
}
}
return false;
}();

// Second check handles case where polygon outline intersects a hole,
// ensuring that the hit matches with the visual representation
Expand Down Expand Up @@ -361,24 +360,6 @@ base class _PolygonPainter<R extends Object>
);
}

/// Checks whether point [p] is within the specified closed [polygon]
///
/// Uses the even-odd algorithm.
static bool _isPointInPolygon(math.Point p, List<Offset> polygon) {
bool isInPolygon = false;

for (int i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
if ((((polygon[i].dy <= p.y) && (p.y < polygon[j].dy)) ||
((polygon[j].dy <= p.y) && (p.y < polygon[i].dy))) &&
(p.x <
(polygon[j].dx - polygon[i].dx) *
(p.y - polygon[i].dy) /
(polygon[j].dy - polygon[i].dy) +
polygon[i].dx)) isInPolygon = !isInPolygon;
}
return isInPolygon;
}

@override
bool shouldRepaint(_PolygonPainter<R> oldDelegate) =>
polygons != oldDelegate.polygons ||
Expand Down
1 change: 1 addition & 0 deletions lib/src/layer/polygon_layer/polygon_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart';
import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart';
import 'package:flutter_map/src/misc/offsets.dart';
import 'package:flutter_map/src/misc/point_in_polygon.dart';
import 'package:flutter_map/src/misc/simplify.dart';
import 'package:latlong2/latlong.dart' hide Path;
import 'package:polylabel/polylabel.dart';
Expand Down
8 changes: 4 additions & 4 deletions lib/src/misc/offsets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ List<Offset> getOffsets(MapCamera camera, Offset origin, List<LatLng> points) {

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

final v = List<Offset>.filled(len, Offset.zero);
final v = List<Offset>.filled(len, Offset.zero, growable: true);
for (int i = 0; i < len; ++i) {
final (x, y) = crs.latLngToXY(points[i], zoomScale);
v[i] = Offset(x + ox, y + oy);
Expand Down Expand Up @@ -63,7 +63,7 @@ List<Offset> getOffsetsXY({
// 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);
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);
Expand All @@ -72,7 +72,7 @@ List<Offset> getOffsetsXY({
return v;
}

final v = List<Offset>.filled(len, Offset.zero);
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);
Expand Down
29 changes: 29 additions & 0 deletions lib/src/misc/point_in_polygon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'dart:math' as math;
import 'dart:ui';

/// Checks whether point [p] is within the specified closed [polygon]
///
/// Uses the even-odd algorithm and requires closed loop polygons, i.e.
/// `polygon.first == polygon.last`.
bool isPointInPolygon(math.Point p, List<Offset> polygon) {
final len = polygon.length;
assert(len >= 3, 'not a polygon');
assert(polygon.first == polygon.last, 'polygon not closed');
final double px = p.x.toDouble();
final double py = p.y.toDouble();

bool isInPolygon = false;
for (int i = 0, j = len - 1; i < len; j = i++) {
final double poIx = polygon[i].dx;
final double poIy = polygon[i].dy;

final double poJx = polygon[j].dx;
final double poJy = polygon[j].dy;

if ((((poIy <= py) && (py < poJy)) || ((poJy <= py) && (py < poIy))) &&
(px < (poJx - poIx) * (py - poIy) / (poJy - poIy) + poIx)) {
isInPolygon = !isInPolygon;
}
}
return isInPolygon;
}
41 changes: 41 additions & 0 deletions test/misc/point_in_polygon_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'dart:math' as math;

import 'package:flutter_map/src/misc/point_in_polygon.dart';
import 'package:flutter_test/flutter_test.dart';

List<Offset> makeCircle(int points, double radius, double phase) {
final slice = math.pi * 2 / (points - 1);
return List.generate(points, (i) {
// Note the modulo is only there to deal with floating point imprecision
// and ensure first == last.
final angle = slice * (i % (points - 1)) + phase;
return Offset(radius * math.cos(angle), radius * math.sin(angle));
}, growable: false);
}

void main() {
test('Smoke test for points in and out of polygons', () {
final circle = makeCircle(100, 1, 0);

// Inside points
for (final point in makeCircle(32, 0.8, 0.0001)) {
final p = math.Point(point.dx, point.dy);
expect(isPointInPolygon(p, circle), isTrue);
}

// Edge-case: check origin
expect(isPointInPolygon(const math.Point(0, 0), circle), isTrue);

// Outside points: small radius
for (final point in makeCircle(32, 1.1, 0.0001)) {
final p = math.Point(point.dx, point.dy);
expect(isPointInPolygon(p, circle), isFalse);
}

// Outside points: large radius
for (final point in makeCircle(32, 100000, 0.0001)) {
final p = math.Point(point.dx, point.dy);
expect(isPointInPolygon(p, circle), isFalse);
}
});
}
Loading