11import 'dart:core' ;
2+ import 'dart:math' as math;
23import 'dart:ui' as ui;
34
45import 'package:flutter/widgets.dart' ;
@@ -20,6 +21,7 @@ class Polyline {
2021 final StrokeCap strokeCap;
2122 final StrokeJoin strokeJoin;
2223 final bool useStrokeWidthInMeter;
24+ final void Function (LatLng point)? onTap;
2325
2426 LatLngBounds ? _boundingBox;
2527
@@ -38,6 +40,7 @@ class Polyline {
3840 this .strokeCap = StrokeCap .round,
3941 this .strokeJoin = StrokeJoin .round,
4042 this .useStrokeWidthInMeter = false ,
43+ this .onTap,
4144 });
4245
4346 /// Used to batch draw calls to the canvas.
@@ -54,45 +57,78 @@ class Polyline {
5457 useStrokeWidthInMeter);
5558}
5659
60+ class _Hit {
61+ final Polyline polyline;
62+ final LatLng point;
63+
64+ const _Hit (this .polyline, this .point);
65+ }
66+
67+ class _LastHit {
68+ _Hit ? hit;
69+ }
70+
5771@immutable
5872class PolylineLayer extends StatelessWidget {
5973 final List <Polyline > polylines;
60- final bool polylineCulling ;
74+ final bool interactive ;
6175
6276 const PolylineLayer ({
6377 super .key,
6478 required this .polylines,
65- this .polylineCulling = false ,
79+ //@Deprecated('Let's always cull')
80+ bool polylineCulling = true ,
81+ this .interactive = false ,
6682 });
6783
6884 @override
6985 Widget build (BuildContext context) {
7086 final map = MapCamera .of (context);
7187
88+ final lastHit = _LastHit ();
89+ final paint = CustomPaint (
90+ painter: _PolylinePainter (
91+ polylines
92+ .where ((p) => p.boundingBox.isOverlapping (map.visibleBounds))
93+ .toList (),
94+ map,
95+ interactive ? lastHit : null ,
96+ ),
97+ size: Size (map.size.x, map.size.y),
98+ isComplex: true ,
99+ );
100+
101+ if (! interactive) {
102+ return MobileLayerTransformer (child: paint);
103+ }
104+
72105 return MobileLayerTransformer (
73- child: CustomPaint (
74- painter: PolylinePainter (
75- polylineCulling
76- ? polylines
77- .where ((p) => p.boundingBox.isOverlapping (map.visibleBounds))
78- .toList ()
79- : polylines,
80- map,
81- ),
82- size: Size (map.size.x, map.size.y),
83- isComplex: true ,
106+ child: GestureDetector (
107+ behavior: HitTestBehavior .deferToChild,
108+ onTap: () {
109+ final hit = lastHit.hit;
110+ if (hit == null ) return ;
111+
112+ final onTap = hit.polyline.onTap;
113+ if (onTap != null ) {
114+ onTap (hit.point);
115+ }
116+ },
117+ child: paint,
84118 ),
85119 );
86120 }
87121}
88122
89- class PolylinePainter extends CustomPainter {
123+ class _PolylinePainter extends CustomPainter {
90124 final List <Polyline > polylines;
91125
92126 final MapCamera map;
93127 final LatLngBounds bounds;
128+ final _LastHit ? lastHit;
94129
95- PolylinePainter (this .polylines, this .map) : bounds = map.visibleBounds;
130+ _PolylinePainter (this .polylines, this .map, this .lastHit)
131+ : bounds = map.visibleBounds;
96132
97133 int get hash => _hash ?? = Object .hashAll (polylines);
98134
@@ -112,6 +148,59 @@ class PolylinePainter extends CustomPainter {
112148 );
113149 }
114150
151+ @override
152+ bool ? hitTest (Offset position) {
153+ if (lastHit == null ) {
154+ return null ;
155+ }
156+
157+ final touch = map.pointToLatLng (math.Point (position.dx, position.dy));
158+ final origin = map.project (map.center).toOffset () - map.size.toOffset () / 2 ;
159+
160+ Polyline ? hit;
161+
162+ outer:
163+ for (final p in polylines.reversed) {
164+ if (! p.boundingBox.contains (touch)) {
165+ continue ;
166+ }
167+
168+ final offsets = getOffsets (origin, p.points);
169+ for (int i = 0 ; i < offsets.length - 1 ; i++ ) {
170+ final o1 = offsets[i];
171+ final o2 = offsets[i + 1 ];
172+
173+ final distance = math.sqrt (_distToSegmentSquared (
174+ position.dx,
175+ position.dy,
176+ o1.dx,
177+ o1.dy,
178+ o2.dx,
179+ o2.dy,
180+ ));
181+
182+ if (distance < p.strokeWidth) {
183+ // We break out of the loop after we find the top-most candidate
184+ // polyline. However, we only register a hit if this polyline is
185+ // tappable. This let's (by design) non-interactive polylines
186+ // occlude polylines beneath.
187+ if (p.onTap != null ) {
188+ hit = p;
189+ }
190+ break outer;
191+ }
192+ }
193+ }
194+
195+ if (hit != null ) {
196+ lastHit! .hit = _Hit (hit, touch);
197+ return true ;
198+ }
199+
200+ lastHit! .hit = null ;
201+ return false ;
202+ }
203+
115204 @override
116205 void paint (Canvas canvas, Size size) {
117206 final rect = Offset .zero & size;
@@ -291,9 +380,28 @@ class PolylinePainter extends CustomPainter {
291380 }
292381
293382 @override
294- bool shouldRepaint (PolylinePainter oldDelegate) {
383+ bool shouldRepaint (_PolylinePainter oldDelegate) {
295384 return oldDelegate.bounds != bounds ||
296385 oldDelegate.polylines.length != polylines.length ||
297386 oldDelegate.hash != hash;
298387 }
299388}
389+
390+ double _distanceSq (double x0, double y0, double x1, double y1) {
391+ final dx = x0 - x1;
392+ final dy = y0 - y1;
393+ return dx * dx + dy * dy;
394+ }
395+
396+ double _distToSegmentSquared (
397+ double px, double py, double x0, double y0, double x1, double y1) {
398+ final dx = x1 - x0;
399+ final dy = y1 - y0;
400+ final distanceSq = dx * dx + dy * dy;
401+ if (distanceSq == 0 ) {
402+ return _distanceSq (px, py, x0, y0);
403+ }
404+
405+ final t = (((px - x0) * dx + (py - y0) * dy) / distanceSq).clamp (0 , 1 );
406+ return _distanceSq (px, py, x0 + t * dx, y0 + t * dy);
407+ }
0 commit comments