Skip to content

Commit 8b2b763

Browse files
committed
Add an onTap callback to polylines.
1 parent 670cb36 commit 8b2b763

File tree

2 files changed

+191
-25
lines changed

2 files changed

+191
-25
lines changed

example/lib/pages/polyline.dart

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,28 @@ class _PolylinePageState extends State<PolylinePage> {
2626
children: [
2727
openStreetMapTileLayer,
2828
PolylineLayer(
29+
interactive: true,
2930
polylines: [
3031
Polyline(
3132
points: [
3233
const LatLng(51.5, -0.09),
3334
const LatLng(53.3498, -6.2603),
3435
const LatLng(48.8566, 2.3522),
3536
],
36-
strokeWidth: 4,
37+
strokeWidth: 8,
3738
color: Colors.purple,
39+
onTap: (point) => openDialog('Purple line: $point'),
40+
),
41+
Polyline(
42+
points: [
43+
const LatLng(48.5, -3.09),
44+
const LatLng(47.3498, -9.2603),
45+
const LatLng(43.8566, -1.3522),
46+
],
47+
strokeWidth: 16000,
48+
color: Colors.pink,
49+
useStrokeWidthInMeter: true,
50+
onTap: (point) => openDialog('StrokeWidthInMetersLine: $point'),
3851
),
3952
Polyline(
4053
points: [
@@ -59,6 +72,7 @@ class _PolylinePageState extends State<PolylinePage> {
5972
color: Colors.blue.withOpacity(0.6),
6073
borderStrokeWidth: 20,
6174
borderColor: Colors.red.withOpacity(0.4),
75+
onTap: (point) => openDialog('blue line: $point'),
6276
),
6377
Polyline(
6478
points: [
@@ -70,6 +84,7 @@ class _PolylinePageState extends State<PolylinePage> {
7084
color: Colors.black.withOpacity(0.2),
7185
borderStrokeWidth: 20,
7286
borderColor: Colors.white30,
87+
onTap: (point) => openDialog('black/white line: $point'),
7388
),
7489
Polyline(
7590
points: [
@@ -99,4 +114,15 @@ class _PolylinePageState extends State<PolylinePage> {
99114
),
100115
);
101116
}
117+
118+
Future<void> openDialog(String message) => showDialog<void>(
119+
context: context,
120+
builder: (context) {
121+
return Dialog(
122+
child: Padding(
123+
padding: const EdgeInsets.all(8),
124+
child: Text(message),
125+
),
126+
);
127+
});
102128
}

lib/src/layer/polyline_layer.dart

Lines changed: 164 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:core';
2+
import 'dart:math' as math;
23
import 'dart:ui' as ui;
34

45
import '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
5872
class 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

Comments
 (0)