Skip to content

Commit c321865

Browse files
ignatzbarfootsies
andauthored
perf: expose isPointInPolygon and make 40% faster (at least in JIT mode) (#1907)
Co-authored-by: Sebastian <[email protected]>
1 parent b69a0d7 commit c321865

File tree

7 files changed

+171
-43
lines changed

7 files changed

+171
-43
lines changed

benchmark/point_in_polygon.dart

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import 'dart:async';
2+
import 'dart:math' as math;
3+
import 'dart:ui';
4+
5+
import 'package:flutter_map/src/misc/point_in_polygon.dart';
6+
import 'package:logger/logger.dart';
7+
8+
class NoFilter extends LogFilter {
9+
@override
10+
bool shouldLog(LogEvent event) => true;
11+
}
12+
13+
typedef Result = ({
14+
String name,
15+
Duration duration,
16+
});
17+
18+
Future<Result> timedRun(String name, dynamic Function() body) async {
19+
Logger().i('running $name...');
20+
final watch = Stopwatch()..start();
21+
await body();
22+
watch.stop();
23+
24+
return (name: name, duration: watch.elapsed);
25+
}
26+
27+
List<Offset> makeCircle(int points, double radius, double phase) {
28+
final slice = math.pi * 2 / (points - 1);
29+
return List.generate(points, (i) {
30+
// Note the modulo is only there to deal with floating point imprecision
31+
// and ensure first == last.
32+
final angle = slice * (i % (points - 1)) + phase;
33+
return Offset(radius * math.cos(angle), radius * math.sin(angle));
34+
}, growable: false);
35+
}
36+
37+
// NOTE: to have a more prod like comparison, run with:
38+
// $ dart compile exe benchmark/crs.dart && ./benchmark/crs.exe
39+
//
40+
// If you run in JIT mode, the resulting execution times will be a lot more similar.
41+
Future<void> main() async {
42+
Logger.level = Level.all;
43+
Logger.defaultFilter = NoFilter.new;
44+
Logger.defaultPrinter = SimplePrinter.new;
45+
46+
final results = <Result>[];
47+
const N = 3000000;
48+
49+
final circle = makeCircle(1000, 1, 0);
50+
51+
results.add(await timedRun('In circle', () {
52+
const point = math.Point(0, 0);
53+
54+
bool yesPlease = true;
55+
for (int i = 0; i < N; ++i) {
56+
yesPlease = yesPlease && isPointInPolygon(point, circle);
57+
}
58+
59+
assert(yesPlease, 'should be in circle');
60+
return yesPlease;
61+
}));
62+
63+
results.add(await timedRun('Not in circle', () {
64+
const point = math.Point(4, 4);
65+
66+
bool noSir = false;
67+
for (int i = 0; i < N; ++i) {
68+
noSir = noSir || isPointInPolygon(point, circle);
69+
}
70+
71+
assert(!noSir, 'should not be in circle');
72+
return noSir;
73+
}));
74+
75+
Logger().i('Results:\n${results.map((r) => r.toString()).join('\n')}');
76+
}

lib/src/geo/crs.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'dart:math' as math hide Point;
22
import 'dart:math' show Point;
33

4-
import 'package:flutter_map/flutter_map.dart';
4+
import 'package:flutter_map/src/misc/bounds.dart';
55
import 'package:latlong2/latlong.dart';
66
import 'package:meta/meta.dart';
77
import 'package:proj4dart/proj4dart.dart' as proj4;

lib/src/layer/polygon_layer/painter.dart

+19-38
Original file line numberDiff line numberDiff line change
@@ -39,42 +39,41 @@ base class _PolygonPainter<R extends Object>
3939
required LatLng coordinate,
4040
}) {
4141
final polygon = projectedPolygon.polygon;
42-
43-
if (!polygon.boundingBox.contains(coordinate)) return false;
42+
if (!polygon.boundingBox.contains(coordinate)) {
43+
return false;
44+
}
4445

4546
final projectedCoords = getOffsetsXY(
4647
camera: camera,
4748
origin: hitTestCameraOrigin,
4849
points: projectedPolygon.points,
49-
).toList();
50+
);
5051

5152
if (projectedCoords.first != projectedCoords.last) {
5253
projectedCoords.add(projectedCoords.first);
5354
}
55+
final isInPolygon = isPointInPolygon(point, projectedCoords);
5456

5557
final hasHoles = projectedPolygon.holePoints.isNotEmpty;
56-
late final List<List<Offset>> projectedHoleCoords;
57-
if (hasHoles) {
58-
projectedHoleCoords = projectedPolygon.holePoints
59-
.map(
60-
(points) => getOffsetsXY(
58+
final isInHole = hasHoles &&
59+
() {
60+
for (final points in projectedPolygon.holePoints) {
61+
final projectedHoleCoords = getOffsetsXY(
6162
camera: camera,
6263
origin: hitTestCameraOrigin,
6364
points: points,
64-
).toList(),
65-
)
66-
.toList();
65+
);
6766

68-
if (projectedHoleCoords.firstOrNull != projectedHoleCoords.lastOrNull) {
69-
projectedHoleCoords.add(projectedHoleCoords.first);
70-
}
71-
}
67+
if (projectedHoleCoords.first != projectedHoleCoords.last) {
68+
projectedHoleCoords.add(projectedHoleCoords.first);
69+
}
7270

73-
final isInPolygon = _isPointInPolygon(point, projectedCoords);
74-
final isInHole = hasHoles &&
75-
projectedHoleCoords
76-
.map((c) => _isPointInPolygon(point, c))
77-
.any((e) => e);
71+
if (isPointInPolygon(point, projectedHoleCoords)) {
72+
return true;
73+
}
74+
}
75+
return false;
76+
}();
7877

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

364-
/// Checks whether point [p] is within the specified closed [polygon]
365-
///
366-
/// Uses the even-odd algorithm.
367-
static bool _isPointInPolygon(math.Point p, List<Offset> polygon) {
368-
bool isInPolygon = false;
369-
370-
for (int i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
371-
if ((((polygon[i].dy <= p.y) && (p.y < polygon[j].dy)) ||
372-
((polygon[j].dy <= p.y) && (p.y < polygon[i].dy))) &&
373-
(p.x <
374-
(polygon[j].dx - polygon[i].dx) *
375-
(p.y - polygon[i].dy) /
376-
(polygon[j].dy - polygon[i].dy) +
377-
polygon[i].dx)) isInPolygon = !isInPolygon;
378-
}
379-
return isInPolygon;
380-
}
381-
382363
@override
383364
bool shouldRepaint(_PolygonPainter<R> oldDelegate) =>
384365
polygons != oldDelegate.polygons ||

lib/src/layer/polygon_layer/polygon_layer.dart

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:flutter_map/flutter_map.dart';
99
import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart';
1010
import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart';
1111
import 'package:flutter_map/src/misc/offsets.dart';
12+
import 'package:flutter_map/src/misc/point_in_polygon.dart';
1213
import 'package:flutter_map/src/misc/simplify.dart';
1314
import 'package:latlong2/latlong.dart' hide Path;
1415
import 'package:polylabel/polylabel.dart';

lib/src/misc/offsets.dart

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ List<Offset> getOffsets(MapCamera camera, Offset origin, List<LatLng> points) {
2525

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

36-
final v = List<Offset>.filled(len, Offset.zero);
36+
final v = List<Offset>.filled(len, Offset.zero, growable: true);
3737
for (int i = 0; i < len; ++i) {
3838
final (x, y) = crs.latLngToXY(points[i], zoomScale);
3939
v[i] = Offset(x + ox, y + oy);
@@ -63,7 +63,7 @@ List<Offset> getOffsetsXY({
6363
// Optimization: monomorphize the CrsWithStaticTransformation-case to avoid
6464
// the virtual function overhead.
6565
if (crs case final CrsWithStaticTransformation crs) {
66-
final v = List<Offset>.filled(len, Offset.zero);
66+
final v = List<Offset>.filled(len, Offset.zero, growable: true);
6767
for (int i = 0; i < len; ++i) {
6868
final p = realPoints.elementAt(i);
6969
final (x, y) = crs.transform(p.x, p.y, zoomScale);
@@ -72,7 +72,7 @@ List<Offset> getOffsetsXY({
7272
return v;
7373
}
7474

75-
final v = List<Offset>.filled(len, Offset.zero);
75+
final v = List<Offset>.filled(len, Offset.zero, growable: true);
7676
for (int i = 0; i < len; ++i) {
7777
final p = realPoints.elementAt(i);
7878
final (x, y) = crs.transform(p.x, p.y, zoomScale);

lib/src/misc/point_in_polygon.dart

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'dart:math' as math;
2+
import 'dart:ui';
3+
4+
/// Checks whether point [p] is within the specified closed [polygon]
5+
///
6+
/// Uses the even-odd algorithm and requires closed loop polygons, i.e.
7+
/// `polygon.first == polygon.last`.
8+
bool isPointInPolygon(math.Point p, List<Offset> polygon) {
9+
final len = polygon.length;
10+
assert(len >= 3, 'not a polygon');
11+
assert(polygon.first == polygon.last, 'polygon not closed');
12+
final double px = p.x.toDouble();
13+
final double py = p.y.toDouble();
14+
15+
bool isInPolygon = false;
16+
for (int i = 0, j = len - 1; i < len; j = i++) {
17+
final double poIx = polygon[i].dx;
18+
final double poIy = polygon[i].dy;
19+
20+
final double poJx = polygon[j].dx;
21+
final double poJy = polygon[j].dy;
22+
23+
if ((((poIy <= py) && (py < poJy)) || ((poJy <= py) && (py < poIy))) &&
24+
(px < (poJx - poIx) * (py - poIy) / (poJy - poIy) + poIx)) {
25+
isInPolygon = !isInPolygon;
26+
}
27+
}
28+
return isInPolygon;
29+
}

test/misc/point_in_polygon_test.dart

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import 'dart:math' as math;
2+
3+
import 'package:flutter_map/src/misc/point_in_polygon.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
List<Offset> makeCircle(int points, double radius, double phase) {
7+
final slice = math.pi * 2 / (points - 1);
8+
return List.generate(points, (i) {
9+
// Note the modulo is only there to deal with floating point imprecision
10+
// and ensure first == last.
11+
final angle = slice * (i % (points - 1)) + phase;
12+
return Offset(radius * math.cos(angle), radius * math.sin(angle));
13+
}, growable: false);
14+
}
15+
16+
void main() {
17+
test('Smoke test for points in and out of polygons', () {
18+
final circle = makeCircle(100, 1, 0);
19+
20+
// Inside points
21+
for (final point in makeCircle(32, 0.8, 0.0001)) {
22+
final p = math.Point(point.dx, point.dy);
23+
expect(isPointInPolygon(p, circle), isTrue);
24+
}
25+
26+
// Edge-case: check origin
27+
expect(isPointInPolygon(const math.Point(0, 0), circle), isTrue);
28+
29+
// Outside points: small radius
30+
for (final point in makeCircle(32, 1.1, 0.0001)) {
31+
final p = math.Point(point.dx, point.dy);
32+
expect(isPointInPolygon(p, circle), isFalse);
33+
}
34+
35+
// Outside points: large radius
36+
for (final point in makeCircle(32, 100000, 0.0001)) {
37+
final p = math.Point(point.dx, point.dy);
38+
expect(isPointInPolygon(p, circle), isFalse);
39+
}
40+
});
41+
}

0 commit comments

Comments
 (0)