diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 5acc40753..8423c5060 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -39,6 +39,7 @@ class _PolylinePageState extends State { userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), PolylineLayer( + interactive: true, polylines: [ Polyline( points: [ @@ -72,6 +73,18 @@ class _PolylinePageState extends State { color: Colors.blue.withOpacity(0.6), borderStrokeWidth: 20, borderColor: Colors.red.withOpacity(0.4), + onTap: (LatLng point) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + child: Padding( + padding: const EdgeInsets.all(8), + child: Text('clicked $point'), + ), + ); + }); + }, ), Polyline( points: [ diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index b20de99f1..89d7b3989 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -1,4 +1,5 @@ import 'dart:core'; +import 'dart:math' as math; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; @@ -20,6 +21,7 @@ class Polyline { final StrokeCap strokeCap; final StrokeJoin strokeJoin; final bool useStrokeWidthInMeter; + final void Function(LatLng point)? onTap; LatLngBounds? _boundingBox; @@ -38,6 +40,7 @@ class Polyline { this.strokeCap = StrokeCap.round, this.strokeJoin = StrokeJoin.round, this.useStrokeWidthInMeter = false, + this.onTap, }); /// Used to batch draw calls to the canvas. @@ -54,45 +57,78 @@ class Polyline { useStrokeWidthInMeter); } +class _Hit { + final Polyline polyline; + final LatLng point; + + const _Hit(this.polyline, this.point); +} + +class _LastHit { + _Hit? hit; +} + @immutable class PolylineLayer extends StatelessWidget { final List polylines; - final bool polylineCulling; + final bool interactive; const PolylineLayer({ super.key, required this.polylines, - this.polylineCulling = false, + //@Deprecated('Let's always cull') + bool polylineCulling = true, + this.interactive = false, }); @override Widget build(BuildContext context) { final map = MapCamera.of(context); + final lastHit = _LastHit(); + final paint = CustomPaint( + painter: _PolylinePainter( + polylines + .where((p) => p.boundingBox.isOverlapping(map.visibleBounds)) + .toList(), + map, + interactive ? lastHit : null, + ), + size: Size(map.size.x, map.size.y), + isComplex: true, + ); + + if (!interactive) { + return MobileLayerTransformer(child: paint); + } + return MobileLayerTransformer( - child: CustomPaint( - painter: PolylinePainter( - polylineCulling - ? polylines - .where((p) => p.boundingBox.isOverlapping(map.visibleBounds)) - .toList() - : polylines, - map, - ), - size: Size(map.size.x, map.size.y), - isComplex: true, + child: GestureDetector( + behavior: HitTestBehavior.deferToChild, + onTap: () { + final hit = lastHit.hit; + if (hit == null) return; + + final onTap = hit.polyline.onTap; + if (onTap != null) { + onTap(hit.point); + } + }, + child: paint, ), ); } } -class PolylinePainter extends CustomPainter { +class _PolylinePainter extends CustomPainter { final List polylines; final MapCamera map; final LatLngBounds bounds; + final _LastHit? lastHit; - PolylinePainter(this.polylines, this.map) : bounds = map.visibleBounds; + _PolylinePainter(this.polylines, this.map, this.lastHit) + : bounds = map.visibleBounds; int get hash => _hash ??= Object.hashAll(polylines); @@ -112,6 +148,56 @@ class PolylinePainter extends CustomPainter { ); } + @override + bool? hitTest(Offset position) { + if (lastHit == null) { + return null; + } + + final hit = map.pointToLatLng(math.Point(position.dx, position.dy)); + final origin = map.project(map.center).toOffset() - map.size.toOffset() / 2; + + final candidates = []; + + outer: + for (final p in polylines) { + if (p.onTap == null) { + continue; + } + + if (!p.boundingBox.contains(hit)) { + continue; + } + + final offsets = getOffsets(origin, p.points); + for (int i = 0; i < offsets.length - 1; i++) { + final o1 = offsets[i]; + final o2 = offsets[i + 1]; + + final distance = math.sqrt(_distToSegmentSquared( + position.dx, + position.dy, + o1.dx, + o1.dy, + o2.dx, + o2.dy, + )); + if (distance < p.strokeWidth) { + candidates.add(p); + continue outer; + } + } + } + + if (candidates.isNotEmpty) { + lastHit!.hit = _Hit(candidates.last, hit); + return true; + } + + lastHit!.hit = null; + return false; + } + @override void paint(Canvas canvas, Size size) { final rect = Offset.zero & size; @@ -291,9 +377,28 @@ class PolylinePainter extends CustomPainter { } @override - bool shouldRepaint(PolylinePainter oldDelegate) { + bool shouldRepaint(_PolylinePainter oldDelegate) { return oldDelegate.bounds != bounds || oldDelegate.polylines.length != polylines.length || oldDelegate.hash != hash; } } + +double _distanceSq(double x0, double y0, double x1, double y1) { + final dx = x0 - x1; + final dy = y0 - y1; + return dx * dx + dy * dy; +} + +double _distToSegmentSquared( + double px, double py, double x0, double y0, double x1, double y1) { + final dx = x1 - x0; + final dy = y1 - y0; + final distanceSq = dx * dx + dy * dy; + if (distanceSq == 0) { + return _distanceSq(px, py, x0, y0); + } + + final t = (((px - x0) * dx + (py - y0) * dy) / distanceSq).clamp(0, 1); + return _distanceSq(px, py, x0 + t * dx, y0 + t * dy); +}