From 8b2b76362d058ffa30d43719a67b0248f33a5a7e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 9 Nov 2023 13:55:06 +0100 Subject: [PATCH 01/17] Add an onTap callback to polylines. --- example/lib/pages/polyline.dart | 28 ++++- lib/src/layer/polyline_layer.dart | 188 ++++++++++++++++++++++++++---- 2 files changed, 191 insertions(+), 25 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 9cecc5fb8..5ce2e2c58 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -26,6 +26,7 @@ class _PolylinePageState extends State { children: [ openStreetMapTileLayer, PolylineLayer( + interactive: true, polylines: [ Polyline( points: [ @@ -33,8 +34,20 @@ class _PolylinePageState extends State { const LatLng(53.3498, -6.2603), const LatLng(48.8566, 2.3522), ], - strokeWidth: 4, + strokeWidth: 8, color: Colors.purple, + onTap: (point) => openDialog('Purple line: $point'), + ), + Polyline( + points: [ + const LatLng(48.5, -3.09), + const LatLng(47.3498, -9.2603), + const LatLng(43.8566, -1.3522), + ], + strokeWidth: 16000, + color: Colors.pink, + useStrokeWidthInMeter: true, + onTap: (point) => openDialog('StrokeWidthInMetersLine: $point'), ), Polyline( points: [ @@ -59,6 +72,7 @@ class _PolylinePageState extends State { color: Colors.blue.withOpacity(0.6), borderStrokeWidth: 20, borderColor: Colors.red.withOpacity(0.4), + onTap: (point) => openDialog('blue line: $point'), ), Polyline( points: [ @@ -70,6 +84,7 @@ class _PolylinePageState extends State { color: Colors.black.withOpacity(0.2), borderStrokeWidth: 20, borderColor: Colors.white30, + onTap: (point) => openDialog('black/white line: $point'), ), Polyline( points: [ @@ -99,4 +114,15 @@ class _PolylinePageState extends State { ), ); } + + Future openDialog(String message) => showDialog( + context: context, + builder: (context) { + return Dialog( + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(message), + ), + ); + }); } diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index b20de99f1..cb2815016 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,72 @@ class PolylinePainter extends CustomPainter { ); } + @override + bool? hitTest(Offset position) { + if (lastHit == null) { + return null; + } + + final touch = map.pointToLatLng(math.Point(position.dx, position.dy)); + final origin = map.project(map.center).toOffset() - map.size.toOffset() / 2; + + Polyline? hit; + + outer: + for (final p in polylines.reversed) { + // TODO: For efficiency we'd ideally filter by bounding box here. However + // we'd need to compute an extended bounding box that accounts account for + // the stroke width. + // if (!p.boundingBox.contains(touch)) { + // continue; + // } + + final offsets = getOffsets(origin, p.points); + final strokeWidth = p.useStrokeWidthInMeter + ? _metersToStrokeWidth( + origin, + p.points.first, + offsets.first, + p.strokeWidth, + ) + : p.strokeWidth; + final maxDistance = strokeWidth / 2 + p.borderStrokeWidth / 2; + + 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 < maxDistance) { + // We break out of the loop after we find the top-most candidate + // polyline. However, we only register a hit if this polyline is + // tappable. This let's (by design) non-interactive polylines + // occlude polylines beneath. + if (p.onTap != null) { + hit = p; + } + break outer; + } + } + } + + if (hit != null) { + lastHit!.hit = _Hit(hit, touch); + return true; + } + + lastHit!.hit = null; + return false; + } + @override void paint(Canvas canvas, Size size) { final rect = Offset.zero & size; @@ -169,16 +271,12 @@ class PolylinePainter extends CustomPainter { late final double strokeWidth; if (polyline.useStrokeWidthInMeter) { - final firstPoint = polyline.points.first; - final firstOffset = offsets.first; - final r = const Distance().offset( - firstPoint, + strokeWidth = _metersToStrokeWidth( + origin, + polyline.points.first, + offsets.first, polyline.strokeWidth, - 180, ); - final delta = firstOffset - getOffset(origin, r); - - strokeWidth = delta.distance; } else { strokeWidth = polyline.strokeWidth; } @@ -290,10 +388,52 @@ class PolylinePainter extends CustomPainter { .toList(); } + double _metersToStrokeWidth( + Offset origin, + LatLng p0, + Offset o0, + double strokeWidthInMeters, + ) { + final r = _distance.offset( + p0, + strokeWidthInMeters, + 180, + ); + final delta = o0 - getOffset(origin, r); + return delta.distance; + } + @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); +} + +const _distance = Distance(); From b55d499462f6d82a21e5be47b8b3765ec6baa754 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 6 Dec 2023 20:38:38 +0000 Subject: [PATCH 02/17] Remove `PolylineLayer.interactive` argument, and calculate it automatically internally Replace `_Hit` with a `Record` Improve code style --- example/lib/pages/polyline.dart | 1 - lib/src/layer/polyline_layer.dart | 103 +++++++++++------------------- 2 files changed, 38 insertions(+), 66 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 9b8f6d766..2aaeee369 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -27,7 +27,6 @@ class _PolylinePageState extends State { children: [ openStreetMapTileLayer, PolylineLayer( - interactive: true, polylines: [ Polyline( points: [ diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 86513c9db..eee6a41eb 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -46,77 +46,63 @@ class Polyline { /// Used to batch draw calls to the canvas. int get renderHashCode => Object.hash( - strokeWidth, - color, - borderStrokeWidth, - borderColor, - gradientColors, - colorsStop, - isDotted, - strokeCap, - strokeJoin, - useStrokeWidthInMeter); -} - -class _Hit { - final Polyline polyline; - final LatLng point; - - const _Hit(this.polyline, this.point); + strokeWidth, + color, + borderStrokeWidth, + borderColor, + gradientColors, + colorsStop, + isDotted, + strokeCap, + strokeJoin, + useStrokeWidthInMeter, + ); } class _LastHit { - _Hit? hit; + ({Polyline polyline, LatLng tapPoint})? hit; } @immutable class PolylineLayer extends StatelessWidget { final List polylines; - final bool interactive; const PolylineLayer({ super.key, required this.polylines, - //@Deprecated('Let's always cull') + // TODO: Remove once PR #1704 is merged bool polylineCulling = true, - this.interactive = false, }); @override Widget build(BuildContext context) { final map = MapCamera.of(context); + final culledPolylines = []; + bool interactive = false; + for (final line in polylines) { + if (!line.boundingBox.isOverlapping(map.visibleBounds)) continue; + if (!interactive && line.onTap != null) interactive = true; + culledPolylines.add(line); + } + final lastHit = _LastHit(); final paint = CustomPaint( - painter: _PolylinePainter( - polylines - .where((p) => p.boundingBox.isOverlapping(map.visibleBounds)) - .toList(), - map, - interactive ? lastHit : null, - ), + painter: + _PolylinePainter(culledPolylines, map, interactive ? lastHit : null), size: Size(map.size.x, map.size.y), isComplex: true, ); - if (!interactive) { - return MobileLayerTransformer(child: paint); - } - return MobileLayerTransformer( - 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, - ), + child: interactive + ? GestureDetector( + behavior: HitTestBehavior.deferToChild, + onTap: () => + lastHit.hit?.polyline.onTap?.call(lastHit.hit!.tapPoint), + child: paint, + ) + : paint, ); } } @@ -137,9 +123,7 @@ class _PolylinePainter extends CustomPainter { @override bool? hitTest(Offset position) { - if (lastHit == null) { - return null; - } + if (lastHit == null) return null; final touch = map.pointToLatLng(math.Point(position.dx, position.dy)); final origin = map.project(map.center).toOffset() - map.size.toOffset() / 2; @@ -155,7 +139,7 @@ class _PolylinePainter extends CustomPainter { // continue; // } - final offsets = getOffsets(origin, p.points); + final offsets = getOffsets(map, origin, p.points); final strokeWidth = p.useStrokeWidthInMeter ? _metersToStrokeWidth( origin, @@ -184,21 +168,14 @@ class _PolylinePainter extends CustomPainter { // polyline. However, we only register a hit if this polyline is // tappable. This let's (by design) non-interactive polylines // occlude polylines beneath. - if (p.onTap != null) { - hit = p; - } + if (p.onTap != null) hit = p; break outer; } } } - if (hit != null) { - lastHit!.hit = _Hit(hit, touch); - return true; - } - - lastHit!.hit = null; - return false; + lastHit!.hit = hit != null ? (polyline: hit, tapPoint: touch) : null; + return hit != null; } @override @@ -381,12 +358,8 @@ class _PolylinePainter extends CustomPainter { Offset o0, double strokeWidthInMeters, ) { - final r = _distance.offset( - p0, - strokeWidthInMeters, - 180, - ); - final delta = o0 - getOffset(origin, r); + final r = _distance.offset(p0, strokeWidthInMeters, 180); + final delta = o0 - getOffset(map, origin, r); return delta.distance; } From f75abfc04604c8ef932c220cee951bb76227d4a5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 6 Dec 2023 22:18:59 +0000 Subject: [PATCH 03/17] Added `onTapTolerance` --- lib/src/layer/polyline_layer.dart | 38 +++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index eee6a41eb..bc1bafd24 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -10,6 +10,10 @@ import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:latlong2/latlong.dart'; +class _LastHit { + ({Polyline polyline, LatLng tapPoint})? hit; +} + class Polyline { final List points; final double strokeWidth; @@ -59,17 +63,20 @@ class Polyline { ); } -class _LastHit { - ({Polyline polyline, LatLng tapPoint})? hit; -} - @immutable class PolylineLayer extends StatelessWidget { final List polylines; + /// The number of pixels away from the visual line (including any width and + /// outline) in which a tap should still register as a tap on the line + /// + /// Defaults to 5. + final double onTapTolerance; + const PolylineLayer({ super.key, required this.polylines, + this.onTapTolerance = 5, // TODO: Remove once PR #1704 is merged bool polylineCulling = true, }); @@ -88,8 +95,12 @@ class PolylineLayer extends StatelessWidget { final lastHit = _LastHit(); final paint = CustomPaint( - painter: - _PolylinePainter(culledPolylines, map, interactive ? lastHit : null), + painter: _PolylinePainter( + culledPolylines, + map, + interactive ? lastHit : null, + onTapTolerance, + ), size: Size(map.size.x, map.size.y), isComplex: true, ); @@ -114,8 +125,14 @@ class _PolylinePainter extends CustomPainter { final LatLngBounds bounds; final _LastHit? lastHit; - _PolylinePainter(this.polylines, this.map, this.lastHit) - : bounds = map.visibleBounds; + final double onTapTolerance; + + _PolylinePainter( + this.polylines, + this.map, + this.lastHit, + this.onTapTolerance, + ) : bounds = map.visibleBounds; int get hash => _hash ??= Object.hashAll(polylines); @@ -148,7 +165,8 @@ class _PolylinePainter extends CustomPainter { p.strokeWidth, ) : p.strokeWidth; - final maxDistance = strokeWidth / 2 + p.borderStrokeWidth / 2; + final maxDistance = + (strokeWidth / 2 + p.borderStrokeWidth / 2) + onTapTolerance; for (int i = 0; i < offsets.length - 1; i++) { final o1 = offsets[i]; @@ -166,7 +184,7 @@ class _PolylinePainter extends CustomPainter { if (distance < maxDistance) { // We break out of the loop after we find the top-most candidate // polyline. However, we only register a hit if this polyline is - // tappable. This let's (by design) non-interactive polylines + // tappable. This lets (by design) non-interactive polylines // occlude polylines beneath. if (p.onTap != null) hit = p; break outer; From bd7dd5e02304c5b43a7e1ffb661712cf6b94cc0e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 7 Dec 2023 20:40:51 +0000 Subject: [PATCH 04/17] Add handling for taps on multiple `Polyline`s Add `PolylineLayer` level `onTap` callback Improved polyline example page --- example/lib/pages/polyline.dart | 109 +++++++++++++----- lib/src/layer/polyline_layer.dart | 176 +++++++++++++++++++++--------- 2 files changed, 209 insertions(+), 76 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 2aaeee369..3456a76a2 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -13,6 +13,8 @@ class PolylinePage extends StatefulWidget { State createState() => _PolylinePageState(); } +typedef _TapKeyType = ({String title, String subtitle}); + class _PolylinePageState extends State { @override Widget build(BuildContext context) { @@ -26,7 +28,11 @@ class _PolylinePageState extends State { ), children: [ openStreetMapTileLayer, - PolylineLayer( + PolylineLayer<_TapKeyType>( + onTap: (tappedLineKeys, coords) => _openTappedLinesModal( + tappedLineKeys, + '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', + ), polylines: [ Polyline( points: [ @@ -36,7 +42,10 @@ class _PolylinePageState extends State { ], strokeWidth: 8, color: Colors.purple, - onTap: (point) => openDialog('Purple line: $point'), + tapKey: ( + title: 'Purple Line', + subtitle: 'Nothing really special here...', + ), ), Polyline( points: [ @@ -47,7 +56,10 @@ class _PolylinePageState extends State { strokeWidth: 16000, color: Colors.pink, useStrokeWidthInMeter: true, - onTap: (point) => openDialog('StrokeWidthInMetersLine: $point'), + tapKey: ( + title: 'Pink Line', + subtitle: 'Fixed radius in meters instead of pixels', + ), ), Polyline( points: [ @@ -61,6 +73,10 @@ class _PolylinePageState extends State { const Color(0xffFEED00), const Color(0xff007E2D), ], + tapKey: ( + title: 'Traffic Light Line', + subtitle: 'Fancy gradient instead of a solid color', + ), ), Polyline( points: [ @@ -72,7 +88,11 @@ class _PolylinePageState extends State { color: Colors.blue.withOpacity(0.6), borderStrokeWidth: 20, borderColor: Colors.red.withOpacity(0.4), - onTap: (point) => openDialog('blue line: $point'), + tapKey: ( + title: 'BlueRed Line', + subtitle: + 'Solid translucent color fill, with different color outline', + ), ), Polyline( points: [ @@ -84,7 +104,11 @@ class _PolylinePageState extends State { color: Colors.black.withOpacity(0.2), borderStrokeWidth: 20, borderColor: Colors.white30, - onTap: (point) => openDialog('black/white line: $point'), + tapKey: ( + title: 'BlackWhite Line', + subtitle: + 'Solid translucent color fill, with different color outline', + ), ), Polyline( points: [ @@ -96,17 +120,11 @@ class _PolylinePageState extends State { color: Colors.yellow, borderStrokeWidth: 10, borderColor: Colors.blue.withOpacity(0.5), - ), - Polyline( - points: [ - const LatLng(48.1, -0.03), - const LatLng(50.5, -7.8), - const LatLng(56.5, 0.4), - ], - strokeWidth: 10, - color: Colors.amber, - borderStrokeWidth: 10, - borderColor: Colors.blue.withOpacity(0.5), + tapKey: ( + title: 'YellowBlue Line', + subtitle: + 'Solid translucent color fill, with different color outline', + ), ), ], ), @@ -115,14 +133,55 @@ class _PolylinePageState extends State { ); } - Future openDialog(String message) => showDialog( + void _openTappedLinesModal( + List<_TapKeyType> tappedLineKeys, + String coords, + ) { + showModalBottomSheet( context: context, - builder: (context) { - return Dialog( - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(message), - ), - ); - }); + builder: (context) => Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Tapped Polyline(s)', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + Text('Tapped at point: $coords'), + const SizedBox(height: 8), + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + final tappedLineKey = tappedLineKeys.elementAt(index); + return ListTile( + leading: index == 0 + ? const Icon(Icons.vertical_align_top) + : index == tappedLineKeys.length - 1 + ? const Icon(Icons.vertical_align_bottom) + : const SizedBox.shrink(), + title: Text(tappedLineKey.title), + subtitle: Text(tappedLineKey.subtitle), + dense: true, + ); + }, + itemCount: tappedLineKeys.length, + ), + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ), + ), + ], + ), + ), + ); + } } diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index bc1bafd24..fdb0dad31 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -2,6 +2,7 @@ import 'dart:core'; import 'dart:math' as math; import 'dart:ui' as ui; +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; @@ -10,11 +11,14 @@ import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:latlong2/latlong.dart'; -class _LastHit { - ({Polyline polyline, LatLng tapPoint})? hit; +class _LastHit { + ({List> tapped, LatLng point})? hit; } -class Polyline { +typedef PolylineOnTap = void Function(LatLng point); + +@optionalTypeArgs +class Polyline { final List points; final double strokeWidth; final Color color; @@ -26,7 +30,8 @@ class Polyline { final StrokeCap strokeCap; final StrokeJoin strokeJoin; final bool useStrokeWidthInMeter; - final void Function(LatLng point)? onTap; + final PolylineOnTap? onTap; + final TapKeyType? tapKey; LatLngBounds? _boundingBox; @@ -46,6 +51,7 @@ class Polyline { this.strokeJoin = StrokeJoin.round, this.useStrokeWidthInMeter = false, this.onTap, + this.tapKey, }); /// Used to batch draw calls to the canvas. @@ -64,75 +70,139 @@ class Polyline { } @immutable -class PolylineLayer extends StatelessWidget { - final List polylines; +@optionalTypeArgs +class PolylineLayer extends StatelessWidget { + final List> polylines; + + /// Called when a tap is detected on any [Polyline] with a defined + /// [Polyline.tapKey] + /// + /// Individual [Polyline]s may have their own [Polyline.onTap] callback + /// defined, regardless of whether this is defined. + /// + /// See [nonTappablesObscure] and [tappablesObscure] to set behaviour when a + /// tap is over multiple overlapping [Polyline]s. + final void Function(List tappedKeys, LatLng point)? onTap; /// The number of pixels away from the visual line (including any width and /// outline) in which a tap should still register as a tap on the line /// - /// Defaults to 5. + /// Applies to both [onTap] and every [Polyline.onTap]. + /// + /// Defaults to 3. final double onTapTolerance; + /// Whether a non-tappable [Polyline] should prevent taps from being handled + /// on all [Polyline]s beneath it, at overlaps + /// + /// "Tappable" means that either or both of [Polyline.onTap] and + /// [Polyline.tapKey] has been defined. + /// + /// Applies to both [onTap] and every [Polyline.onTap]. + /// + /// Defaults to `true`. + final bool nonTappablesObscure; + + /// Whether a tappable [Polyline] should prevent taps from being handled + /// on all [Polyline]s beneath it, at overlaps + /// + /// "Tappable" means that either or both of [Polyline.onTap] and + /// [Polyline.tapKey] has been defined. + /// + /// If `false`, then [onTap] becomes redundant to [Polyline.onTap]. + /// + /// Defaults to `false`. + final bool tappablesObscure; + const PolylineLayer({ super.key, required this.polylines, - this.onTapTolerance = 5, + this.onTap, + this.onTapTolerance = 3, + this.tappablesObscure = false, + this.nonTappablesObscure = true, // TODO: Remove once PR #1704 is merged bool polylineCulling = true, }); @override Widget build(BuildContext context) { - final map = MapCamera.of(context); + final camera = MapCamera.of(context); - final culledPolylines = []; - bool interactive = false; + final culledPolylines = >[]; + bool interactive = onTap != null; for (final line in polylines) { - if (!line.boundingBox.isOverlapping(map.visibleBounds)) continue; + if (!line.boundingBox.isOverlapping(camera.visibleBounds)) continue; if (!interactive && line.onTap != null) interactive = true; culledPolylines.add(line); } - final lastHit = _LastHit(); + final lastHit = _LastHit(); final paint = CustomPaint( - painter: _PolylinePainter( - culledPolylines, - map, - interactive ? lastHit : null, - onTapTolerance, + painter: _PolylinePainter( + polylines: culledPolylines, + camera: camera, + lastHit: interactive ? lastHit : null, + onTapTolerance: onTapTolerance, + tappablesObscure: tappablesObscure, + nonTappablesObscure: nonTappablesObscure, ), - size: Size(map.size.x, map.size.y), + size: Size(camera.size.x, camera.size.y), isComplex: true, ); return MobileLayerTransformer( child: interactive - ? GestureDetector( - behavior: HitTestBehavior.deferToChild, - onTap: () => - lastHit.hit?.polyline.onTap?.call(lastHit.hit!.tapPoint), - child: paint, + ? MouseRegion( + hitTestBehavior: HitTestBehavior.deferToChild, + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (lastHit.hit == null) return; + + if (onTap == null) { + for (final polyline in lastHit.hit!.tapped) { + polyline.onTap?.call(lastHit.hit!.point); + } + return; + } + + onTap!.call( + lastHit.hit!.tapped + .map((e) { + e.onTap?.call(lastHit.hit!.point); + return e.tapKey; + }) + .whereNotNull() + .toList(), + lastHit.hit!.point, + ); + }, + child: paint, + ), ) : paint, ); } } -class _PolylinePainter extends CustomPainter { - final List polylines; - - final MapCamera map; +class _PolylinePainter extends CustomPainter { + final List> polylines; + final MapCamera camera; final LatLngBounds bounds; final _LastHit? lastHit; - final double onTapTolerance; + final bool tappablesObscure; + final bool nonTappablesObscure; - _PolylinePainter( - this.polylines, - this.map, - this.lastHit, - this.onTapTolerance, - ) : bounds = map.visibleBounds; + _PolylinePainter({ + required this.polylines, + required this.camera, + required this.lastHit, + required this.onTapTolerance, + required this.tappablesObscure, + required this.nonTappablesObscure, + }) : bounds = camera.visibleBounds; int get hash => _hash ??= Object.hashAll(polylines); @@ -142,10 +212,9 @@ class _PolylinePainter extends CustomPainter { bool? hitTest(Offset position) { if (lastHit == null) return null; - final touch = map.pointToLatLng(math.Point(position.dx, position.dy)); - final origin = map.project(map.center).toOffset() - map.size.toOffset() / 2; - - Polyline? hit; + final hits = >[]; + final origin = + camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; outer: for (final p in polylines.reversed) { @@ -156,7 +225,7 @@ class _PolylinePainter extends CustomPainter { // continue; // } - final offsets = getOffsets(map, origin, p.points); + final offsets = getOffsets(camera, origin, p.points); final strokeWidth = p.useStrokeWidthInMeter ? _metersToStrokeWidth( origin, @@ -182,18 +251,22 @@ class _PolylinePainter extends CustomPainter { )); if (distance < maxDistance) { - // We break out of the loop after we find the top-most candidate - // polyline. However, we only register a hit if this polyline is - // tappable. This lets (by design) non-interactive polylines - // occlude polylines beneath. - if (p.onTap != null) hit = p; - break outer; + if (nonTappablesObscure && p.onTap == null && p.tapKey == null) { + break outer; + } + hits.add(p); + if (tappablesObscure) break outer; } } } - lastHit!.hit = hit != null ? (polyline: hit, tapPoint: touch) : null; - return hit != null; + if (hits.isEmpty) return false; + + lastHit!.hit = ( + tapped: hits, + point: camera.pointToLatLng(math.Point(position.dx, position.dy)), + ); + return true; } @override @@ -235,10 +308,11 @@ class _PolylinePainter extends CustomPainter { paint = Paint(); } - final origin = map.project(map.center).toOffset() - map.size.toOffset() / 2; + final origin = + camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; for (final polyline in polylines) { - final offsets = getOffsets(map, origin, polyline.points); + final offsets = getOffsets(camera, origin, polyline.points); if (offsets.isEmpty) { continue; } @@ -377,7 +451,7 @@ class _PolylinePainter extends CustomPainter { double strokeWidthInMeters, ) { final r = _distance.offset(p0, strokeWidthInMeters, 180); - final delta = o0 - getOffset(map, origin, r); + final delta = o0 - getOffset(camera, origin, r); return delta.distance; } From b7ee08515c25821eca9218b350e9df013e57304c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 7 Dec 2023 21:53:16 +0000 Subject: [PATCH 05/17] Added support for long presses and secondary taps Fixed bug where hits close to the intersection of two segements of a line could cause the result to duplicate Added a little 'easter egg' to example app --- example/lib/pages/polyline.dart | 24 ++++++- lib/src/layer/polyline_layer.dart | 100 +++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 5 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 3456a76a2..9d639e2c4 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -30,6 +30,17 @@ class _PolylinePageState extends State { openStreetMapTileLayer, PolylineLayer<_TapKeyType>( onTap: (tappedLineKeys, coords) => _openTappedLinesModal( + 0, + tappedLineKeys, + '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', + ), + onLongPress: (tappedLineKeys, coords) => _openTappedLinesModal( + 1, + tappedLineKeys, + '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', + ), + onSecondaryTap: (tappedLineKeys, coords) => _openTappedLinesModal( + 2, tappedLineKeys, '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', ), @@ -41,9 +52,9 @@ class _PolylinePageState extends State { const LatLng(48.8566, 2.3522), ], strokeWidth: 8, - color: Colors.purple, + color: const Color(0xFF60399E), tapKey: ( - title: 'Purple Line', + title: 'Elizabeth Line', subtitle: 'Nothing really special here...', ), ), @@ -134,6 +145,7 @@ class _PolylinePageState extends State { } void _openTappedLinesModal( + int mode, List<_TapKeyType> tappedLineKeys, String coords, ) { @@ -148,7 +160,13 @@ class _PolylinePageState extends State { 'Tapped Polyline(s)', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - Text('Tapped at point: $coords'), + Text( + '${[ + 'Tapped', + 'Long-pressed/held', + 'Secondary-tapped/alternative-clicked' + ][mode]} at point: $coords', + ), const SizedBox(height: 8), Expanded( child: ListView.builder( diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index fdb0dad31..e22e84d9b 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:core'; import 'dart:math' as math; import 'dart:ui' as ui; @@ -16,6 +17,8 @@ class _LastHit { } typedef PolylineOnTap = void Function(LatLng point); +typedef PolylineLayerOnTap = void Function( + List tappedKeys, LatLng point)?; @optionalTypeArgs class Polyline { @@ -30,7 +33,33 @@ class Polyline { final StrokeCap strokeCap; final StrokeJoin strokeJoin; final bool useStrokeWidthInMeter; + + /// Called when a tap is detected on this [Polyline] + /// + /// See [PolylineLayer.onTap], [PolylineLayer.onTapTolerance], + /// [PolylineLayer.nonTappablesObscure], and [PolylineLayer.tappablesObscure] + /// for more information. final PolylineOnTap? onTap; + + /// Called when a long press is detected on this [Polyline] + /// + /// See [PolylineLayer.onLongPress], [PolylineLayer.onTapTolerance], + /// [PolylineLayer.nonTappablesObscure], and [PolylineLayer.tappablesObscure] + /// for more information. + final PolylineOnTap? onLongPress; + + /// Called when a secondary tap/alternative click is detected on this + /// [Polyline] + /// + /// See [PolylineLayer.onSecondaryTap], [PolylineLayer.onTapTolerance], + /// [PolylineLayer.nonTappablesObscure], and [PolylineLayer.tappablesObscure] + /// for more information. + final PolylineOnTap? onSecondaryTap; + + /// Custom value that identifies this particular [Polyline] when used with + /// [PolylineLayer.onTap]/[PolylineLayer.onLongPress]/ + /// [PolylineLayer.onSecondaryTap] (either instead of or in addition to + /// [onTap]/[onLongPress]/[onSecondaryTap]) final TapKeyType? tapKey; LatLngBounds? _boundingBox; @@ -51,6 +80,8 @@ class Polyline { this.strokeJoin = StrokeJoin.round, this.useStrokeWidthInMeter = false, this.onTap, + this.onLongPress, + this.onSecondaryTap, this.tapKey, }); @@ -82,7 +113,27 @@ class PolylineLayer extends StatelessWidget { /// /// See [nonTappablesObscure] and [tappablesObscure] to set behaviour when a /// tap is over multiple overlapping [Polyline]s. - final void Function(List tappedKeys, LatLng point)? onTap; + final PolylineLayerOnTap? onTap; + + /// Called when a long press is detected on any [Polyline] with a defined + /// [Polyline.tapKey] + /// + /// Individual [Polyline]s may have their own [Polyline.onLongPress] callback + /// defined, regardless of whether this is defined. + /// + /// See [nonTappablesObscure] and [tappablesObscure] to set behaviour when a + /// tap is over multiple overlapping [Polyline]s. + final PolylineLayerOnTap? onLongPress; + + /// Called when a secondary tap/alternative click is detected on any [Polyline] + /// with a defined [Polyline.tapKey] + /// + /// Individual [Polyline]s may have their own [Polyline.onSecondaryTap] + /// callback defined, regardless of whether this is defined. + /// + /// See [nonTappablesObscure] and [tappablesObscure] to set behaviour when a + /// tap is over multiple overlapping [Polyline]s. + final PolylineLayerOnTap? onSecondaryTap; /// The number of pixels away from the visual line (including any width and /// outline) in which a tap should still register as a tap on the line @@ -118,6 +169,8 @@ class PolylineLayer extends StatelessWidget { super.key, required this.polylines, this.onTap, + this.onLongPress, + this.onSecondaryTap, this.onTapTolerance = 3, this.tappablesObscure = false, this.nonTappablesObscure = true, @@ -178,6 +231,48 @@ class PolylineLayer extends StatelessWidget { lastHit.hit!.point, ); }, + onLongPress: () { + if (lastHit.hit == null) return; + + if (onLongPress == null) { + for (final polyline in lastHit.hit!.tapped) { + polyline.onLongPress?.call(lastHit.hit!.point); + } + return; + } + + onLongPress!.call( + lastHit.hit!.tapped + .map((e) { + e.onLongPress?.call(lastHit.hit!.point); + return e.tapKey; + }) + .whereNotNull() + .toList(), + lastHit.hit!.point, + ); + }, + onSecondaryTap: () { + if (lastHit.hit == null) return; + + if (onSecondaryTap == null) { + for (final polyline in lastHit.hit!.tapped) { + polyline.onSecondaryTap?.call(lastHit.hit!.point); + } + return; + } + + onSecondaryTap!.call( + lastHit.hit!.tapped + .map((e) { + e.onSecondaryTap?.call(lastHit.hit!.point); + return e.tapKey; + }) + .whereNotNull() + .toList(), + lastHit.hit!.point, + ); + }, child: paint, ), ) @@ -263,7 +358,8 @@ class _PolylinePainter extends CustomPainter { if (hits.isEmpty) return false; lastHit!.hit = ( - tapped: hits, + // Remove duplicates caused when the hit is close to two segements + tapped: LinkedHashSet>.from(hits).toList(), point: camera.pointToLatLng(math.Point(position.dx, position.dy)), ); return true; From de17f41da1dea75af15c962213a0e07c05ba6c1b Mon Sep 17 00:00:00 2001 From: Luka S Date: Tue, 12 Dec 2023 19:01:37 +0000 Subject: [PATCH 06/17] Fix minor documentation mistake --- lib/src/layer/polyline_layer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index e22e84d9b..9641901b2 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -160,7 +160,7 @@ class PolylineLayer extends StatelessWidget { /// "Tappable" means that either or both of [Polyline.onTap] and /// [Polyline.tapKey] has been defined. /// - /// If `false`, then [onTap] becomes redundant to [Polyline.onTap]. + /// If `true`, then [onTap] becomes redundant to [Polyline.onTap]. /// /// Defaults to `false`. final bool tappablesObscure; From 88830fffb568269dddcad4bba608063ba1d73faf Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 12 Dec 2023 19:11:37 +0000 Subject: [PATCH 07/17] Simplified example changes --- example/lib/pages/polyline.dart | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 9d639e2c4..fae6d75d8 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -29,18 +29,18 @@ class _PolylinePageState extends State { children: [ openStreetMapTileLayer, PolylineLayer<_TapKeyType>( - onTap: (tappedLineKeys, coords) => _openTappedLinesModal( - 0, + onTap: (tappedLineKeys, coords) => _openTouchedLinesModal( + 'Tapped', tappedLineKeys, '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', ), - onLongPress: (tappedLineKeys, coords) => _openTappedLinesModal( - 1, + onLongPress: (tappedLineKeys, coords) => _openTouchedLinesModal( + 'Long pressed', tappedLineKeys, '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', ), - onSecondaryTap: (tappedLineKeys, coords) => _openTappedLinesModal( - 2, + onSecondaryTap: (tappedLineKeys, coords) => _openTouchedLinesModal( + 'Secondary tapped', tappedLineKeys, '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', ), @@ -144,8 +144,8 @@ class _PolylinePageState extends State { ); } - void _openTappedLinesModal( - int mode, + void _openTouchedLinesModal( + String eventType, List<_TapKeyType> tappedLineKeys, String coords, ) { @@ -161,11 +161,7 @@ class _PolylinePageState extends State { style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), Text( - '${[ - 'Tapped', - 'Long-pressed/held', - 'Secondary-tapped/alternative-clicked' - ][mode]} at point: $coords', + '$eventType at point: $coords', ), const SizedBox(height: 8), Expanded( From f82080753d3c8e35e3575401630684901f851ebe Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 12 Dec 2023 19:37:17 +0000 Subject: [PATCH 08/17] Use "occlude" instead of "obscure" --- lib/src/layer/polyline_layer.dart | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 9641901b2..36dfcc698 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -37,14 +37,14 @@ class Polyline { /// Called when a tap is detected on this [Polyline] /// /// See [PolylineLayer.onTap], [PolylineLayer.onTapTolerance], - /// [PolylineLayer.nonTappablesObscure], and [PolylineLayer.tappablesObscure] + /// [PolylineLayer.nonTappablesOcclude], and [PolylineLayer.tappablesOcclude] /// for more information. final PolylineOnTap? onTap; /// Called when a long press is detected on this [Polyline] /// /// See [PolylineLayer.onLongPress], [PolylineLayer.onTapTolerance], - /// [PolylineLayer.nonTappablesObscure], and [PolylineLayer.tappablesObscure] + /// [PolylineLayer.nonTappablesOcclude], and [PolylineLayer.tappablesOcclude] /// for more information. final PolylineOnTap? onLongPress; @@ -52,7 +52,7 @@ class Polyline { /// [Polyline] /// /// See [PolylineLayer.onSecondaryTap], [PolylineLayer.onTapTolerance], - /// [PolylineLayer.nonTappablesObscure], and [PolylineLayer.tappablesObscure] + /// [PolylineLayer.nonTappablesOcclude], and [PolylineLayer.tappablesOcclude] /// for more information. final PolylineOnTap? onSecondaryTap; @@ -111,7 +111,7 @@ class PolylineLayer extends StatelessWidget { /// Individual [Polyline]s may have their own [Polyline.onTap] callback /// defined, regardless of whether this is defined. /// - /// See [nonTappablesObscure] and [tappablesObscure] to set behaviour when a + /// See [nonTappablesOcclude] and [tappablesOcclude] to set behaviour when a /// tap is over multiple overlapping [Polyline]s. final PolylineLayerOnTap? onTap; @@ -121,7 +121,7 @@ class PolylineLayer extends StatelessWidget { /// Individual [Polyline]s may have their own [Polyline.onLongPress] callback /// defined, regardless of whether this is defined. /// - /// See [nonTappablesObscure] and [tappablesObscure] to set behaviour when a + /// See [nonTappablesOcclude] and [tappablesOcclude] to set behaviour when a /// tap is over multiple overlapping [Polyline]s. final PolylineLayerOnTap? onLongPress; @@ -131,7 +131,7 @@ class PolylineLayer extends StatelessWidget { /// Individual [Polyline]s may have their own [Polyline.onSecondaryTap] /// callback defined, regardless of whether this is defined. /// - /// See [nonTappablesObscure] and [tappablesObscure] to set behaviour when a + /// See [nonTappablesOcclude] and [tappablesOcclude] to set behaviour when a /// tap is over multiple overlapping [Polyline]s. final PolylineLayerOnTap? onSecondaryTap; @@ -152,7 +152,7 @@ class PolylineLayer extends StatelessWidget { /// Applies to both [onTap] and every [Polyline.onTap]. /// /// Defaults to `true`. - final bool nonTappablesObscure; + final bool nonTappablesOcclude; /// Whether a tappable [Polyline] should prevent taps from being handled /// on all [Polyline]s beneath it, at overlaps @@ -163,7 +163,7 @@ class PolylineLayer extends StatelessWidget { /// If `true`, then [onTap] becomes redundant to [Polyline.onTap]. /// /// Defaults to `false`. - final bool tappablesObscure; + final bool tappablesOcclude; const PolylineLayer({ super.key, @@ -172,8 +172,8 @@ class PolylineLayer extends StatelessWidget { this.onLongPress, this.onSecondaryTap, this.onTapTolerance = 3, - this.tappablesObscure = false, - this.nonTappablesObscure = true, + this.tappablesOcclude = false, + this.nonTappablesOcclude = true, // TODO: Remove once PR #1704 is merged bool polylineCulling = true, }); @@ -197,8 +197,8 @@ class PolylineLayer extends StatelessWidget { camera: camera, lastHit: interactive ? lastHit : null, onTapTolerance: onTapTolerance, - tappablesObscure: tappablesObscure, - nonTappablesObscure: nonTappablesObscure, + tappablesOcclude: tappablesOcclude, + nonTappablesOcclude: nonTappablesOcclude, ), size: Size(camera.size.x, camera.size.y), isComplex: true, @@ -287,16 +287,16 @@ class _PolylinePainter extends CustomPainter { final LatLngBounds bounds; final _LastHit? lastHit; final double onTapTolerance; - final bool tappablesObscure; - final bool nonTappablesObscure; + final bool tappablesOcclude; + final bool nonTappablesOcclude; _PolylinePainter({ required this.polylines, required this.camera, required this.lastHit, required this.onTapTolerance, - required this.tappablesObscure, - required this.nonTappablesObscure, + required this.tappablesOcclude, + required this.nonTappablesOcclude, }) : bounds = camera.visibleBounds; int get hash => _hash ??= Object.hashAll(polylines); @@ -346,11 +346,11 @@ class _PolylinePainter extends CustomPainter { )); if (distance < maxDistance) { - if (nonTappablesObscure && p.onTap == null && p.tapKey == null) { + if (nonTappablesOcclude && p.onTap == null && p.tapKey == null) { break outer; } hits.add(p); - if (tappablesObscure) break outer; + if (tappablesOcclude) break outer; } } } From 6a80c32c7a65e9c5b3fbd2ca5e2193d3f4147c6a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 12 Dec 2023 21:52:33 +0000 Subject: [PATCH 09/17] Pass entire `Polyline` object through in handler callbacks, instead of just `tapKey` Cleaned up Fixed bugs surrounding `onTap` variants Fixed bugs and removed unnecessary code surrounding the detection algorithm's duplication of results --- example/lib/pages/polyline.dart | 37 ++++------ lib/src/layer/polyline_layer.dart | 116 ++++++++++++------------------ 2 files changed, 59 insertions(+), 94 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index fae6d75d8..d25876c4b 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -29,21 +29,12 @@ class _PolylinePageState extends State { children: [ openStreetMapTileLayer, PolylineLayer<_TapKeyType>( - onTap: (tappedLineKeys, coords) => _openTouchedLinesModal( - 'Tapped', - tappedLineKeys, - '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', - ), - onLongPress: (tappedLineKeys, coords) => _openTouchedLinesModal( - 'Long pressed', - tappedLineKeys, - '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', - ), - onSecondaryTap: (tappedLineKeys, coords) => _openTouchedLinesModal( - 'Secondary tapped', - tappedLineKeys, - '(${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', - ), + onTap: (lines, coords) => + _openTouchedLinesModal('Tapped', lines, coords), + onLongPress: (lines, coords) => + _openTouchedLinesModal('Long pressed', lines, coords), + onSecondaryTap: (lines, coords) => + _openTouchedLinesModal('Secondary tapped', lines, coords), polylines: [ Polyline( points: [ @@ -146,8 +137,8 @@ class _PolylinePageState extends State { void _openTouchedLinesModal( String eventType, - List<_TapKeyType> tappedLineKeys, - String coords, + List> tappedLines, + LatLng coords, ) { showModalBottomSheet( context: context, @@ -161,25 +152,25 @@ class _PolylinePageState extends State { style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), Text( - '$eventType at point: $coords', + '$eventType at point: (${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', ), const SizedBox(height: 8), Expanded( child: ListView.builder( itemBuilder: (context, index) { - final tappedLineKey = tappedLineKeys.elementAt(index); + final tappedLine = tappedLines[index]; return ListTile( leading: index == 0 ? const Icon(Icons.vertical_align_top) - : index == tappedLineKeys.length - 1 + : index == tappedLines.length - 1 ? const Icon(Icons.vertical_align_bottom) : const SizedBox.shrink(), - title: Text(tappedLineKey.title), - subtitle: Text(tappedLineKey.subtitle), + title: Text(tappedLine.tapKey!.title), + subtitle: Text(tappedLine.tapKey!.subtitle), dense: true, ); }, - itemCount: tappedLineKeys.length, + itemCount: tappedLines.length, ), ), const SizedBox(height: 8), diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 36dfcc698..e9bb24c2a 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -1,9 +1,7 @@ -import 'dart:collection'; import 'dart:core'; import 'dart:math' as math; import 'dart:ui' as ui; -import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; @@ -13,12 +11,17 @@ import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:latlong2/latlong.dart'; class _LastHit { - ({List> tapped, LatLng point})? hit; + List>? lines; + LatLng? point; + + bool get toIgnore => lines == null || point == null; } typedef PolylineOnTap = void Function(LatLng point); typedef PolylineLayerOnTap = void Function( - List tappedKeys, LatLng point)?; + List> lines, + LatLng point, +)?; @optionalTypeArgs class Polyline { @@ -57,9 +60,12 @@ class Polyline { final PolylineOnTap? onSecondaryTap; /// Custom value that identifies this particular [Polyline] when used with - /// [PolylineLayer.onTap]/[PolylineLayer.onLongPress]/ - /// [PolylineLayer.onSecondaryTap] (either instead of or in addition to - /// [onTap]/[onLongPress]/[onSecondaryTap]) + /// [PolylineLayer.onTap] (and variants) (either instead of or in addition to + /// [onTap] (and variants)) + /// + /// When non-null, also indicates this [Polyline] is "interactive" if [onTap] + /// (and variants) is `null`. ("Tappable" means that either or both of [onTap] + /// (and variants) and [tapKey] has been defined.) final TapKeyType? tapKey; LatLngBounds? _boundingBox; @@ -138,7 +144,7 @@ class PolylineLayer extends StatelessWidget { /// The number of pixels away from the visual line (including any width and /// outline) in which a tap should still register as a tap on the line /// - /// Applies to both [onTap] and every [Polyline.onTap]. + /// Applies to both [onTap] and every [Polyline.onTap] (and variants). /// /// Defaults to 3. final double onTapTolerance; @@ -146,10 +152,10 @@ class PolylineLayer extends StatelessWidget { /// Whether a non-tappable [Polyline] should prevent taps from being handled /// on all [Polyline]s beneath it, at overlaps /// - /// "Tappable" means that either or both of [Polyline.onTap] and + /// "Tappable" means that either or both of [Polyline.onTap] (and variants) and /// [Polyline.tapKey] has been defined. /// - /// Applies to both [onTap] and every [Polyline.onTap]. + /// Applies to both [onTap] and every [Polyline.onTap] (and variants). /// /// Defaults to `true`. final bool nonTappablesOcclude; @@ -157,10 +163,11 @@ class PolylineLayer extends StatelessWidget { /// Whether a tappable [Polyline] should prevent taps from being handled /// on all [Polyline]s beneath it, at overlaps /// - /// "Tappable" means that either or both of [Polyline.onTap] and + /// "Tappable" means that either or both of [Polyline.onTap] (and variants) and /// [Polyline.tapKey] has been defined. /// - /// If `true`, then [onTap] becomes redundant to [Polyline.onTap]. + /// If `true`, then [onTap] becomes redundant to [Polyline.onTap] + /// (and variants). /// /// Defaults to `false`. final bool tappablesOcclude; @@ -182,15 +189,17 @@ class PolylineLayer extends StatelessWidget { Widget build(BuildContext context) { final camera = MapCamera.of(context); + bool interactive = + onTap != null || onLongPress != null || onSecondaryTap != null; + final lastHit = _LastHit(); + final culledPolylines = >[]; - bool interactive = onTap != null; for (final line in polylines) { if (!line.boundingBox.isOverlapping(camera.visibleBounds)) continue; if (!interactive && line.onTap != null) interactive = true; culledPolylines.add(line); } - final lastHit = _LastHit(); final paint = CustomPaint( painter: _PolylinePainter( polylines: culledPolylines, @@ -211,67 +220,28 @@ class PolylineLayer extends StatelessWidget { cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { - if (lastHit.hit == null) return; + if (lastHit.toIgnore) return; - if (onTap == null) { - for (final polyline in lastHit.hit!.tapped) { - polyline.onTap?.call(lastHit.hit!.point); - } - return; + for (final polyline in lastHit.lines!) { + polyline.onTap?.call(lastHit.point!); } - - onTap!.call( - lastHit.hit!.tapped - .map((e) { - e.onTap?.call(lastHit.hit!.point); - return e.tapKey; - }) - .whereNotNull() - .toList(), - lastHit.hit!.point, - ); + onTap?.call(lastHit.lines!, lastHit.point!); }, onLongPress: () { - if (lastHit.hit == null) return; + if (lastHit.toIgnore) return; - if (onLongPress == null) { - for (final polyline in lastHit.hit!.tapped) { - polyline.onLongPress?.call(lastHit.hit!.point); - } - return; + for (final polyline in lastHit.lines!) { + polyline.onLongPress?.call(lastHit.point!); } - - onLongPress!.call( - lastHit.hit!.tapped - .map((e) { - e.onLongPress?.call(lastHit.hit!.point); - return e.tapKey; - }) - .whereNotNull() - .toList(), - lastHit.hit!.point, - ); + onLongPress?.call(lastHit.lines!, lastHit.point!); }, onSecondaryTap: () { - if (lastHit.hit == null) return; + if (lastHit.toIgnore) return; - if (onSecondaryTap == null) { - for (final polyline in lastHit.hit!.tapped) { - polyline.onSecondaryTap?.call(lastHit.hit!.point); - } - return; + for (final polyline in lastHit.lines!) { + polyline.onSecondaryTap?.call(lastHit.point!); } - - onSecondaryTap!.call( - lastHit.hit!.tapped - .map((e) { - e.onSecondaryTap?.call(lastHit.hit!.point); - return e.tapKey; - }) - .whereNotNull() - .toList(), - lastHit.hit!.point, - ); + onSecondaryTap?.call(lastHit.lines!, lastHit.point!); }, child: paint, ), @@ -346,22 +316,26 @@ class _PolylinePainter extends CustomPainter { )); if (distance < maxDistance) { - if (nonTappablesOcclude && p.onTap == null && p.tapKey == null) { + if (nonTappablesOcclude && + p.onTap == null && + p.onLongPress == null && + p.onSecondaryTap == null && + p.tapKey == null) { break outer; } hits.add(p); if (tappablesOcclude) break outer; + break; } } } if (hits.isEmpty) return false; - lastHit!.hit = ( - // Remove duplicates caused when the hit is close to two segements - tapped: LinkedHashSet>.from(hits).toList(), - point: camera.pointToLatLng(math.Point(position.dx, position.dy)), - ); + lastHit! + ..lines = hits + ..point = camera.pointToLatLng(math.Point(position.dx, position.dy)); + return true; } From 1fd6e6d594ea486ff9b532f82fa54dc8a305ea5e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 13 Dec 2023 13:37:55 +0000 Subject: [PATCH 10/17] Implement new base-only hit detection API --- example/lib/pages/polyline.dart | 214 ++++++++++++++------------ lib/src/layer/polyline_layer.dart | 247 ++++++++---------------------- 2 files changed, 184 insertions(+), 277 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index d25876c4b..24fb0b794 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -16,6 +16,8 @@ class PolylinePage extends StatefulWidget { typedef _TapKeyType = ({String title, String subtitle}); class _PolylinePageState extends State { + final PolylineHitNotifier<_TapKeyType> hitNotifier = ValueNotifier(null); + @override Widget build(BuildContext context) { return Scaffold( @@ -28,107 +30,123 @@ class _PolylinePageState extends State { ), children: [ openStreetMapTileLayer, - PolylineLayer<_TapKeyType>( - onTap: (lines, coords) => - _openTouchedLinesModal('Tapped', lines, coords), - onLongPress: (lines, coords) => - _openTouchedLinesModal('Long pressed', lines, coords), - onSecondaryTap: (lines, coords) => - _openTouchedLinesModal('Secondary tapped', lines, coords), - polylines: [ - Polyline( - points: [ - const LatLng(51.5, -0.09), - const LatLng(53.3498, -6.2603), - const LatLng(48.8566, 2.3522), - ], - strokeWidth: 8, - color: const Color(0xFF60399E), - tapKey: ( - title: 'Elizabeth Line', - subtitle: 'Nothing really special here...', - ), - ), - Polyline( - points: [ - const LatLng(48.5, -3.09), - const LatLng(47.3498, -9.2603), - const LatLng(43.8566, -1.3522), - ], - strokeWidth: 16000, - color: Colors.pink, - useStrokeWidthInMeter: true, - tapKey: ( - title: 'Pink Line', - subtitle: 'Fixed radius in meters instead of pixels', - ), - ), - Polyline( - points: [ - const LatLng(55.5, -0.09), - const LatLng(54.3498, -6.2603), - const LatLng(52.8566, 2.3522), - ], - strokeWidth: 4, - gradientColors: [ - const Color(0xffE40203), - const Color(0xffFEED00), - const Color(0xff007E2D), - ], - tapKey: ( - title: 'Traffic Light Line', - subtitle: 'Fancy gradient instead of a solid color', - ), + MouseRegion( + hitTestBehavior: HitTestBehavior.deferToChild, + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => _openTouchedLinesModal( + 'Tapped', + hitNotifier.value!.lines, + hitNotifier.value!.point, ), - Polyline( - points: [ - const LatLng(50.5, -0.09), - const LatLng(51.3498, 6.2603), - const LatLng(53.8566, 2.3522), - ], - strokeWidth: 20, - color: Colors.blue.withOpacity(0.6), - borderStrokeWidth: 20, - borderColor: Colors.red.withOpacity(0.4), - tapKey: ( - title: 'BlueRed Line', - subtitle: - 'Solid translucent color fill, with different color outline', - ), + onLongPress: () => _openTouchedLinesModal( + 'Long pressed', + hitNotifier.value!.lines, + hitNotifier.value!.point, ), - Polyline( - points: [ - const LatLng(50.2, -0.08), - const LatLng(51.2498, -10.2603), - const LatLng(54.8566, -9.3522), - ], - strokeWidth: 20, - color: Colors.black.withOpacity(0.2), - borderStrokeWidth: 20, - borderColor: Colors.white30, - tapKey: ( - title: 'BlackWhite Line', - subtitle: - 'Solid translucent color fill, with different color outline', - ), + onSecondaryTap: () => _openTouchedLinesModal( + 'Secondary tapped', + hitNotifier.value!.lines, + hitNotifier.value!.point, ), - Polyline( - points: [ - const LatLng(49.1, -0.06), - const LatLng(52.15, -1.4), - const LatLng(55.5, 0.8), + child: PolylineLayer( + hitNotifier: hitNotifier, + polylines: [ + Polyline( + points: [ + const LatLng(51.5, -0.09), + const LatLng(53.3498, -6.2603), + const LatLng(48.8566, 2.3522), + ], + strokeWidth: 8, + color: const Color(0xFF60399E), + hitKey: ( + title: 'Elizabeth Line', + subtitle: 'Nothing really special here...', + ), + ), + Polyline( + points: [ + const LatLng(48.5, -3.09), + const LatLng(47.3498, -9.2603), + const LatLng(43.8566, -1.3522), + ], + strokeWidth: 16000, + color: Colors.pink, + useStrokeWidthInMeter: true, + hitKey: ( + title: 'Pink Line', + subtitle: 'Fixed radius in meters instead of pixels', + ), + ), + Polyline( + points: [ + const LatLng(55.5, -0.09), + const LatLng(54.3498, -6.2603), + const LatLng(52.8566, 2.3522), + ], + strokeWidth: 4, + gradientColors: [ + const Color(0xffE40203), + const Color(0xffFEED00), + const Color(0xff007E2D), + ], + hitKey: ( + title: 'Traffic Light Line', + subtitle: 'Fancy gradient instead of a solid color', + ), + ), + Polyline( + points: [ + const LatLng(50.5, -0.09), + const LatLng(51.3498, 6.2603), + const LatLng(53.8566, 2.3522), + ], + strokeWidth: 20, + color: Colors.blue.withOpacity(0.6), + borderStrokeWidth: 20, + borderColor: Colors.red.withOpacity(0.4), + hitKey: ( + title: 'BlueRed Line', + subtitle: + 'Solid translucent color fill, with different color outline', + ), + ), + Polyline( + points: [ + const LatLng(50.2, -0.08), + const LatLng(51.2498, -10.2603), + const LatLng(54.8566, -9.3522), + ], + strokeWidth: 20, + color: Colors.black.withOpacity(0.2), + borderStrokeWidth: 20, + borderColor: Colors.white30, + hitKey: ( + title: 'BlackWhite Line', + subtitle: + 'Solid translucent color fill, with different color outline', + ), + ), + Polyline( + points: [ + const LatLng(49.1, -0.06), + const LatLng(52.15, -1.4), + const LatLng(55.5, 0.8), + ], + strokeWidth: 10, + color: Colors.yellow, + borderStrokeWidth: 10, + borderColor: Colors.blue.withOpacity(0.5), + hitKey: ( + title: 'YellowBlue Line', + subtitle: + 'Solid translucent color fill, with different color outline', + ), + ), ], - strokeWidth: 10, - color: Colors.yellow, - borderStrokeWidth: 10, - borderColor: Colors.blue.withOpacity(0.5), - tapKey: ( - title: 'YellowBlue Line', - subtitle: - 'Solid translucent color fill, with different color outline', - ), ), - ], + ), ), ], ), @@ -165,8 +183,8 @@ class _PolylinePageState extends State { : index == tappedLines.length - 1 ? const Icon(Icons.vertical_align_bottom) : const SizedBox.shrink(), - title: Text(tappedLine.tapKey!.title), - subtitle: Text(tappedLine.tapKey!.subtitle), + title: Text(tappedLine.hitKey!.title), + subtitle: Text(tappedLine.hitKey!.subtitle), dense: true, ); }, diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index e9bb24c2a..328f68620 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -10,18 +10,29 @@ import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:latlong2/latlong.dart'; -class _LastHit { - List>? lines; - LatLng? point; +/// Result from polyline hit detection +/// +/// Emmitted by [PolylineLayer.hitNotifier]'s [ValueNotifier] +/// ([PolylineHitNotifier]). +@optionalTypeArgs +class PolylineHit { + /// All hit [Polyline]s within the corresponding layer + /// + /// Ordered from first-last, visually top-bottom. + final List> lines; - bool get toIgnore => lines == null || point == null; + /// Coordinates of the detected hit + /// + /// Note that this may not lie on a [Polyline]. + final LatLng point; + + const PolylineHit._({required this.lines, required this.point}); } -typedef PolylineOnTap = void Function(LatLng point); -typedef PolylineLayerOnTap = void Function( - List> lines, - LatLng point, -)?; +/// Typedef used on [PolylineLayer.hitNotifier] +@optionalTypeArgs +typedef PolylineHitNotifier + = ValueNotifier?>; @optionalTypeArgs class Polyline { @@ -37,36 +48,11 @@ class Polyline { final StrokeJoin strokeJoin; final bool useStrokeWidthInMeter; - /// Called when a tap is detected on this [Polyline] - /// - /// See [PolylineLayer.onTap], [PolylineLayer.onTapTolerance], - /// [PolylineLayer.nonTappablesOcclude], and [PolylineLayer.tappablesOcclude] - /// for more information. - final PolylineOnTap? onTap; - - /// Called when a long press is detected on this [Polyline] - /// - /// See [PolylineLayer.onLongPress], [PolylineLayer.onTapTolerance], - /// [PolylineLayer.nonTappablesOcclude], and [PolylineLayer.tappablesOcclude] - /// for more information. - final PolylineOnTap? onLongPress; - - /// Called when a secondary tap/alternative click is detected on this - /// [Polyline] + /// Custom value that can identify this particular [Polyline] after hit + /// detection /// - /// See [PolylineLayer.onSecondaryTap], [PolylineLayer.onTapTolerance], - /// [PolylineLayer.nonTappablesOcclude], and [PolylineLayer.tappablesOcclude] - /// for more information. - final PolylineOnTap? onSecondaryTap; - - /// Custom value that identifies this particular [Polyline] when used with - /// [PolylineLayer.onTap] (and variants) (either instead of or in addition to - /// [onTap] (and variants)) - /// - /// When non-null, also indicates this [Polyline] is "interactive" if [onTap] - /// (and variants) is `null`. ("Tappable" means that either or both of [onTap] - /// (and variants) and [tapKey] has been defined.) - final TapKeyType? tapKey; + /// See [PolylineLayer.hitNotifier] for more info. + final TapKeyType? hitKey; LatLngBounds? _boundingBox; @@ -85,10 +71,7 @@ class Polyline { this.strokeCap = StrokeCap.round, this.strokeJoin = StrokeJoin.round, this.useStrokeWidthInMeter = false, - this.onTap, - this.onLongPress, - this.onSecondaryTap, - this.tapKey, + this.hitKey, }); /// Used to batch draw calls to the canvas. @@ -103,6 +86,7 @@ class Polyline { strokeCap, strokeJoin, useStrokeWidthInMeter, + hitKey, ); } @@ -111,76 +95,36 @@ class Polyline { class PolylineLayer extends StatelessWidget { final List> polylines; - /// Called when a tap is detected on any [Polyline] with a defined - /// [Polyline.tapKey] - /// - /// Individual [Polyline]s may have their own [Polyline.onTap] callback - /// defined, regardless of whether this is defined. + /// A notifier to notify when a hit is detected over a/multiple [Polyline]s /// - /// See [nonTappablesOcclude] and [tappablesOcclude] to set behaviour when a - /// tap is over multiple overlapping [Polyline]s. - final PolylineLayerOnTap? onTap; - - /// Called when a long press is detected on any [Polyline] with a defined - /// [Polyline.tapKey] + /// Note that a hover event is included as a hit event. /// - /// Individual [Polyline]s may have their own [Polyline.onLongPress] callback - /// defined, regardless of whether this is defined. - /// - /// See [nonTappablesOcclude] and [tappablesOcclude] to set behaviour when a - /// tap is over multiple overlapping [Polyline]s. - final PolylineLayerOnTap? onLongPress; - - /// Called when a secondary tap/alternative click is detected on any [Polyline] - /// with a defined [Polyline.tapKey] + /// To listen for hits, wrap the layer in a standard hit detector widget, such + /// as [GestureDetector] and/or [MouseRegion] (and set + /// [HitTestBehavior.deferToChild] if necessary). Then use the latest value + /// (via [ValueNotifier.value]) in the detector's callbacks to get the latest + /// [PolylineHit] result. It is also possible to listen to the notifier + /// directly. /// - /// Individual [Polyline]s may have their own [Polyline.onSecondaryTap] - /// callback defined, regardless of whether this is defined. + /// A [Polyline.hitKey] may be used to attach additional retrievable + /// information, or to signify whether a [Polyline] is "tappable", for example. /// - /// See [nonTappablesOcclude] and [tappablesOcclude] to set behaviour when a - /// tap is over multiple overlapping [Polyline]s. - final PolylineLayerOnTap? onSecondaryTap; + /// See online documentation for more detailed usage instructions. See the + /// example project for an example implementation. + final PolylineHitNotifier? hitNotifier; - /// The number of pixels away from the visual line (including any width and - /// outline) in which a tap should still register as a tap on the line - /// - /// Applies to both [onTap] and every [Polyline.onTap] (and variants). + /// The number of pixels away from a visual line (including any width and + /// outline) in which a hit should still register as a hit on the line, and + /// trigger a notification to [hitNotifier] /// /// Defaults to 3. - final double onTapTolerance; - - /// Whether a non-tappable [Polyline] should prevent taps from being handled - /// on all [Polyline]s beneath it, at overlaps - /// - /// "Tappable" means that either or both of [Polyline.onTap] (and variants) and - /// [Polyline.tapKey] has been defined. - /// - /// Applies to both [onTap] and every [Polyline.onTap] (and variants). - /// - /// Defaults to `true`. - final bool nonTappablesOcclude; - - /// Whether a tappable [Polyline] should prevent taps from being handled - /// on all [Polyline]s beneath it, at overlaps - /// - /// "Tappable" means that either or both of [Polyline.onTap] (and variants) and - /// [Polyline.tapKey] has been defined. - /// - /// If `true`, then [onTap] becomes redundant to [Polyline.onTap] - /// (and variants). - /// - /// Defaults to `false`. - final bool tappablesOcclude; + final double hitTolerance; const PolylineLayer({ super.key, required this.polylines, - this.onTap, - this.onLongPress, - this.onSecondaryTap, - this.onTapTolerance = 3, - this.tappablesOcclude = false, - this.nonTappablesOcclude = true, + this.hitNotifier, + this.hitTolerance = 3, // TODO: Remove once PR #1704 is merged bool polylineCulling = true, }); @@ -189,64 +133,19 @@ class PolylineLayer extends StatelessWidget { Widget build(BuildContext context) { final camera = MapCamera.of(context); - bool interactive = - onTap != null || onLongPress != null || onSecondaryTap != null; - final lastHit = _LastHit(); - - final culledPolylines = >[]; - for (final line in polylines) { - if (!line.boundingBox.isOverlapping(camera.visibleBounds)) continue; - if (!interactive && line.onTap != null) interactive = true; - culledPolylines.add(line); - } - - final paint = CustomPaint( - painter: _PolylinePainter( - polylines: culledPolylines, - camera: camera, - lastHit: interactive ? lastHit : null, - onTapTolerance: onTapTolerance, - tappablesOcclude: tappablesOcclude, - nonTappablesOcclude: nonTappablesOcclude, - ), - size: Size(camera.size.x, camera.size.y), - isComplex: true, - ); - return MobileLayerTransformer( - child: interactive - ? MouseRegion( - hitTestBehavior: HitTestBehavior.deferToChild, - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - if (lastHit.toIgnore) return; - - for (final polyline in lastHit.lines!) { - polyline.onTap?.call(lastHit.point!); - } - onTap?.call(lastHit.lines!, lastHit.point!); - }, - onLongPress: () { - if (lastHit.toIgnore) return; - - for (final polyline in lastHit.lines!) { - polyline.onLongPress?.call(lastHit.point!); - } - onLongPress?.call(lastHit.lines!, lastHit.point!); - }, - onSecondaryTap: () { - if (lastHit.toIgnore) return; - - for (final polyline in lastHit.lines!) { - polyline.onSecondaryTap?.call(lastHit.point!); - } - onSecondaryTap?.call(lastHit.lines!, lastHit.point!); - }, - child: paint, - ), - ) - : paint, + child: CustomPaint( + painter: _PolylinePainter( + polylines: polylines + .where((p) => p.boundingBox.isOverlapping(camera.visibleBounds)) + .toList(), + camera: camera, + hitNotifier: hitNotifier, + onTapTolerance: hitTolerance, + ), + size: Size(camera.size.x, camera.size.y), + isComplex: true, + ), ); } } @@ -255,33 +154,30 @@ class _PolylinePainter extends CustomPainter { final List> polylines; final MapCamera camera; final LatLngBounds bounds; - final _LastHit? lastHit; + final PolylineHitNotifier? hitNotifier; final double onTapTolerance; - final bool tappablesOcclude; - final bool nonTappablesOcclude; _PolylinePainter({ required this.polylines, required this.camera, - required this.lastHit, + required this.hitNotifier, required this.onTapTolerance, - required this.tappablesOcclude, - required this.nonTappablesOcclude, }) : bounds = camera.visibleBounds; - int get hash => _hash ??= Object.hashAll(polylines); + final hits = List>.empty(growable: true); + int get hash => _hash ??= Object.hashAll(polylines); int? _hash; @override bool? hitTest(Offset position) { - if (lastHit == null) return null; + if (hitNotifier == null) return null; + + hits.clear(); - final hits = >[]; final origin = camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; - outer: for (final p in polylines.reversed) { // TODO: For efficiency we'd ideally filter by bounding box here. However // we'd need to compute an extended bounding box that accounts account for @@ -316,15 +212,7 @@ class _PolylinePainter extends CustomPainter { )); if (distance < maxDistance) { - if (nonTappablesOcclude && - p.onTap == null && - p.onLongPress == null && - p.onSecondaryTap == null && - p.tapKey == null) { - break outer; - } hits.add(p); - if (tappablesOcclude) break outer; break; } } @@ -332,9 +220,10 @@ class _PolylinePainter extends CustomPainter { if (hits.isEmpty) return false; - lastHit! - ..lines = hits - ..point = camera.pointToLatLng(math.Point(position.dx, position.dy)); + hitNotifier!.value = PolylineHit._( + lines: hits, + point: camera.pointToLatLng(math.Point(position.dx, position.dy)), + ); return true; } From 7f586c247bf2226f3e30bd1d72a89694dffcad80 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 13 Dec 2023 19:33:43 +0000 Subject: [PATCH 11/17] Improved example application Added equality methods to `Polyline` --- example/lib/pages/polyline.dart | 28 +++++++++++++++- lib/src/layer/polyline_layer.dart | 54 +++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 24fb0b794..d021e2dde 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -18,6 +18,8 @@ typedef _TapKeyType = ({String title, String subtitle}); class _PolylinePageState extends State { final PolylineHitNotifier<_TapKeyType> hitNotifier = ValueNotifier(null); + List>? hoverLines; + @override Widget build(BuildContext context) { return Scaffold( @@ -33,6 +35,27 @@ class _PolylinePageState extends State { MouseRegion( hitTestBehavior: HitTestBehavior.deferToChild, cursor: SystemMouseCursors.click, + onHover: (_) { + if (hitNotifier.value == null) return; + + final lines = hitNotifier.value!.lines + .where((e) => e.hitKey != null) + .map( + (e) => Polyline<_TapKeyType>( + points: e.points, + strokeWidth: e.strokeWidth + e.borderStrokeWidth, + color: Colors.transparent, + borderStrokeWidth: 15, + borderColor: Colors.green, + useStrokeWidthInMeter: e.useStrokeWidthInMeter, + ), + ) + .toList(); + setState(() => hoverLines = lines); + }, + + /// Clear hovered lines when touched lines modal appears + onExit: (_) => setState(() => hoverLines = null), child: GestureDetector( onTap: () => _openTouchedLinesModal( 'Tapped', @@ -49,7 +72,7 @@ class _PolylinePageState extends State { hitNotifier.value!.lines, hitNotifier.value!.point, ), - child: PolylineLayer( + child: PolylineLayer<_TapKeyType>( hitNotifier: hitNotifier, polylines: [ Polyline( @@ -144,6 +167,7 @@ class _PolylinePageState extends State { 'Solid translucent color fill, with different color outline', ), ), + if (hoverLines != null) ...hoverLines!, ], ), ), @@ -158,6 +182,8 @@ class _PolylinePageState extends State { List> tappedLines, LatLng coords, ) { + tappedLines.removeWhere((e) => e.hitKey == null); + showModalBottomSheet( context: context, builder: (context) => Padding( diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 328f68620..28bc10277 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -2,6 +2,7 @@ import 'dart:core'; import 'dart:math' as math; import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; @@ -14,7 +15,6 @@ import 'package:latlong2/latlong.dart'; /// /// Emmitted by [PolylineLayer.hitNotifier]'s [ValueNotifier] /// ([PolylineHitNotifier]). -@optionalTypeArgs class PolylineHit { /// All hit [Polyline]s within the corresponding layer /// @@ -30,11 +30,9 @@ class PolylineHit { } /// Typedef used on [PolylineLayer.hitNotifier] -@optionalTypeArgs typedef PolylineHitNotifier = ValueNotifier?>; -@optionalTypeArgs class Polyline { final List points; final double strokeWidth; @@ -74,8 +72,24 @@ class Polyline { this.hitKey, }); - /// Used to batch draw calls to the canvas. - int get renderHashCode => Object.hash( + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Polyline && + listEquals(points, other.points) && + strokeWidth == other.strokeWidth && + color == other.color && + borderStrokeWidth == other.borderStrokeWidth && + borderColor == other.borderColor && + listEquals(gradientColors, other.gradientColors) && + listEquals(colorsStop, other.colorsStop) && + isDotted == other.isDotted && + strokeCap == other.strokeCap && + strokeJoin == other.strokeJoin && + useStrokeWidthInMeter == other.useStrokeWidthInMeter && + hitKey == other.hitKey); + + List get _baseHashableValues => [ strokeWidth, color, borderStrokeWidth, @@ -87,30 +101,40 @@ class Polyline { strokeJoin, useStrokeWidthInMeter, hitKey, - ); + ]; + + /// Used to batch draw calls to the canvas + int get renderHashCode => Object.hashAll(_baseHashableValues); + + @override + int get hashCode => Object.hashAll([points, ..._baseHashableValues]); } @immutable -@optionalTypeArgs class PolylineLayer extends StatelessWidget { final List> polylines; /// A notifier to notify when a hit is detected over a/multiple [Polyline]s /// - /// Note that a hover event is included as a hit event. - /// /// To listen for hits, wrap the layer in a standard hit detector widget, such /// as [GestureDetector] and/or [MouseRegion] (and set /// [HitTestBehavior.deferToChild] if necessary). Then use the latest value - /// (via [ValueNotifier.value]) in the detector's callbacks to get the latest - /// [PolylineHit] result. It is also possible to listen to the notifier - /// directly. + /// (via [ValueNotifier.value]) in the detector's callbacks. It is also + /// possible to listen to the notifier directly. /// /// A [Polyline.hitKey] may be used to attach additional retrievable /// information, or to signify whether a [Polyline] is "tappable", for example. /// + /// Note that a hover event is included as a hit event. Therefore for + /// performance reasons, it may be advantageous to check the new value's + /// equality against the previous value (excluding the [PolylineHit.point], + /// which will always change), and avoid doing heavy work if they are the same. + /// /// See online documentation for more detailed usage instructions. See the /// example project for an example implementation. + /// + /// Will notify [PolylineHit]s if any [Polyline]s were hit, otherwise will + /// notify `null`. final PolylineHitNotifier? hitNotifier; /// The number of pixels away from a visual line (including any width and @@ -218,13 +242,15 @@ class _PolylinePainter extends CustomPainter { } } - if (hits.isEmpty) return false; + if (hits.isEmpty) { + hitNotifier!.value = null; + return false; + } hitNotifier!.value = PolylineHit._( lines: hits, point: camera.pointToLatLng(math.Point(position.dx, position.dy)), ); - return true; } From 908f0ddb3fe578b597005ed2fa05db20e43a63e0 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 13 Dec 2023 21:29:08 +0000 Subject: [PATCH 12/17] Minor improvements from review --- lib/src/layer/polyline_layer.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 28bc10277..ed227850e 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -89,7 +89,8 @@ class Polyline { useStrokeWidthInMeter == other.useStrokeWidthInMeter && hitKey == other.hitKey); - List get _baseHashableValues => [ + /// Used to batch draw calls to the canvas + int get renderHashCode => Object.hash( strokeWidth, color, borderStrokeWidth, @@ -101,13 +102,10 @@ class Polyline { strokeJoin, useStrokeWidthInMeter, hitKey, - ]; - - /// Used to batch draw calls to the canvas - int get renderHashCode => Object.hashAll(_baseHashableValues); + ); @override - int get hashCode => Object.hashAll([points, ..._baseHashableValues]); + int get hashCode => Object.hash(points, renderHashCode); } @immutable @@ -188,6 +186,7 @@ class _PolylinePainter extends CustomPainter { required this.onTapTolerance, }) : bounds = camera.visibleBounds; + // Avoids reallocation on every `hitTest`, is cleared every time final hits = List>.empty(growable: true); int get hash => _hash ??= Object.hashAll(polylines); From 7954ba20314878082633010b6741246e67e2cbf5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 14 Dec 2023 12:50:58 +0000 Subject: [PATCH 13/17] Remove `hitKey` and generic typing --- example/lib/pages/polyline.dart | 204 ++++++++++++++---------------- lib/src/layer/polyline_layer.dart | 38 ++---- 2 files changed, 111 insertions(+), 131 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index d021e2dde..2ed174194 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -13,12 +13,96 @@ class PolylinePage extends StatefulWidget { State createState() => _PolylinePageState(); } -typedef _TapKeyType = ({String title, String subtitle}); - class _PolylinePageState extends State { - final PolylineHitNotifier<_TapKeyType> hitNotifier = ValueNotifier(null); + final PolylineHitNotifier hitNotifier = ValueNotifier(null); + + final polylines = { + Polyline( + points: [ + const LatLng(51.5, -0.09), + const LatLng(53.3498, -6.2603), + const LatLng(48.8566, 2.3522), + ], + strokeWidth: 8, + color: const Color(0xFF60399E), + ): ( + title: 'Elizabeth Line', + subtitle: 'Nothing really special here...', + ), + Polyline( + points: [ + const LatLng(48.5, -3.09), + const LatLng(47.3498, -9.2603), + const LatLng(43.8566, -1.3522), + ], + strokeWidth: 16000, + color: Colors.pink, + useStrokeWidthInMeter: true, + ): ( + title: 'Pink Line', + subtitle: 'Fixed radius in meters instead of pixels', + ), + Polyline( + points: [ + const LatLng(55.5, -0.09), + const LatLng(54.3498, -6.2603), + const LatLng(52.8566, 2.3522), + ], + strokeWidth: 4, + gradientColors: [ + const Color(0xffE40203), + const Color(0xffFEED00), + const Color(0xff007E2D), + ], + ): ( + title: 'Traffic Light Line', + subtitle: 'Fancy gradient instead of a solid color', + ), + Polyline( + points: [ + const LatLng(50.5, -0.09), + const LatLng(51.3498, 6.2603), + const LatLng(53.8566, 2.3522), + ], + strokeWidth: 20, + color: Colors.blue.withOpacity(0.6), + borderStrokeWidth: 20, + borderColor: Colors.red.withOpacity(0.4), + ): ( + title: 'BlueRed Line', + subtitle: 'Solid translucent color fill, with different color outline', + ), + Polyline( + points: [ + const LatLng(50.2, -0.08), + const LatLng(51.2498, -10.2603), + const LatLng(54.8566, -9.3522), + ], + strokeWidth: 20, + color: Colors.black.withOpacity(0.2), + borderStrokeWidth: 20, + borderColor: Colors.white30, + ): ( + title: 'BlackWhite Line', + subtitle: 'Solid translucent color fill, with different color outline', + ), + Polyline( + points: [ + const LatLng(49.1, -0.06), + const LatLng(52.15, -1.4), + const LatLng(55.5, 0.8), + ], + strokeWidth: 10, + color: Colors.yellow, + borderStrokeWidth: 10, + borderColor: Colors.blue.withOpacity(0.5), + ): ( + title: 'YellowBlue Line', + subtitle: 'Solid translucent color fill, with different color outline', + ), + }; - List>? hoverLines; + List? hoverLines; @override Widget build(BuildContext context) { @@ -39,9 +123,9 @@ class _PolylinePageState extends State { if (hitNotifier.value == null) return; final lines = hitNotifier.value!.lines - .where((e) => e.hitKey != null) + .where((e) => polylines.containsKey(e)) .map( - (e) => Polyline<_TapKeyType>( + (e) => Polyline( points: e.points, strokeWidth: e.strokeWidth + e.borderStrokeWidth, color: Colors.transparent, @@ -72,103 +156,9 @@ class _PolylinePageState extends State { hitNotifier.value!.lines, hitNotifier.value!.point, ), - child: PolylineLayer<_TapKeyType>( + child: PolylineLayer( hitNotifier: hitNotifier, - polylines: [ - Polyline( - points: [ - const LatLng(51.5, -0.09), - const LatLng(53.3498, -6.2603), - const LatLng(48.8566, 2.3522), - ], - strokeWidth: 8, - color: const Color(0xFF60399E), - hitKey: ( - title: 'Elizabeth Line', - subtitle: 'Nothing really special here...', - ), - ), - Polyline( - points: [ - const LatLng(48.5, -3.09), - const LatLng(47.3498, -9.2603), - const LatLng(43.8566, -1.3522), - ], - strokeWidth: 16000, - color: Colors.pink, - useStrokeWidthInMeter: true, - hitKey: ( - title: 'Pink Line', - subtitle: 'Fixed radius in meters instead of pixels', - ), - ), - Polyline( - points: [ - const LatLng(55.5, -0.09), - const LatLng(54.3498, -6.2603), - const LatLng(52.8566, 2.3522), - ], - strokeWidth: 4, - gradientColors: [ - const Color(0xffE40203), - const Color(0xffFEED00), - const Color(0xff007E2D), - ], - hitKey: ( - title: 'Traffic Light Line', - subtitle: 'Fancy gradient instead of a solid color', - ), - ), - Polyline( - points: [ - const LatLng(50.5, -0.09), - const LatLng(51.3498, 6.2603), - const LatLng(53.8566, 2.3522), - ], - strokeWidth: 20, - color: Colors.blue.withOpacity(0.6), - borderStrokeWidth: 20, - borderColor: Colors.red.withOpacity(0.4), - hitKey: ( - title: 'BlueRed Line', - subtitle: - 'Solid translucent color fill, with different color outline', - ), - ), - Polyline( - points: [ - const LatLng(50.2, -0.08), - const LatLng(51.2498, -10.2603), - const LatLng(54.8566, -9.3522), - ], - strokeWidth: 20, - color: Colors.black.withOpacity(0.2), - borderStrokeWidth: 20, - borderColor: Colors.white30, - hitKey: ( - title: 'BlackWhite Line', - subtitle: - 'Solid translucent color fill, with different color outline', - ), - ), - Polyline( - points: [ - const LatLng(49.1, -0.06), - const LatLng(52.15, -1.4), - const LatLng(55.5, 0.8), - ], - strokeWidth: 10, - color: Colors.yellow, - borderStrokeWidth: 10, - borderColor: Colors.blue.withOpacity(0.5), - hitKey: ( - title: 'YellowBlue Line', - subtitle: - 'Solid translucent color fill, with different color outline', - ), - ), - if (hoverLines != null) ...hoverLines!, - ], + polylines: (hoverLines ?? []).followedBy(polylines.keys), ), ), ), @@ -179,10 +169,10 @@ class _PolylinePageState extends State { void _openTouchedLinesModal( String eventType, - List> tappedLines, + List tappedLines, LatLng coords, ) { - tappedLines.removeWhere((e) => e.hitKey == null); + tappedLines.removeWhere((e) => !polylines.containsKey(e)); showModalBottomSheet( context: context, @@ -202,15 +192,15 @@ class _PolylinePageState extends State { Expanded( child: ListView.builder( itemBuilder: (context, index) { - final tappedLine = tappedLines[index]; + final tappedLineData = polylines[tappedLines[index]]!; return ListTile( leading: index == 0 ? const Icon(Icons.vertical_align_top) : index == tappedLines.length - 1 ? const Icon(Icons.vertical_align_bottom) : const SizedBox.shrink(), - title: Text(tappedLine.hitKey!.title), - subtitle: Text(tappedLine.hitKey!.subtitle), + title: Text(tappedLineData.title), + subtitle: Text(tappedLineData.subtitle), dense: true, ); }, diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index ed227850e..cf4b52c7d 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -15,11 +15,11 @@ import 'package:latlong2/latlong.dart'; /// /// Emmitted by [PolylineLayer.hitNotifier]'s [ValueNotifier] /// ([PolylineHitNotifier]). -class PolylineHit { +class PolylineHit { /// All hit [Polyline]s within the corresponding layer /// /// Ordered from first-last, visually top-bottom. - final List> lines; + final List lines; /// Coordinates of the detected hit /// @@ -30,10 +30,9 @@ class PolylineHit { } /// Typedef used on [PolylineLayer.hitNotifier] -typedef PolylineHitNotifier - = ValueNotifier?>; +typedef PolylineHitNotifier = ValueNotifier; -class Polyline { +class Polyline { final List points; final double strokeWidth; final Color color; @@ -46,12 +45,6 @@ class Polyline { final StrokeJoin strokeJoin; final bool useStrokeWidthInMeter; - /// Custom value that can identify this particular [Polyline] after hit - /// detection - /// - /// See [PolylineLayer.hitNotifier] for more info. - final TapKeyType? hitKey; - LatLngBounds? _boundingBox; LatLngBounds get boundingBox => @@ -69,13 +62,12 @@ class Polyline { this.strokeCap = StrokeCap.round, this.strokeJoin = StrokeJoin.round, this.useStrokeWidthInMeter = false, - this.hitKey, }); @override bool operator ==(Object other) => identical(this, other) || - (other is Polyline && + (other is Polyline && listEquals(points, other.points) && strokeWidth == other.strokeWidth && color == other.color && @@ -86,8 +78,7 @@ class Polyline { isDotted == other.isDotted && strokeCap == other.strokeCap && strokeJoin == other.strokeJoin && - useStrokeWidthInMeter == other.useStrokeWidthInMeter && - hitKey == other.hitKey); + useStrokeWidthInMeter == other.useStrokeWidthInMeter); /// Used to batch draw calls to the canvas int get renderHashCode => Object.hash( @@ -101,7 +92,6 @@ class Polyline { strokeCap, strokeJoin, useStrokeWidthInMeter, - hitKey, ); @override @@ -109,8 +99,8 @@ class Polyline { } @immutable -class PolylineLayer extends StatelessWidget { - final List> polylines; +class PolylineLayer extends StatelessWidget { + final Iterable polylines; /// A notifier to notify when a hit is detected over a/multiple [Polyline]s /// @@ -133,7 +123,7 @@ class PolylineLayer extends StatelessWidget { /// /// Will notify [PolylineHit]s if any [Polyline]s were hit, otherwise will /// notify `null`. - final PolylineHitNotifier? hitNotifier; + final PolylineHitNotifier? hitNotifier; /// The number of pixels away from a visual line (including any width and /// outline) in which a hit should still register as a hit on the line, and @@ -157,7 +147,7 @@ class PolylineLayer extends StatelessWidget { return MobileLayerTransformer( child: CustomPaint( - painter: _PolylinePainter( + painter: _PolylinePainter( polylines: polylines .where((p) => p.boundingBox.isOverlapping(camera.visibleBounds)) .toList(), @@ -172,8 +162,8 @@ class PolylineLayer extends StatelessWidget { } } -class _PolylinePainter extends CustomPainter { - final List> polylines; +class _PolylinePainter extends CustomPainter { + final List polylines; final MapCamera camera; final LatLngBounds bounds; final PolylineHitNotifier? hitNotifier; @@ -187,7 +177,7 @@ class _PolylinePainter extends CustomPainter { }) : bounds = camera.visibleBounds; // Avoids reallocation on every `hitTest`, is cleared every time - final hits = List>.empty(growable: true); + final hits = List.empty(growable: true); int get hash => _hash ??= Object.hashAll(polylines); int? _hash; @@ -246,7 +236,7 @@ class _PolylinePainter extends CustomPainter { return false; } - hitNotifier!.value = PolylineHit._( + hitNotifier!.value = PolylineHit._( lines: hits, point: camera.pointToLatLng(math.Point(position.dx, position.dy)), ); From 403d8371f9b33af8c8685c3834614312dad79492 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 14 Dec 2023 13:01:45 +0000 Subject: [PATCH 14/17] Replace `hitTolerance` with `minimumHitbox` --- lib/src/layer/polyline_layer.dart | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index cf4b52c7d..cc9eeb29e 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -125,18 +125,20 @@ class PolylineLayer extends StatelessWidget { /// notify `null`. final PolylineHitNotifier? hitNotifier; - /// The number of pixels away from a visual line (including any width and - /// outline) in which a hit should still register as a hit on the line, and - /// trigger a notification to [hitNotifier] + /// The minimum radius of the hittable area around each [Polyline] in logical + /// pixels /// - /// Defaults to 3. - final double hitTolerance; + /// The entire visible area is always hittable, but if the visible area is + /// smaller than this, then this will be the hittable area. + /// + /// Defaults to 10. + final double minimumHitbox; const PolylineLayer({ super.key, required this.polylines, this.hitNotifier, - this.hitTolerance = 3, + this.minimumHitbox = 10, // TODO: Remove once PR #1704 is merged bool polylineCulling = true, }); @@ -153,7 +155,7 @@ class PolylineLayer extends StatelessWidget { .toList(), camera: camera, hitNotifier: hitNotifier, - onTapTolerance: hitTolerance, + minimumHitbox: minimumHitbox, ), size: Size(camera.size.x, camera.size.y), isComplex: true, @@ -167,13 +169,13 @@ class _PolylinePainter extends CustomPainter { final MapCamera camera; final LatLngBounds bounds; final PolylineHitNotifier? hitNotifier; - final double onTapTolerance; + final double minimumHitbox; _PolylinePainter({ required this.polylines, required this.camera, required this.hitNotifier, - required this.onTapTolerance, + required this.minimumHitbox, }) : bounds = camera.visibleBounds; // Avoids reallocation on every `hitTest`, is cleared every time @@ -208,8 +210,8 @@ class _PolylinePainter extends CustomPainter { p.strokeWidth, ) : p.strokeWidth; - final maxDistance = - (strokeWidth / 2 + p.borderStrokeWidth / 2) + onTapTolerance; + final hittableDistance = + math.max(strokeWidth / 2 + p.borderStrokeWidth / 2, minimumHitbox); for (int i = 0; i < offsets.length - 1; i++) { final o1 = offsets[i]; @@ -224,7 +226,7 @@ class _PolylinePainter extends CustomPainter { o2.dy, )); - if (distance < maxDistance) { + if (distance < hittableDistance) { hits.add(p); break; } From 5eec2113d2dadbd0df16eb764362dcbc74f9cdb7 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 14 Dec 2023 13:29:53 +0000 Subject: [PATCH 15/17] Remove remaining reference to `Polyline.hitKey` Minor documentation improvements --- lib/src/layer/polyline_layer.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index cc9eeb29e..d5e31a6e3 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -110,19 +110,17 @@ class PolylineLayer extends StatelessWidget { /// (via [ValueNotifier.value]) in the detector's callbacks. It is also /// possible to listen to the notifier directly. /// - /// A [Polyline.hitKey] may be used to attach additional retrievable - /// information, or to signify whether a [Polyline] is "tappable", for example. - /// /// Note that a hover event is included as a hit event. Therefore for /// performance reasons, it may be advantageous to check the new value's /// equality against the previous value (excluding the [PolylineHit.point], - /// which will always change), and avoid doing heavy work if they are the same. + /// which will always change), and avoid doing any heavy work if they are the + /// same. /// /// See online documentation for more detailed usage instructions. See the /// example project for an example implementation. /// - /// Will notify [PolylineHit]s if any [Polyline]s were hit, otherwise will - /// notify `null`. + /// Will notify with [PolylineHit]s when any [Polyline]s are hit, otherwise + /// will notify with `null`. final PolylineHitNotifier? hitNotifier; /// The minimum radius of the hittable area around each [Polyline] in logical From 3ef7ee351618932b6900a41f26cef5bc1605651a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 15 Dec 2023 22:32:49 +0000 Subject: [PATCH 16/17] Revert `polylines` type to `List` from `Iterable` --- example/lib/pages/polyline.dart | 3 ++- lib/src/layer/polyline_layer.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 2ed174194..4a597a497 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -158,7 +158,8 @@ class _PolylinePageState extends State { ), child: PolylineLayer( hitNotifier: hitNotifier, - polylines: (hoverLines ?? []).followedBy(polylines.keys), + polylines: + (hoverLines ?? []).followedBy(polylines.keys).toList(), ), ), ), diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index d5e31a6e3..af4530711 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -100,7 +100,7 @@ class Polyline { @immutable class PolylineLayer extends StatelessWidget { - final Iterable polylines; + final List polylines; /// A notifier to notify when a hit is detected over a/multiple [Polyline]s /// From fb9751df3276b9621198f5c7deee34342d5cf5d3 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 15 Dec 2023 22:39:48 +0000 Subject: [PATCH 17/17] Paint hover lines on top in polyline example --- example/lib/pages/polyline.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 4a597a497..92dbc9ff5 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -158,8 +158,7 @@ class _PolylinePageState extends State { ), child: PolylineLayer( hitNotifier: hitNotifier, - polylines: - (hoverLines ?? []).followedBy(polylines.keys).toList(), + polylines: polylines.keys.followedBy(hoverLines ?? []).toList(), ), ), ),