diff --git a/benchmark/point_in_polygon.dart b/benchmark/point_in_polygon.dart new file mode 100644 index 000000000..e6b89d9c4 --- /dev/null +++ b/benchmark/point_in_polygon.dart @@ -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 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 main() async { + Logger.level = Level.all; + Logger.defaultFilter = NoFilter.new; + Logger.defaultPrinter = SimplePrinter.new; + + final results = []; + 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')}'); +} diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index 9b97aa1c5..63b5e1936 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -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; diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 315fe0402..111d4d38d 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -70,11 +70,9 @@ base class _PolygonPainter } } - final isInPolygon = _isPointInPolygon(point, projectedCoords); + final isInPolygon = pointInPolygon(point, projectedCoords); final isInHole = hasHoles && - projectedHoleCoords - .map((c) => _isPointInPolygon(point, c)) - .any((e) => e); + projectedHoleCoords.map((c) => pointInPolygon(point, c)).any((e) => e); // Second check handles case where polygon outline intersects a hole, // ensuring that the hit matches with the visual representation @@ -361,24 +359,6 @@ base class _PolygonPainter ); } - /// Checks whether point [p] is within the specified closed [polygon] - /// - /// Uses the even-odd algorithm. - static bool _isPointInPolygon(math.Point p, List 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 oldDelegate) => polygons != oldDelegate.polygons || diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index ce20d0c99..ac959ed10 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -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'; diff --git a/lib/src/misc/point_in_polygon.dart b/lib/src/misc/point_in_polygon.dart new file mode 100644 index 000000000..0d0974cd8 --- /dev/null +++ b/lib/src/misc/point_in_polygon.dart @@ -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 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; +}