From 225d8ce87ece0b1b1f1f1026d2de69008adb6694 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 26 Jan 2025 16:20:43 +0100 Subject: [PATCH 1/5] feat: CircleMarkers replicated in multi-worlds Impacted files: * `multi_worlds.dart`: added a couple of `CircleMarker`s * `painter.dart`: added replication of circle display and hit detection on all visible worlds --- example/lib/pages/multi_worlds.dart | 32 ++++ lib/src/layer/circle_layer/painter.dart | 194 +++++++++++++++++------- 2 files changed, 175 insertions(+), 51 deletions(-) diff --git a/example/lib/pages/multi_worlds.dart b/example/lib/pages/multi_worlds.dart index a82171bb1..dc560b2a6 100644 --- a/example/lib/pages/multi_worlds.dart +++ b/example/lib/pages/multi_worlds.dart @@ -15,6 +15,8 @@ class MultiWorldsPage extends StatefulWidget { } class _MultiWorldsPageState extends State { + final LayerHitNotifier _hitNotifier = ValueNotifier(null); + @override Widget build(BuildContext context) { return Scaffold( @@ -30,6 +32,36 @@ class _MultiWorldsPageState extends State { ), children: [ openStreetMapTileLayer, + GestureDetector( + onTap: () => ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(_hitNotifier.value!.hitValues.join(', ')), + duration: const Duration(seconds: 1), + showCloseIcon: true, + ), + ), + child: CircleLayer( + circles: [ + const CircleMarker( + point: LatLng(-27.466667, 153.033333), + radius: 10, + color: Colors.yellow, + borderColor: Colors.green, + borderStrokeWidth: 2, + hitValue: 'Brisbane', + ), + const CircleMarker( + point: LatLng(45.466667, 9.166667), + radius: 10, + color: Colors.green, + borderColor: Colors.red, + borderStrokeWidth: 2, + hitValue: 'Milan', + ), + ], + hitNotifier: _hitNotifier, + ), + ), MarkerLayer( markers: [ Marker( diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index 10d593e59..2f92a51f7 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -1,7 +1,6 @@ part of 'circle_layer.dart'; /// The [CustomPainter] used to draw [CircleMarker] for the [CircleLayer]. -@immutable base class CirclePainter extends HitDetectablePainter> { /// Reference to the list of [CircleMarker]s of the [CircleLayer]. @@ -23,72 +22,136 @@ base class CirclePainter required Offset point, required LatLng coordinate, }) { - final circle = element; // Should be optimized out by compiler, avoids lint - - final center = camera.getOffsetFromOrigin(circle.point); - final radius = circle.useRadiusInMeter - ? (center - - camera.getOffsetFromOrigin( - _distance.offset(circle.point, circle.radius, 180))) - .distance - : circle.radius; - - return pow(point.dx - center.dx, 2) + pow(point.dy - center.dy, 2) <= - radius * radius; + final worldWidth = _getWorldWidth(); + + /// Returns null if invisible, true if hit, false if not hit. + bool? checkIfHit(double worldWidth) { + final center = _getOffset(element, worldWidth); + final radius = _getRadius(center, element); + if (!_isVisible( + screenRect: _screenRect, + center: center, + radius: radius, + )) { + return null; + } + + return pow(point.dx - center.dx, 2) + pow(point.dy - center.dy, 2) <= + radius * radius; + } + + if (checkIfHit(0) ?? false) { + return true; + } + + // Repeat over all worlds (<--||-->) until culling determines that + // that element is out of view, and therefore all further elements in + // that direction will also be + if (worldWidth == 0) return false; + for (double shift = -worldWidth;; shift -= worldWidth) { + final isHit = checkIfHit(shift); + if (isHit == null) break; + if (isHit) return true; + } + for (double shift = worldWidth;; shift += worldWidth) { + final isHit = checkIfHit(shift); + if (isHit == null) break; + if (isHit) return true; + } + + return false; } @override Iterable> get elements => circles; + late Rect _screenRect; + @override void paint(Canvas canvas, Size size) { - final rect = Offset.zero & size; - canvas.clipRect(rect); + _screenRect = Offset.zero & size; + canvas.clipRect(_screenRect); + + final worldWidth = _getWorldWidth(); // Let's calculate all the points grouped by color and radius final points = >>{}; final pointsFilledBorder = >>{}; final pointsBorder = >>>{}; for (final circle in circles) { - final center = camera.getOffsetFromOrigin(circle.point); - final radius = circle.useRadiusInMeter - ? (center - - camera.getOffsetFromOrigin( - _distance.offset(circle.point, circle.radius, 180))) - .distance - : circle.radius; - points[circle.color] ??= {}; - points[circle.color]![radius] ??= []; - points[circle.color]![radius]!.add(center); - - if (circle.borderStrokeWidth > 0) { - // Check if color have some transparency or not - // As drawPoints is more efficient than drawCircle - if (circle.color.a == 1) { - double radiusBorder = circle.radius + circle.borderStrokeWidth; - if (circle.useRadiusInMeter) { - final rBorder = _distance.offset(circle.point, radiusBorder, 180); - final deltaBorder = center - camera.getOffsetFromOrigin(rBorder); - radiusBorder = deltaBorder.distance; + bool checkIfVisible(double worldWidth) { + bool result = false; + final center = _getOffset(circle, worldWidth); + + bool isVisible(double radius) { + if (_isVisible( + screenRect: _screenRect, + center: center, + radius: radius, + )) { + return result = true; } - pointsFilledBorder[circle.borderColor] ??= {}; - pointsFilledBorder[circle.borderColor]![radiusBorder] ??= []; - pointsFilledBorder[circle.borderColor]![radiusBorder]!.add(center); - } else { - double realRadius = circle.radius; - if (circle.useRadiusInMeter) { - final rBorder = _distance.offset(circle.point, realRadius, 180); - final deltaBorder = center - camera.getOffsetFromOrigin(rBorder); - realRadius = deltaBorder.distance; + return false; + } + + final radius = _getRadius(center, circle); + if (isVisible(radius)) { + points[circle.color] ??= {}; + points[circle.color]![radius] ??= []; + points[circle.color]![radius]!.add(center); + } + + if (circle.borderStrokeWidth > 0) { + // Check if color have some transparency or not + // As drawPoints is more efficient than drawCircle + if (circle.color.a == 1) { + double radiusBorder = circle.radius + circle.borderStrokeWidth; + if (circle.useRadiusInMeter) { + final rBorder = _distance.offset(circle.point, radiusBorder, 180); + final deltaBorder = center - camera.getOffsetFromOrigin(rBorder); + radiusBorder = deltaBorder.distance; + } + if (isVisible(radiusBorder)) { + pointsFilledBorder[circle.borderColor] ??= {}; + pointsFilledBorder[circle.borderColor]![radiusBorder] ??= []; + pointsFilledBorder[circle.borderColor]![radiusBorder]! + .add(center); + } + } else { + double realRadius = circle.radius; + if (circle.useRadiusInMeter) { + final rBorder = _distance.offset(circle.point, realRadius, 180); + final deltaBorder = center - camera.getOffsetFromOrigin(rBorder); + realRadius = deltaBorder.distance; + } + if (isVisible(circle.borderStrokeWidth)) { + pointsBorder[circle.borderColor] ??= {}; + pointsBorder[circle.borderColor]![circle.borderStrokeWidth] ??= + {}; + pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![ + realRadius] ??= []; + pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![ + realRadius]! + .add(center); + } } - pointsBorder[circle.borderColor] ??= {}; - pointsBorder[circle.borderColor]![circle.borderStrokeWidth] ??= {}; - pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![ - realRadius] ??= []; - pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![ - realRadius]! - .add(center); } + return result; + } + + checkIfVisible(0); + + // Repeat over all worlds (<--||-->) until culling determines that + // that element is out of view, and therefore all further elements in + // that direction will also be + if (worldWidth == 0) continue; + for (double shift = -worldWidth;; shift -= worldWidth) { + final isVisible = checkIfVisible(shift); + if (!isVisible) break; + } + for (double shift = worldWidth;; shift += worldWidth) { + final isVisible = checkIfVisible(shift); + if (!isVisible) break; } } @@ -145,4 +208,33 @@ base class CirclePainter @override bool shouldRepaint(CirclePainter oldDelegate) => circles != oldDelegate.circles || camera != oldDelegate.camera; + + Offset _getOffset(CircleMarker circle, double worldWidth) => + camera.getOffsetFromOrigin(circle.point).translate( + worldWidth, + 0, + ); + + /// Returns true if a centered circle with this radius is on the screen. + bool _isVisible({ + required Rect screenRect, + required Offset center, + required double radius, + }) => + screenRect.overlaps( + Rect.fromCircle( + center: center, + radius: radius, + ), + ); + + double _getRadius(Offset center, CircleMarker circle) => + circle.useRadiusInMeter + ? (center - + camera.getOffsetFromOrigin( + _distance.offset(circle.point, circle.radius, 180))) + .distance + : circle.radius; + + double _getWorldWidth() => camera.getWorldWidthAtZoom(); } From b69f223b343a98ce5c81ac38ee0a21be5e475e6b Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Tue, 28 Jan 2025 15:42:49 +0100 Subject: [PATCH 2/5] fixed "radius in meter" cases Impacted files: * `multi_worlds.dart`: used a radius in meter in the example * `painter.dart`: refactored and fixed --- example/lib/pages/multi_worlds.dart | 7 +- lib/src/layer/circle_layer/painter.dart | 101 ++++++++++-------------- 2 files changed, 47 insertions(+), 61 deletions(-) diff --git a/example/lib/pages/multi_worlds.dart b/example/lib/pages/multi_worlds.dart index dc560b2a6..677bed7b8 100644 --- a/example/lib/pages/multi_worlds.dart +++ b/example/lib/pages/multi_worlds.dart @@ -44,11 +44,12 @@ class _MultiWorldsPageState extends State { circles: [ const CircleMarker( point: LatLng(-27.466667, 153.033333), - radius: 10, - color: Colors.yellow, + radius: 1_000_000, + color: Color.from(alpha: .8, red: 1, green: 1, blue: 0), borderColor: Colors.green, - borderStrokeWidth: 2, + borderStrokeWidth: 100_000, hitValue: 'Brisbane', + useRadiusInMeter: true, ), const CircleMarker( point: LatLng(45.466667, 9.166667), diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index 2f92a51f7..ff558d5e0 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -23,15 +23,17 @@ base class CirclePainter required LatLng coordinate, }) { final worldWidth = _getWorldWidth(); + final radius = + _getRadiusInPixel(element, element.radius + element.borderStrokeWidth); + final initialCenter = _getOffset(element.point); /// Returns null if invisible, true if hit, false if not hit. - bool? checkIfHit(double worldWidth) { - final center = _getOffset(element, worldWidth); - final radius = _getRadius(center, element); + bool? checkIfHit(double shift) { + final center = initialCenter + Offset(shift, 0); if (!_isVisible( screenRect: _screenRect, center: center, - radius: radius, + radiusInPixel: radius, )) { return null; } @@ -79,61 +81,50 @@ base class CirclePainter final pointsFilledBorder = >>{}; final pointsBorder = >>>{}; for (final circle in circles) { - bool checkIfVisible(double worldWidth) { + final radiusWithoutBorder = _getRadiusInPixel(circle, circle.radius); + final radiusWithBorder = + _getRadiusInPixel(circle, circle.radius + circle.borderStrokeWidth); + final initialCenter = _getOffset(circle.point); + + bool checkIfVisible(double shift) { bool result = false; - final center = _getOffset(circle, worldWidth); + final center = initialCenter + Offset(shift, 0); bool isVisible(double radius) { if (_isVisible( screenRect: _screenRect, center: center, - radius: radius, + radiusInPixel: radius, )) { return result = true; } return false; } - final radius = _getRadius(center, circle); - if (isVisible(radius)) { + if (isVisible(radiusWithoutBorder)) { points[circle.color] ??= {}; - points[circle.color]![radius] ??= []; - points[circle.color]![radius]!.add(center); + points[circle.color]![radiusWithoutBorder] ??= []; + points[circle.color]![radiusWithoutBorder]!.add(center); } - if (circle.borderStrokeWidth > 0) { + if (circle.borderStrokeWidth > 0 && isVisible(radiusWithBorder)) { // Check if color have some transparency or not // As drawPoints is more efficient than drawCircle if (circle.color.a == 1) { - double radiusBorder = circle.radius + circle.borderStrokeWidth; - if (circle.useRadiusInMeter) { - final rBorder = _distance.offset(circle.point, radiusBorder, 180); - final deltaBorder = center - camera.getOffsetFromOrigin(rBorder); - radiusBorder = deltaBorder.distance; - } - if (isVisible(radiusBorder)) { - pointsFilledBorder[circle.borderColor] ??= {}; - pointsFilledBorder[circle.borderColor]![radiusBorder] ??= []; - pointsFilledBorder[circle.borderColor]![radiusBorder]! - .add(center); - } + pointsFilledBorder[circle.borderColor] ??= {}; + pointsFilledBorder[circle.borderColor]![radiusWithBorder] ??= []; + pointsFilledBorder[circle.borderColor]![radiusWithBorder]! + .add(center); } else { - double realRadius = circle.radius; - if (circle.useRadiusInMeter) { - final rBorder = _distance.offset(circle.point, realRadius, 180); - final deltaBorder = center - camera.getOffsetFromOrigin(rBorder); - realRadius = deltaBorder.distance; - } - if (isVisible(circle.borderStrokeWidth)) { - pointsBorder[circle.borderColor] ??= {}; - pointsBorder[circle.borderColor]![circle.borderStrokeWidth] ??= - {}; - pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![ - realRadius] ??= []; - pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![ - realRadius]! - .add(center); - } + final borderStrokeWidth = radiusWithBorder - radiusWithoutBorder; + final radiusForBorder = radiusWithoutBorder + borderStrokeWidth / 2; + pointsBorder[circle.borderColor] ??= {}; + pointsBorder[circle.borderColor]![borderStrokeWidth] ??= {}; + pointsBorder[circle.borderColor]![borderStrokeWidth]![ + radiusForBorder] ??= []; + pointsBorder[circle.borderColor]![borderStrokeWidth]![ + radiusForBorder]! + .add(center); } } return result; @@ -156,6 +147,8 @@ base class CirclePainter } // Now that all the points are grouped, let's draw them + + // First, the border when with non opaque disk final paintBorder = Paint()..style = PaintingStyle.stroke; for (final color in pointsBorder.keys) { final paint = paintBorder..color = color; @@ -209,32 +202,24 @@ base class CirclePainter bool shouldRepaint(CirclePainter oldDelegate) => circles != oldDelegate.circles || camera != oldDelegate.camera; - Offset _getOffset(CircleMarker circle, double worldWidth) => - camera.getOffsetFromOrigin(circle.point).translate( - worldWidth, - 0, - ); + Offset _getOffset(LatLng pos) => camera.getOffsetFromOrigin(pos); + + double _getRadiusInPixel(CircleMarker circle, double radius) => + circle.useRadiusInMeter + ? (_getOffset(circle.point) - + _getOffset(_distance.offset(circle.point, radius, 180))) + .distance + : radius; /// Returns true if a centered circle with this radius is on the screen. bool _isVisible({ required Rect screenRect, required Offset center, - required double radius, + required double radiusInPixel, }) => screenRect.overlaps( - Rect.fromCircle( - center: center, - radius: radius, - ), + Rect.fromCircle(center: center, radius: radiusInPixel), ); - double _getRadius(Offset center, CircleMarker circle) => - circle.useRadiusInMeter - ? (center - - camera.getOffsetFromOrigin( - _distance.offset(circle.point, circle.radius, 180))) - .distance - : circle.radius; - double _getWorldWidth() => camera.getWorldWidthAtZoom(); } From fb45e2ce12ba50313893a865f447d43cde868bc2 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Tue, 28 Jan 2025 15:45:03 +0100 Subject: [PATCH 3/5] meters refactoring --- example/lib/pages/multi_worlds.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/pages/multi_worlds.dart b/example/lib/pages/multi_worlds.dart index 677bed7b8..74c3d79cb 100644 --- a/example/lib/pages/multi_worlds.dart +++ b/example/lib/pages/multi_worlds.dart @@ -44,10 +44,10 @@ class _MultiWorldsPageState extends State { circles: [ const CircleMarker( point: LatLng(-27.466667, 153.033333), - radius: 1_000_000, + radius: 1000000, color: Color.from(alpha: .8, red: 1, green: 1, blue: 0), borderColor: Colors.green, - borderStrokeWidth: 100_000, + borderStrokeWidth: 100000, hitValue: 'Brisbane', useRadiusInMeter: true, ), From 4940c9aa880c0536fd16e735b236310f49255765 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Wed, 29 Jan 2025 17:02:49 +0100 Subject: [PATCH 4/5] border always in pixel - not meters Impacted files: * `circle.dart`: minor refactoring * `painter.dart`: border always in pixel - not meters --- example/lib/pages/circle.dart | 18 ++++++++----- lib/src/layer/circle_layer/painter.dart | 34 ++++++++++++------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/example/lib/pages/circle.dart b/example/lib/pages/circle.dart index ee967cb78..2d3d3797d 100644 --- a/example/lib/pages/circle.dart +++ b/example/lib/pages/circle.dart @@ -21,12 +21,15 @@ class _CirclePageState extends State { List? _prevHitValues; List>? _hoverCircles; + static const double _initialBorderStrokeWidth = 2; + static const double _hoverBorderStrokeWidth = 15; + final _circlesRaw = >[ CircleMarker( point: const LatLng(51.5, -0.09), color: Colors.white.withAlpha(178), borderColor: Colors.black, - borderStrokeWidth: 2, + borderStrokeWidth: _initialBorderStrokeWidth, useRadiusInMeter: false, radius: 100, hitValue: (title: 'White', subtitle: 'Radius in logical pixels'), @@ -35,7 +38,7 @@ class _CirclePageState extends State { point: const LatLng(51.5, -0.09), color: Colors.black.withAlpha(178), borderColor: Colors.black, - borderStrokeWidth: 2, + borderStrokeWidth: _initialBorderStrokeWidth, useRadiusInMeter: false, radius: 50, hitValue: ( @@ -48,9 +51,10 @@ class _CirclePageState extends State { // Dorney Lake is ~2km long color: Colors.green.withAlpha(229), borderColor: Colors.black, - borderStrokeWidth: 2, + borderStrokeWidth: _initialBorderStrokeWidth, useRadiusInMeter: true, - radius: 1000, // 1000 meters + radius: 1000, + // 1000 meters hitValue: ( title: 'Green', subtitle: 'Radius in meters, calibrated over ~2km rowing lake' @@ -87,10 +91,12 @@ class _CirclePageState extends State { return CircleMarker( point: original.point, - radius: original.radius + 6.5, + radius: original.radius + + _initialBorderStrokeWidth / 2 + + _hoverBorderStrokeWidth / 2, useRadiusInMeter: original.useRadiusInMeter, color: Colors.transparent, - borderStrokeWidth: 15, + borderStrokeWidth: _hoverBorderStrokeWidth, borderColor: Colors.green, ); }).toList(); diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index ff558d5e0..5600ef370 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -23,8 +23,7 @@ base class CirclePainter required LatLng coordinate, }) { final worldWidth = _getWorldWidth(); - final radius = - _getRadiusInPixel(element, element.radius + element.borderStrokeWidth); + final radius = _getRadiusInPixel(element, withBorder: true); final initialCenter = _getOffset(element.point); /// Returns null if invisible, true if hit, false if not hit. @@ -81,9 +80,8 @@ base class CirclePainter final pointsFilledBorder = >>{}; final pointsBorder = >>>{}; for (final circle in circles) { - final radiusWithoutBorder = _getRadiusInPixel(circle, circle.radius); - final radiusWithBorder = - _getRadiusInPixel(circle, circle.radius + circle.borderStrokeWidth); + final radiusWithoutBorder = _getRadiusInPixel(circle, withBorder: false); + final radiusWithBorder = _getRadiusInPixel(circle, withBorder: true); final initialCenter = _getOffset(circle.point); bool checkIfVisible(double shift) { @@ -116,14 +114,12 @@ base class CirclePainter pointsFilledBorder[circle.borderColor]![radiusWithBorder]! .add(center); } else { - final borderStrokeWidth = radiusWithBorder - radiusWithoutBorder; - final radiusForBorder = radiusWithoutBorder + borderStrokeWidth / 2; pointsBorder[circle.borderColor] ??= {}; - pointsBorder[circle.borderColor]![borderStrokeWidth] ??= {}; - pointsBorder[circle.borderColor]![borderStrokeWidth]![ - radiusForBorder] ??= []; - pointsBorder[circle.borderColor]![borderStrokeWidth]![ - radiusForBorder]! + pointsBorder[circle.borderColor]![circle.borderStrokeWidth] ??= {}; + pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![ + radiusWithoutBorder] ??= []; + pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![ + radiusWithoutBorder]! .add(center); } } @@ -164,7 +160,7 @@ base class CirclePainter } } - // Then the filled border in order to be under the circle + // Then the filled border in order to be under the disk final paintPoint = Paint() ..isAntiAlias = false ..strokeCap = StrokeCap.round; @@ -178,7 +174,7 @@ base class CirclePainter } } - // And then the circle + // And then the disk for (final color in points.keys) { final paint = paintPoint..color = color; final pointsByRadius = points[color]!; @@ -204,12 +200,14 @@ base class CirclePainter Offset _getOffset(LatLng pos) => camera.getOffsetFromOrigin(pos); - double _getRadiusInPixel(CircleMarker circle, double radius) => - circle.useRadiusInMeter + double _getRadiusInPixel(CircleMarker circle, {required bool withBorder}) => + (withBorder ? circle.borderStrokeWidth / 2 : 0) + + (circle.useRadiusInMeter ? (_getOffset(circle.point) - - _getOffset(_distance.offset(circle.point, radius, 180))) + _getOffset( + _distance.offset(circle.point, circle.radius, 180))) .distance - : radius; + : circle.radius); /// Returns true if a centered circle with this radius is on the screen. bool _isVisible({ From 74888014dac68890b661ed8e55b93802c33863ce Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Wed, 29 Jan 2025 19:15:16 +0100 Subject: [PATCH 5/5] fixed test border width - in pixel, not in meters --- example/lib/pages/multi_worlds.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/pages/multi_worlds.dart b/example/lib/pages/multi_worlds.dart index 74c3d79cb..3d40cc13f 100644 --- a/example/lib/pages/multi_worlds.dart +++ b/example/lib/pages/multi_worlds.dart @@ -47,7 +47,7 @@ class _MultiWorldsPageState extends State { radius: 1000000, color: Color.from(alpha: .8, red: 1, green: 1, blue: 0), borderColor: Colors.green, - borderStrokeWidth: 100000, + borderStrokeWidth: 2, hitValue: 'Brisbane', useRadiusInMeter: true, ),