Skip to content

Commit

Permalink
Make isPointInPolygon 40% faster (at least in JIT mode).
Browse files Browse the repository at this point in the history
I mostly looked at the function because I wanted to use it myself but it
was private. So I thought, if I expose it then I could at least put a
ribbon on it by adding a benchmark.

Before:

  (duration: 0:00:05.998949, name: In circle)
  (duration: 0:00:06.866919, name: Not in circle)

After:

  (duration: 0:00:03.649496, name: In circle)
  (duration: 0:00:04.611599, name: Not in circle)

Note, I opportunistically touched crs to remove the dart:ui dependency.
This way it can be compiled with dart (w/o flutter) rendering the
instructions in the benchmark correct again. Unfortunately,
pointInPolygon is not so fortunate and needs flutter due to
the dependency on ui.Offset. That's why I could only run it with
"flutter test" in JIT mode.
  • Loading branch information
ignatz committed Jun 5, 2024
1 parent b69a0d7 commit e98ca3b
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 21 deletions.
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:ui';
import 'dart:math' as math;

import 'package:flutter_map/src/misc/point_in_polygon.dart';
import 'package:latlong2/latlong.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);
}

// 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;
const POINTS = 1000;

results.add(await timedRun('In circle', () {
final polygon = List.generate(POINTS, (i) {
final angle = math.pi * 2 / POINTS * i;
return Offset(math.cos(angle), math.sin(angle));
});

const point = math.Point(0, 0);

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

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

results.add(await timedRun('Not in circle', () {
final polygon = List.generate(POINTS, (i) {
final angle = math.pi * 2 / POINTS * i;
return Offset(math.cos(angle), math.sin(angle));
});

const point = math.Point(4, 4);

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

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
22 changes: 2 additions & 20 deletions lib/src/layer/polygon_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ base class _PolygonPainter<R extends Object>
}
}

final isInPolygon = _isPointInPolygon(point, projectedCoords);
final isInPolygon = pointInPolygon(point, projectedCoords);
final isInHole = hasHoles &&
projectedHoleCoords
.map((c) => _isPointInPolygon(point, c))
.map((c) => pointInPolygon(point, c))
.any((e) => e);

// Second check handles case where polygon outline intersects a hole,
Expand Down Expand Up @@ -361,24 +361,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
25 changes: 25 additions & 0 deletions lib/src/misc/point_in_polygon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'dart:math' as math;
import 'dart:ui';

/// Checks whether point [p] is within the specified closed [polygon]
///
/// Uses the even-odd algorithm.
bool pointInPolygon(math.Point p, List<Offset> polygon) {
final double px = p.x.toDouble();
final double py = p.y.toDouble();

bool isInPolygon = false;
for (int i = 0, j = polygon.length - 1; i < polygon.length; 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;
}

0 comments on commit e98ca3b

Please sign in to comment.