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,72 @@ 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+ // TODO: For efficiency we'd ideally filter by bounding box here. However
165+ // we'd need to compute an extended bounding box that accounts account for
166+ // the stroke width.
167+ // if (!p.boundingBox.contains(touch)) {
168+ // continue;
169+ // }
170+
171+ final offsets = getOffsets (origin, p.points);
172+ final strokeWidth = p.useStrokeWidthInMeter
173+ ? _metersToStrokeWidth (
174+ origin,
175+ p.points.first,
176+ offsets.first,
177+ p.strokeWidth,
178+ )
179+ : p.strokeWidth;
180+ final maxDistance = strokeWidth / 2 + p.borderStrokeWidth / 2 ;
181+
182+ for (int i = 0 ; i < offsets.length - 1 ; i++ ) {
183+ final o1 = offsets[i];
184+ final o2 = offsets[i + 1 ];
185+
186+ final distance = math.sqrt (_distToSegmentSquared (
187+ position.dx,
188+ position.dy,
189+ o1.dx,
190+ o1.dy,
191+ o2.dx,
192+ o2.dy,
193+ ));
194+
195+ if (distance < maxDistance) {
196+ // We break out of the loop after we find the top-most candidate
197+ // polyline. However, we only register a hit if this polyline is
198+ // tappable. This let's (by design) non-interactive polylines
199+ // occlude polylines beneath.
200+ if (p.onTap != null ) {
201+ hit = p;
202+ }
203+ break outer;
204+ }
205+ }
206+ }
207+
208+ if (hit != null ) {
209+ lastHit! .hit = _Hit (hit, touch);
210+ return true ;
211+ }
212+
213+ lastHit! .hit = null ;
214+ return false ;
215+ }
216+
115217 @override
116218 void paint (Canvas canvas, Size size) {
117219 final rect = Offset .zero & size;
@@ -169,16 +271,12 @@ class PolylinePainter extends CustomPainter {
169271
170272 late final double strokeWidth;
171273 if (polyline.useStrokeWidthInMeter) {
172- final firstPoint = polyline.points.first;
173- final firstOffset = offsets.first;
174- final r = const Distance (). offset (
175- firstPoint ,
274+ strokeWidth = _metersToStrokeWidth (
275+ origin,
276+ polyline.points.first,
277+ offsets.first ,
176278 polyline.strokeWidth,
177- 180 ,
178279 );
179- final delta = firstOffset - getOffset (origin, r);
180-
181- strokeWidth = delta.distance;
182280 } else {
183281 strokeWidth = polyline.strokeWidth;
184282 }
@@ -290,10 +388,52 @@ class PolylinePainter extends CustomPainter {
290388 .toList ();
291389 }
292390
391+ double _metersToStrokeWidth (
392+ Offset origin,
393+ LatLng p0,
394+ Offset o0,
395+ double strokeWidthInMeters,
396+ ) {
397+ final r = _distance.offset (
398+ p0,
399+ strokeWidthInMeters,
400+ 180 ,
401+ );
402+ final delta = o0 - getOffset (origin, r);
403+ return delta.distance;
404+ }
405+
293406 @override
294- bool shouldRepaint (PolylinePainter oldDelegate) {
407+ bool shouldRepaint (_PolylinePainter oldDelegate) {
295408 return oldDelegate.bounds != bounds ||
296409 oldDelegate.polylines.length != polylines.length ||
297410 oldDelegate.hash != hash;
298411 }
299412}
413+
414+ double _distanceSq (double x0, double y0, double x1, double y1) {
415+ final dx = x0 - x1;
416+ final dy = y0 - y1;
417+ return dx * dx + dy * dy;
418+ }
419+
420+ double _distToSegmentSquared (
421+ double px,
422+ double py,
423+ double x0,
424+ double y0,
425+ double x1,
426+ double y1,
427+ ) {
428+ final dx = x1 - x0;
429+ final dy = y1 - y0;
430+ final distanceSq = dx * dx + dy * dy;
431+ if (distanceSq == 0 ) {
432+ return _distanceSq (px, py, x0, y0);
433+ }
434+
435+ final t = (((px - x0) * dx + (py - y0) * dy) / distanceSq).clamp (0 , 1 );
436+ return _distanceSq (px, py, x0 + t * dx, y0 + t * dy);
437+ }
438+
439+ const _distance = Distance ();
0 commit comments