From c2e891a77c429ff9de44e2441fe2fa5b711dc76a Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 9 Feb 2025 15:01:55 +0100 Subject: [PATCH 01/11] feat: add multi-world support to Polygons and Polylines New file: * `multi_world_layer_helper.dart`: Helper for multi world: e.g. draw and hitTest on all world copies. Impacted files: * `internal_hit_detectable.dart`: new `MultiWorldLayerHelper` field * `label.dart`: implemented position computation for new value `PolygonLabelPlacement.centroidWithMultiWorld` * `multi_worlds.dart`: added a `PolygonLayer` and a `PolylineLayer` * `offsets.dart`: added a `double shift = 0` parameter for `getOffset` and `getOffsetXY` * `circle_layer/painter.dart`: refactored using new class `MultiWorldLayerHelper` * `polygon_layer/painter.dart`: implemented the multi world using new class `MultiWorldLayerHelper` * `polyline_layer/painter.dart`: implemented the multi world using new class `MultiWorldLayerHelper` * `polygon.dart`: new value for `PolygonLabelPlacement` - `centroidWithMultiWorld`, as a fix for side-effects around the -180 or 180 longitude --- example/lib/pages/multi_worlds.dart | 81 ++++ lib/src/layer/circle_layer/painter.dart | 72 +--- lib/src/layer/polygon_layer/label.dart | 24 ++ lib/src/layer/polygon_layer/painter.dart | 352 ++++++++++-------- lib/src/layer/polygon_layer/polygon.dart | 3 + lib/src/layer/polyline_layer/painter.dart | 315 ++++++++-------- .../internal_hit_detectable.dart | 8 +- .../shared/multi_world_layer_helper.dart | 106 ++++++ lib/src/misc/offsets.dart | 15 +- 9 files changed, 589 insertions(+), 387 deletions(-) create mode 100644 lib/src/layer/shared/multi_world_layer_helper.dart diff --git a/example/lib/pages/multi_worlds.dart b/example/lib/pages/multi_worlds.dart index 8ef79549e..af3c0af55 100644 --- a/example/lib/pages/multi_worlds.dart +++ b/example/lib/pages/multi_worlds.dart @@ -127,6 +127,87 @@ class _MultiWorldsPageState extends State { ..._customMarkers, ], ), + GestureDetector( + onTap: () => ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(_hitNotifier.value!.hitValues.join(', ')), + duration: const Duration(seconds: 1), + showCloseIcon: true, + ), + ), + child: PolygonLayer( + hitNotifier: _hitNotifier, + simplificationTolerance: 0, + useAltRendering: true, + drawLabelsLast: false, + polygons: [ + Polygon( + label: 'Aloha!', + labelStyle: + const TextStyle(color: Colors.green, fontSize: 40), + labelPlacement: + PolygonLabelPlacement.centroidWithMultiWorld, + rotateLabel: false, + points: const [ + LatLng(40, 149), + LatLng(45, 159), + LatLng(50, 169), + LatLng(55, 179), + LatLng(50, -170), + LatLng(45, -160), + LatLng(40, -150), + LatLng(35, -160), + LatLng(30, -170), + LatLng(25, -180), + LatLng(30, 169), + LatLng(35, 159), + ], + holePointsList: const [ + [ + LatLng(45, 175), + LatLng(45, -175), + LatLng(35, -175), + LatLng(35, 175), + ], + ], + color: const Color(0xFFFF0000), + hitValue: 'Red Line, Across the universe...', + ), + ], + ), + ), + GestureDetector( + onTap: () => ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(_hitNotifier.value!.hitValues.join(', ')), + duration: const Duration(seconds: 1), + showCloseIcon: true, + ), + ), + child: PolylineLayer( + hitNotifier: _hitNotifier, + simplificationTolerance: 0, + polylines: [ + Polyline( + points: const [ + LatLng(-40, 150), + LatLng(-45, 160), + LatLng(-50, 170), + LatLng(-55, 180), + LatLng(-50, -170), + LatLng(-45, -160), + LatLng(-40, -150), + LatLng(-45, -140), + LatLng(-50, -130), + ], + useStrokeWidthInMeter: true, + strokeWidth: 500000, + color: const Color(0xFF0000FF), + hitValue: 'Blue Line', + ), + ], + ), + ), ], ), ], diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index 5600ef370..c310d5ad0 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -14,26 +14,19 @@ base class CirclePainter required super.hitNotifier, }); - static const _distance = Distance(); - @override bool elementHitTest( CircleMarker element, { required Offset point, required LatLng coordinate, }) { - final worldWidth = _getWorldWidth(); final radius = _getRadiusInPixel(element, withBorder: true); final initialCenter = _getOffset(element.point); /// Returns null if invisible, true if hit, false if not hit. bool? checkIfHit(double shift) { final center = initialCenter + Offset(shift, 0); - if (!_isVisible( - screenRect: _screenRect, - center: center, - radiusInPixel: radius, - )) { + if (!_isVisible(center: center, radiusInPixel: radius)) { return null; } @@ -41,39 +34,16 @@ base class CirclePainter 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; + return helper.checkIfHitInTheWorlds(checkIfHit); } @override Iterable> get elements => circles; - late Rect _screenRect; - @override void paint(Canvas canvas, Size size) { - _screenRect = Offset.zero & size; - canvas.clipRect(_screenRect); - - final worldWidth = _getWorldWidth(); + helper.setSize(size); + canvas.clipRect(helper.screenRect); // Let's calculate all the points grouped by color and radius final points = >>{}; @@ -84,16 +54,13 @@ base class CirclePainter final radiusWithBorder = _getRadiusInPixel(circle, withBorder: true); final initialCenter = _getOffset(circle.point); - bool checkIfVisible(double shift) { + /// Draws on a "single-world". Returns true if visible. + bool drawIfVisible(double shift) { bool result = false; final center = initialCenter + Offset(shift, 0); bool isVisible(double radius) { - if (_isVisible( - screenRect: _screenRect, - center: center, - radiusInPixel: radius, - )) { + if (_isVisible(center: center, radiusInPixel: radius)) { return result = true; } return false; @@ -126,20 +93,7 @@ base class CirclePainter 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; - } + helper.drawInTheWorlds(drawIfVisible); } // Now that all the points are grouped, let's draw them @@ -203,21 +157,15 @@ base class CirclePainter double _getRadiusInPixel(CircleMarker circle, {required bool withBorder}) => (withBorder ? circle.borderStrokeWidth / 2 : 0) + (circle.useRadiusInMeter - ? (_getOffset(circle.point) - - _getOffset( - _distance.offset(circle.point, circle.radius, 180))) - .distance + ? helper.getPixelWidthFromMeters(circle.point, circle.radius) : circle.radius); /// Returns true if a centered circle with this radius is on the screen. bool _isVisible({ - required Rect screenRect, required Offset center, required double radiusInPixel, }) => - screenRect.overlaps( + helper.screenRect.overlaps( Rect.fromCircle(center: center, radius: radiusInPixel), ); - - double _getWorldWidth() => camera.getWorldWidthAtZoom(); } diff --git a/lib/src/layer/polygon_layer/label.dart b/lib/src/layer/polygon_layer/label.dart index 39878c2ab..44920cbc8 100644 --- a/lib/src/layer/polygon_layer/label.dart +++ b/lib/src/layer/polygon_layer/label.dart @@ -60,6 +60,7 @@ LatLng _computeLabelPosition( ) { return switch (labelPlacement) { PolygonLabelPlacement.centroid => _computeCentroid(points), + PolygonLabelPlacement.centroidWithMultiWorld => _computeCentroidWithMultiWorld(points), PolygonLabelPlacement.polylabel => _computePolylabel(points), }; } @@ -72,6 +73,29 @@ LatLng _computeCentroid(List points) { ); } +/// Calculate the centroid of a given list of [LatLng] points with multiple worlds. +LatLng _computeCentroidWithMultiWorld(List points) { + if (points.isEmpty) return _computeCentroid(points); + const halfWorld = 180; + int count = 0; + double sum = 0; + late double lastLng; + for (final LatLng point in points) { + double lng = point.longitude; + count ++; + if (count > 1) { + if (lng - lastLng > halfWorld) { + lng -= 2 * halfWorld; + } else if (lng - lastLng < -halfWorld) { + lng += 2 * halfWorld; + } + } + lastLng = lng; + sum += lastLng; + } + return LatLng(points.map((e) => e.latitude).average, sum / count); +} + /// Use the Maxbox Polylabel algorithm to calculate the [LatLng] position for /// a given list of points. LatLng _computePolylabel(List points) { diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 4f261acfd..b7958659e 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -59,39 +59,49 @@ base class _PolygonPainter // continue; // } - final projectedCoords = getOffsetsXY( - camera: camera, - origin: hitTestCameraOrigin, - points: projectedPolygon.points, - ); - if (projectedCoords.first != projectedCoords.last) { - projectedCoords.add(projectedCoords.first); - } + /// Returns null if invisible, true if hit, false if not hit. + bool? checkIfHit(double shift) { + final projectedCoords = getOffsetsXY( + camera: camera, + origin: hitTestCameraOrigin, + points: projectedPolygon.points, + shift: shift, + ); + if (!helper.isVisible(projectedCoords)) { + return null; + } + if (projectedCoords.first != projectedCoords.last) { + projectedCoords.add(projectedCoords.first); + } - final isValidPolygon = projectedCoords.length >= 3; - final isInPolygon = - isValidPolygon && isPointInPolygon(point, projectedCoords); + final isValidPolygon = projectedCoords.length >= 3; + final isInPolygon = + isValidPolygon && isPointInPolygon(point, projectedCoords); - final isInHole = projectedPolygon.holePoints.any( - (points) { - final projectedHoleCoords = getOffsetsXY( - camera: camera, - origin: hitTestCameraOrigin, - points: points, - ); - if (projectedHoleCoords.first != projectedHoleCoords.last) { - projectedHoleCoords.add(projectedHoleCoords.first); - } + final isInHole = projectedPolygon.holePoints.any( + (points) { + final projectedHoleCoords = getOffsetsXY( + camera: camera, + origin: hitTestCameraOrigin, + points: points, + shift: shift, + ); + if (projectedHoleCoords.first != projectedHoleCoords.last) { + projectedHoleCoords.add(projectedHoleCoords.first); + } - final isValidHolePolygon = projectedHoleCoords.length >= 3; - return isValidHolePolygon && - isPointInPolygon(point, projectedHoleCoords); - }, - ); + final isValidHolePolygon = projectedHoleCoords.length >= 3; + return isValidHolePolygon && + isPointInPolygon(point, projectedHoleCoords); + }, + ); - // Second check handles case where polygon outline intersects a hole, - // ensuring that the hit matches with the visual representation - return (isInPolygon && !isInHole) || (!isInPolygon && isInHole); + // Second check handles case where polygon outline intersects a hole, + // ensuring that the hit matches with the visual representation + return (isInPolygon && !isInHole) || (!isInPolygon && isInHole); + } + + return helper.checkIfHitInTheWorlds(checkIfHit); } @override @@ -100,6 +110,7 @@ base class _PolygonPainter @override void paint(Canvas canvas, Size size) { const checkOpacity = true; // for debugging purposes only, should be true + helper.setSize(size); final trianglePoints = []; @@ -190,8 +201,38 @@ base class _PolygonPainter lastHash = null; } - final origin = - camera.projectAtZoom(camera.center) - camera.size.center(Offset.zero); + final origin = helper.origin; + + /// Draws labels on a "single-world". Returns true if visible. + bool drawLabelIfVisible( + double shift, + _ProjectedPolygon projectedPolygon, + ) { + final polygon = projectedPolygon.polygon; + final painter = _buildLabelTextPainter( + mapSize: camera.size, + placementPoint: getOffset( + camera, + origin, + polygon.labelPosition, + shift: shift, + ), + bounds: _getBounds(origin, polygon), + textPainter: polygon.textPainter!, + rotationRad: camera.rotationRad, + rotate: polygon.rotateLabel, + padding: 20, + ); + if (painter == null) { + return false; + } + + // Flush the batch before painting to preserve stacking. + drawPaths(); + + painter(canvas); + return true; + } // Main loop constructing batched fill and border paths from given polygons. for (int i = 0; i <= polygons.length - 1; i++) { @@ -201,126 +242,141 @@ base class _PolygonPainter final polygonTriangles = triangles?[i]; - final fillOffsets = getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolygon.points, - holePoints: - polygonTriangles != null ? projectedPolygon.holePoints : null, - ); - - if (debugAltRenderer) { - const offsetsLabelStyle = TextStyle( - color: Color(0xFF000000), - fontSize: 16, + /// Draws on a "single-world". Returns true if visible. + bool drawIfVisible(double shift) { + final fillOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + holePoints: + polygonTriangles != null ? projectedPolygon.holePoints : null, + shift: shift, ); - for (int i = 0; i < fillOffsets.length; i++) { - TextPainter( - text: TextSpan( - text: i.toString(), - style: offsetsLabelStyle, - ), - textDirection: TextDirection.ltr, - ) - ..layout(maxWidth: 100) - ..paint(canvas, fillOffsets[i]); + if (!helper.isVisible(fillOffsets)) { + return false; } - } - // The hash is based on the polygons visual properties. If the hash from - // the current and the previous polygon no longer match, we need to flush - // the batch previous polygons. - // We also need to flush if the opacity is not 1 or 0, so that they get - // mixed properly. Otherwise, holes get cut, or colors aren't mixed, - // depending on the holes handler. - final hash = polygon.renderHashCode; - final opacity = polygon.color?.a ?? 0; - if (lastHash != hash || (checkOpacity && opacity > 0 && opacity < 1)) { - drawPaths(); - } - lastPolygon = polygon; - lastHash = hash; - - // First add fills and borders to path. - if (polygon.color != null) { - if (polygonTriangles != null) { - final len = polygonTriangles.length; - for (int i = 0; i < len; ++i) { - trianglePoints.add(fillOffsets[polygonTriangles[i]]); + if (debugAltRenderer) { + const offsetsLabelStyle = TextStyle( + color: Color(0xFF000000), + fontSize: 16, + ); + + for (int i = 0; i < fillOffsets.length; i++) { + TextPainter( + text: TextSpan( + text: i.toString(), + style: offsetsLabelStyle, + ), + textDirection: TextDirection.ltr, + ) + ..layout(maxWidth: 100) + ..paint(canvas, fillOffsets[i]); } - } else { - filledPath.addPolygon(fillOffsets, true); } - } - if (polygon.borderStrokeWidth > 0.0) { - _addBorderToPath( - borderPath, - polygon, - getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolygon.points, - ), - size, - canvas, - _getBorderPaint(polygon), - polygon.borderStrokeWidth, - ); - } + // The hash is based on the polygons visual properties. If the hash from + // the current and the previous polygon no longer match, we need to flush + // the batch previous polygons. + // We also need to flush if the opacity is not 1 or 0, so that they get + // mixed properly. Otherwise, holes get cut, or colors aren't mixed, + // depending on the holes handler. + final hash = polygon.renderHashCode; + final opacity = polygon.color?.a ?? 0; + if (lastHash != hash || (checkOpacity && opacity > 0 && opacity < 1)) { + drawPaths(); + } + lastPolygon = polygon; + lastHash = hash; + + // First add fills and borders to path. + if (polygon.color != null) { + if (polygonTriangles != null) { + final len = polygonTriangles.length; + for (int i = 0; i < len; ++i) { + trianglePoints.add(fillOffsets[polygonTriangles[i]]); + } + } else { + filledPath.addPolygon(fillOffsets, true); + } + } - // Afterwards deal with more complicated holes. - // Improper handling of opacity and fill methods may result in normal - // polygons cutting holes into other polygons, when they should be mixing: - // https://github.com/fleaflet/flutter_map/issues/1898. - final holePointsList = polygon.holePointsList; - if (holePointsList != null && holePointsList.isNotEmpty) { - // See `Path.combine` comments below - // Avoids failing to cut holes if the winding directions of the holes - // and the normal points are the same - filledPath.fillType = PathFillType.evenOdd; - - for (final singleHolePoints in projectedPolygon.holePoints) { - final holeOffsets = getOffsetsXY( - camera: camera, - origin: origin, - points: singleHolePoints, + if (polygon.borderStrokeWidth > 0.0) { + _addBorderToPath( + borderPath, + polygon, + getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + shift: shift, + ), + size, + canvas, + _getBorderPaint(polygon), + polygon.borderStrokeWidth, ); - filledPath.addPolygon(holeOffsets, true); - - // TODO: Potentially more efficient and may change the need to do - // opacity checking - needs testing. However, - // https://github.com/flutter/flutter/issues/44572 prevents this. - // Also need to verify if `xor` or `difference` is preferred. - /*filledPath = Path.combine( - PathOperation.xor, - filledPath, - Path()..addPolygon(holeOffsets, true), - );*/ } - if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { - final borderPaint = _getBorderPaint(polygon); + // Afterwards deal with more complicated holes. + // Improper handling of opacity and fill methods may result in normal + // polygons cutting holes into other polygons, when they should be mixing: + // https://github.com/fleaflet/flutter_map/issues/1898. + final holePointsList = polygon.holePointsList; + if (holePointsList != null && holePointsList.isNotEmpty) { + // See `Path.combine` comments below + // Avoids failing to cut holes if the winding directions of the holes + // and the normal points are the same + filledPath.fillType = PathFillType.evenOdd; + for (final singleHolePoints in projectedPolygon.holePoints) { final holeOffsets = getOffsetsXY( camera: camera, origin: origin, points: singleHolePoints, + shift: shift, ); - _addBorderToPath( - borderPath, - polygon, - holeOffsets, - size, - canvas, - borderPaint, - polygon.borderStrokeWidth, - ); + filledPath.addPolygon(holeOffsets, true); + + // TODO: Potentially more efficient and may change the need to do + // opacity checking - needs testing. However, + // https://github.com/flutter/flutter/issues/44572 prevents this. + // Also need to verify if `xor` or `difference` is preferred. + /*filledPath = Path.combine( + PathOperation.xor, + filledPath, + Path()..addPolygon(holeOffsets, true), + );*/ + } + + if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { + final borderPaint = _getBorderPaint(polygon); + for (final singleHolePoints in projectedPolygon.holePoints) { + final holeOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: singleHolePoints, + shift: shift, + ); + _addBorderToPath( + borderPath, + polygon, + holeOffsets, + size, + canvas, + borderPaint, + polygon.borderStrokeWidth, + ); + } } } + + return true; } + helper.drawInTheWorlds(drawIfVisible); + if (!drawLabelsLast && polygonLabels && polygon.textPainter != null) { // Labels are expensive because: // * they themselves cannot easily be pulled into our batched path @@ -331,22 +387,9 @@ base class _PolygonPainter // The painter will be null if the layOuting algorithm determined that // there isn't enough space. - final painter = _buildLabelTextPainter( - mapSize: camera.size, - placementPoint: getOffset(camera, origin, polygon.labelPosition), - bounds: _getBounds(origin, polygon), - textPainter: polygon.textPainter!, - rotationRad: camera.rotationRad, - rotate: polygon.rotateLabel, - padding: 20, + helper.drawInTheWorlds( + (double shift) => drawLabelIfVisible(shift, projectedPolygon), ); - - if (painter != null) { - // Flush the batch before painting to preserve stacking. - drawPaths(); - - painter(canvas); - } } } @@ -357,21 +400,12 @@ base class _PolygonPainter if (projectedPolygon.points.isEmpty) { continue; } - final polygon = projectedPolygon.polygon; - final textPainter = polygon.textPainter; - if (textPainter != null) { - final painter = _buildLabelTextPainter( - mapSize: camera.size, - placementPoint: getOffset(camera, origin, polygon.labelPosition), - bounds: _getBounds(origin, polygon), - textPainter: textPainter, - rotationRad: camera.rotationRad, - rotate: polygon.rotateLabel, - padding: 20, - ); - - painter?.call(canvas); + if (projectedPolygon.polygon.textPainter == null) { + continue; } + helper.drawInTheWorlds( + (double shift) => drawLabelIfVisible(shift, projectedPolygon), + ); } } } diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index 54cf5b15b..8e14bcf91 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -5,6 +5,9 @@ enum PolygonLabelPlacement { /// Use the centroid of the [Polygon] outline as position for the label. centroid, + /// Use the centroid in a multi-world as position for the label. + centroidWithMultiWorld, + /// Use the Mapbox Polylabel algorithm as position for the label. polylabel, } diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index c78023bb2..c8acc6281 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -34,35 +34,42 @@ base class _PolylinePainter // continue; // } - final offsets = getOffsetsXY( - camera: camera, - origin: hitTestCameraOrigin, - points: projectedPolyline.points, - ); - final strokeWidth = polyline.useStrokeWidthInMeter - ? _metersToStrokeWidth( - hitTestCameraOrigin, - _unproject(projectedPolyline.points.first), - offsets.first, - polyline.strokeWidth, - ) - : polyline.strokeWidth; - final hittableDistance = math.max( - strokeWidth / 2 + polyline.borderStrokeWidth / 2, - minimumHitbox, - ); + /// Returns null if invisible, true if hit, false if not hit. + bool? checkIfHit(double shift) { + final offsets = getOffsetsXY( + camera: camera, + origin: hitTestCameraOrigin, + points: projectedPolyline.points, + shift: shift, + ); + if (!helper.isVisible(offsets)) { + return null; + } + final strokeWidth = polyline.useStrokeWidthInMeter + ? helper.getPixelWidthFromMeters( + projectedPolyline.polyline.points.first, + polyline.strokeWidth, + ) + : polyline.strokeWidth; + final hittableDistance = math.max( + strokeWidth / 2 + polyline.borderStrokeWidth / 2, + minimumHitbox, + ); + + for (int i = 0; i < offsets.length - 1; i++) { + final o1 = offsets[i]; + final o2 = offsets[i + 1]; - for (int i = 0; i < offsets.length - 1; i++) { - final o1 = offsets[i]; - final o2 = offsets[i + 1]; + final distanceSq = + getSqSegDist(point.dx, point.dy, o1.dx, o1.dy, o2.dx, o2.dy); - final distanceSq = - getSqSegDist(point.dx, point.dy, o1.dx, o1.dy, o2.dx, o2.dy); + if (distanceSq <= hittableDistance * hittableDistance) return true; + } - if (distanceSq <= hittableDistance * hittableDistance) return true; + return false; } - return false; + return helper.checkIfHitInTheWorlds(checkIfHit); } @override @@ -70,7 +77,7 @@ base class _PolylinePainter @override void paint(Canvas canvas, Size size) { - final rect = Offset.zero & size; + helper.setSize(size); var path = ui.Path(); var borderPath = ui.Path(); @@ -86,7 +93,7 @@ base class _PolylinePainter final hasBorder = borderPaint != null && filterPaint != null; if (hasBorder) { if (needsLayerSaving) { - canvas.saveLayer(rect, Paint()); + canvas.saveLayer(helper.screenRect, Paint()); } canvas.drawPath(borderPath, borderPaint!); @@ -107,142 +114,150 @@ base class _PolylinePainter paint = Paint(); } - final origin = - camera.projectAtZoom(camera.center) - camera.size.center(Offset.zero); + final origin = helper.origin; for (final projectedPolyline in polylines) { final polyline = projectedPolyline.polyline; - final offsets = getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolyline.points, - ); - if (offsets.isEmpty) { + if (polyline.points.isEmpty) { continue; } - final hash = polyline.renderHashCode; - if (needsLayerSaving || (lastHash != null && lastHash != hash)) { - drawPaths(); - } - lastHash = hash; - needsLayerSaving = polyline.color.a < 1 || - (polyline.gradientColors?.any((c) => c.a < 1) ?? false); - - // strokeWidth, or strokeWidth + borderWidth if relevant. - late double largestStrokeWidth; - - late final double strokeWidth; - if (polyline.useStrokeWidthInMeter) { - strokeWidth = _metersToStrokeWidth( - origin, - _unproject(projectedPolyline.points.first), - offsets.first, - polyline.strokeWidth, + /// Draws on a "single-world". Returns true if visible. + bool drawIfVisible(double shift) { + final offsets = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolyline.points, + shift: shift, ); - } else { - strokeWidth = polyline.strokeWidth; - } - largestStrokeWidth = strokeWidth; - - final isSolid = polyline.pattern == const StrokePattern.solid(); - final isDashed = polyline.pattern.segments != null; - final isDotted = polyline.pattern.spacingFactor != null; - - paint = Paint() - ..strokeWidth = strokeWidth - ..strokeCap = polyline.strokeCap - ..strokeJoin = polyline.strokeJoin - ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke - ..blendMode = BlendMode.srcOver; - - if (polyline.gradientColors == null) { - paint.color = polyline.color; - } else { - polyline.gradientColors!.isNotEmpty - ? paint.shader = _paintGradient(polyline, offsets) - : paint.color = polyline.color; - } + if (!helper.isVisible(offsets)) { + return false; + } - if (polyline.borderStrokeWidth > 0.0) { - // Outlined lines are drawn by drawing a thicker path underneath, then - // stenciling the middle (in case the line fill is transparent), and - // finally drawing the line fill. - largestStrokeWidth = strokeWidth + polyline.borderStrokeWidth; - borderPaint = Paint() - ..color = polyline.borderColor - ..strokeWidth = strokeWidth + polyline.borderStrokeWidth - ..strokeCap = polyline.strokeCap - ..strokeJoin = polyline.strokeJoin - ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke - ..blendMode = BlendMode.srcOver; + final hash = polyline.renderHashCode; + if (needsLayerSaving || (lastHash != null && lastHash != hash)) { + drawPaths(); + } + lastHash = hash; + needsLayerSaving = polyline.color.a < 1 || + (polyline.gradientColors?.any((c) => c.a < 1) ?? false); + + // strokeWidth, or strokeWidth + borderWidth if relevant. + late double largestStrokeWidth; - filterPaint = Paint() - ..color = polyline.borderColor.withAlpha(255) + late final double strokeWidth; + if (polyline.useStrokeWidthInMeter) { + strokeWidth = helper.getPixelWidthFromMeters( + projectedPolyline.polyline.points.first, + polyline.strokeWidth, + ); + } else { + strokeWidth = polyline.strokeWidth; + } + largestStrokeWidth = strokeWidth; + + final isSolid = polyline.pattern == const StrokePattern.solid(); + final isDashed = polyline.pattern.segments != null; + final isDotted = polyline.pattern.spacingFactor != null; + + paint = Paint() ..strokeWidth = strokeWidth ..strokeCap = polyline.strokeCap ..strokeJoin = polyline.strokeJoin ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke - ..blendMode = BlendMode.dstOut; - } + ..blendMode = BlendMode.srcOver; + + if (polyline.gradientColors == null) { + paint.color = polyline.color; + } else { + polyline.gradientColors!.isNotEmpty + ? paint.shader = _paintGradient(polyline, offsets) + : paint.color = polyline.color; + } - final radius = paint.strokeWidth / 2; - final borderRadius = (borderPaint?.strokeWidth ?? 0) / 2; + if (polyline.borderStrokeWidth > 0.0) { + // Outlined lines are drawn by drawing a thicker path underneath, then + // stenciling the middle (in case the line fill is transparent), and + // finally drawing the line fill. + largestStrokeWidth = strokeWidth + polyline.borderStrokeWidth; + borderPaint = Paint() + ..color = polyline.borderColor + ..strokeWidth = strokeWidth + polyline.borderStrokeWidth + ..strokeCap = polyline.strokeCap + ..strokeJoin = polyline.strokeJoin + ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke + ..blendMode = BlendMode.srcOver; + + filterPaint = Paint() + ..color = polyline.borderColor.withAlpha(255) + ..strokeWidth = strokeWidth + ..strokeCap = polyline.strokeCap + ..strokeJoin = polyline.strokeJoin + ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke + ..blendMode = BlendMode.dstOut; + } - final List paths = []; - if (borderPaint != null && filterPaint != null) { - paths.add(borderPath); - paths.add(filterPath); - } - paths.add(path); - if (isSolid) { - final SolidPixelHiker hiker = SolidPixelHiker( - offsets: offsets, - closePath: false, - canvasSize: size, - strokeWidth: largestStrokeWidth, - ); - hiker.addAllVisibleSegments(paths); - } else if (isDotted) { - final DottedPixelHiker hiker = DottedPixelHiker( - offsets: offsets, - stepLength: strokeWidth * polyline.pattern.spacingFactor!, - patternFit: polyline.pattern.patternFit!, - closePath: false, - canvasSize: size, - strokeWidth: largestStrokeWidth, - ); + final radius = paint.strokeWidth / 2; + final borderRadius = (borderPaint?.strokeWidth ?? 0) / 2; - final List radii = []; + final List paths = []; if (borderPaint != null && filterPaint != null) { - radii.add(borderRadius); - radii.add(radius); + paths.add(borderPath); + paths.add(filterPath); } - radii.add(radius); - - for (final visibleDot in hiker.getAllVisibleDots()) { - for (int i = 0; i < paths.length; i++) { - paths[i] - .addOval(Rect.fromCircle(center: visibleDot, radius: radii[i])); + paths.add(path); + if (isSolid) { + final SolidPixelHiker hiker = SolidPixelHiker( + offsets: offsets, + closePath: false, + canvasSize: size, + strokeWidth: largestStrokeWidth, + ); + hiker.addAllVisibleSegments(paths); + } else if (isDotted) { + final DottedPixelHiker hiker = DottedPixelHiker( + offsets: offsets, + stepLength: strokeWidth * polyline.pattern.spacingFactor!, + patternFit: polyline.pattern.patternFit!, + closePath: false, + canvasSize: size, + strokeWidth: largestStrokeWidth, + ); + + final List radii = []; + if (borderPaint != null && filterPaint != null) { + radii.add(borderRadius); + radii.add(radius); } - } - } else if (isDashed) { - final DashedPixelHiker hiker = DashedPixelHiker( - offsets: offsets, - segmentValues: polyline.pattern.segments!, - patternFit: polyline.pattern.patternFit!, - closePath: false, - canvasSize: size, - strokeWidth: largestStrokeWidth, - ); + radii.add(radius); - for (final visibleSegment in hiker.getAllVisibleSegments()) { - for (final path in paths) { - path.moveTo(visibleSegment.begin.dx, visibleSegment.begin.dy); - path.lineTo(visibleSegment.end.dx, visibleSegment.end.dy); + for (final visibleDot in hiker.getAllVisibleDots()) { + for (int i = 0; i < paths.length; i++) { + paths[i].addOval( + Rect.fromCircle(center: visibleDot, radius: radii[i])); + } + } + } else if (isDashed) { + final DashedPixelHiker hiker = DashedPixelHiker( + offsets: offsets, + segmentValues: polyline.pattern.segments!, + patternFit: polyline.pattern.patternFit!, + closePath: false, + canvasSize: size, + strokeWidth: largestStrokeWidth, + ); + + for (final visibleSegment in hiker.getAllVisibleSegments()) { + for (final path in paths) { + path.moveTo(visibleSegment.begin.dx, visibleSegment.begin.dy); + path.lineTo(visibleSegment.end.dx, visibleSegment.end.dy); + } } } + return true; } + + helper.drawInTheWorlds(drawIfVisible); } drawPaths(); @@ -267,26 +282,6 @@ base class _PolylinePainter .toList(); } - double _metersToStrokeWidth( - Offset origin, - LatLng p0, - Offset o0, - double strokeWidthInMeters, - ) { - final r = _distance.offset(p0, strokeWidthInMeters, 180); - var delta = o0 - getOffset(camera, origin, r); - final worldSize = camera.crs.scale(camera.zoom); - if (delta.dx < 0) { - delta = delta.translate(worldSize, 0); - } else if (delta.dx >= worldSize) { - delta = delta.translate(-worldSize, 0); - } - return delta.distance; - } - - LatLng _unproject(Offset p0) => - camera.crs.projection.unprojectXY(p0.dx, p0.dy); - @override bool shouldRepaint(_PolylinePainter oldDelegate) => polylines != oldDelegate.polylines || @@ -294,5 +289,3 @@ base class _PolylinePainter hitNotifier != oldDelegate.hitNotifier || minimumHitbox != oldDelegate.minimumHitbox; } - -const _distance = Distance(); diff --git a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart index b59135d7a..cd9536258 100644 --- a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart +++ b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/shared/multi_world_layer_helper.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @@ -21,11 +22,16 @@ mixin HitDetectableElement { @internal abstract base class HitDetectablePainter> extends CustomPainter { - HitDetectablePainter({required this.camera, required this.hitNotifier}); + HitDetectablePainter({ + required this.camera, + required this.hitNotifier, + }) : helper = MultiWorldLayerHelper(camera); final MapCamera camera; final LayerHitNotifier? hitNotifier; + final MultiWorldLayerHelper helper; + /// Elements that should be possibly be hit tested by [elementHitTest] /// ([hitTest]) /// diff --git a/lib/src/layer/shared/multi_world_layer_helper.dart b/lib/src/layer/shared/multi_world_layer_helper.dart new file mode 100644 index 000000000..fef59cddb --- /dev/null +++ b/lib/src/layer/shared/multi_world_layer_helper.dart @@ -0,0 +1,106 @@ +import 'dart:ui'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +/// Helper for multi world: e.g. draw and hitTest on all world copies. +class MultiWorldLayerHelper { + /// Helper for multi world. + MultiWorldLayerHelper(this.camera); + + static const _distance = Distance(); + + /// Sets the screen size. To be called immediately after `paint`. + void setSize(Size size) => _screenRect = Offset.zero & size; + + late Rect _screenRect; + + /// Screen rect. + Rect get screenRect => _screenRect; + + /// Camera. + final MapCamera camera; + + /// Returns true if the points are visible on the screen. + bool isVisible(List points) { + if (points.isEmpty) { + return false; + } + double minX; + double maxX; + double minY; + double maxY; + minX = maxX = points.first.dx; + minY = maxY = points.first.dy; + for (final Offset offset in points) { + if (screenRect.contains(offset)) return true; + if (minX > offset.dx) minX = offset.dx; + if (minY > offset.dy) minY = offset.dy; + if (maxX < offset.dx) maxX = offset.dx; + if (maxY < offset.dy) maxY = offset.dy; + } + return screenRect.overlaps(Rect.fromLTRB(minX, minY, maxX, maxY)); + } + + /// Returns true if hit in all world copies. + /// + /// Uses a "single-world" method that returns* + /// * null if invisible + /// * true if hit + /// * false if not hit + bool checkIfHitInTheWorlds(bool? Function(double) checkIfHit) { + 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; + } + + /// Draws in all world copies. + /// + /// Uses a "single-world" method that returns* + /// * true if visible + /// * false if not visible + void drawInTheWorlds(bool Function(double) drawIfVisible) { + drawIfVisible(0); + + if (worldWidth == 0) return; + for (double shift = -worldWidth;; shift -= worldWidth) { + final isVisible = drawIfVisible(shift); + if (!isVisible) break; + } + for (double shift = worldWidth;; shift += worldWidth) { + final isVisible = drawIfVisible(shift); + if (!isVisible) break; + } + } + + /// Returns the origin of the camera. + Offset get origin => + camera.projectAtZoom(camera.center) - camera.size.center(Offset.zero); + + /// Returns the world size in pixels. + double get worldWidth => camera.getWorldWidthAtZoom(); + + /// Returns the width in pixel of a width in meters, for a given [point]. + double getPixelWidthFromMeters(LatLng point, double meters) => + (camera.getOffsetFromOrigin(point) - + camera.getOffsetFromOrigin(_distance.offset(point, meters, 180))) + .distance; +} diff --git a/lib/src/misc/offsets.dart b/lib/src/misc/offsets.dart index 372891b6c..fed76d672 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -5,13 +5,19 @@ import 'package:flutter_map/src/geo/crs.dart'; import 'package:latlong2/latlong.dart'; /// Calculate the [Offset] for the [LatLng] point. -Offset getOffset(MapCamera camera, Offset origin, LatLng point) { +Offset getOffset( + MapCamera camera, + Offset origin, + LatLng point, { + double shift = 0, +}) { final crs = camera.crs; final zoomScale = crs.scale(camera.zoom); final (x, y) = crs.latLngToXY(point, zoomScale); - return Offset(x - origin.dx, y - origin.dy); + return Offset(x - origin.dx + shift, y - origin.dy); } +// TODO not sure if still relevant /// Calculate the [Offset]s for the list of [LatLng] points. List getOffsets(MapCamera camera, Offset origin, List points) { // Critically create as little garbage as possible. This is called on every frame. @@ -46,6 +52,7 @@ List getOffsetsXY({ required Offset origin, required List points, List>? holePoints, + double shift = 0, }) { // Critically create as little garbage as possible. This is called on every frame. final crs = camera.crs; @@ -97,7 +104,7 @@ List getOffsetsXY({ for (int i = 0; i < len; ++i) { final p = realPoints.elementAt(i); final (x, y) = crs.transform(p.dx + addedWorldWidth, p.dy, zoomScale); - v[i] = Offset(x + ox, y + oy); + v[i] = Offset(x + ox + shift, y + oy); } return v; } @@ -106,7 +113,7 @@ List getOffsetsXY({ for (int i = 0; i < len; ++i) { final p = realPoints.elementAt(i); final (x, y) = crs.transform(p.dx + addedWorldWidth, p.dy, zoomScale); - v[i] = Offset(x + ox, y + oy); + v[i] = Offset(x + ox + shift, y + oy); } return v; } From 7a730774cad3df6c9fb132a56ef37eb6a87dcd8b Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 9 Feb 2025 15:15:08 +0100 Subject: [PATCH 02/11] formatted label.dart --- lib/src/layer/polygon_layer/label.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/polygon_layer/label.dart b/lib/src/layer/polygon_layer/label.dart index 44920cbc8..c7add1a16 100644 --- a/lib/src/layer/polygon_layer/label.dart +++ b/lib/src/layer/polygon_layer/label.dart @@ -60,7 +60,8 @@ LatLng _computeLabelPosition( ) { return switch (labelPlacement) { PolygonLabelPlacement.centroid => _computeCentroid(points), - PolygonLabelPlacement.centroidWithMultiWorld => _computeCentroidWithMultiWorld(points), + PolygonLabelPlacement.centroidWithMultiWorld => + _computeCentroidWithMultiWorld(points), PolygonLabelPlacement.polylabel => _computePolylabel(points), }; } @@ -82,7 +83,7 @@ LatLng _computeCentroidWithMultiWorld(List points) { late double lastLng; for (final LatLng point in points) { double lng = point.longitude; - count ++; + count++; if (count > 1) { if (lng - lastLng > halfWorld) { lng -= 2 * halfWorld; From e77ccfb1da3ba6ff6ce94a281550034732aedf14 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 13 Feb 2025 18:51:10 +0000 Subject: [PATCH 03/11] Converted `MultiWorldLayerHelper` to standalone mixin Deduplicated `.checkIfHitInTheWorlds` & `.drawInTheWorlds` Minor renaming --- lib/src/layer/circle_layer/circle_layer.dart | 1 + lib/src/layer/circle_layer/painter.dart | 31 ++--- lib/src/layer/polygon_layer/painter.dart | 49 ++++---- .../layer/polygon_layer/polygon_layer.dart | 1 + lib/src/layer/polyline_layer/painter.dart | 30 +++-- .../layer/polyline_layer/polyline_layer.dart | 1 + .../internal_hit_detectable.dart | 5 +- .../shared/multi_world_layer_helper.dart | 108 ++++++++---------- 8 files changed, 107 insertions(+), 119 deletions(-) diff --git a/lib/src/layer/circle_layer/circle_layer.dart b/lib/src/layer/circle_layer/circle_layer.dart index 8090fa582..786282074 100644 --- a/lib/src/layer/circle_layer/circle_layer.dart +++ b/lib/src/layer/circle_layer/circle_layer.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/multi_world_layer_helper.dart'; import 'package:latlong2/latlong.dart' hide Path; part 'circle_marker.dart'; diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index c310d5ad0..be17d603a 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -2,7 +2,8 @@ part of 'circle_layer.dart'; /// The [CustomPainter] used to draw [CircleMarker] for the [CircleLayer]. base class CirclePainter - extends HitDetectablePainter> { + extends HitDetectablePainter> + with MultiWorldLayerHelper { /// Reference to the list of [CircleMarker]s of the [CircleLayer]. final List> circles; @@ -23,7 +24,6 @@ base class CirclePainter final radius = _getRadiusInPixel(element, withBorder: true); final initialCenter = _getOffset(element.point); - /// Returns null if invisible, true if hit, false if not hit. bool? checkIfHit(double shift) { final center = initialCenter + Offset(shift, 0); if (!_isVisible(center: center, radiusInPixel: radius)) { @@ -34,7 +34,7 @@ base class CirclePainter radius * radius; } - return helper.checkIfHitInTheWorlds(checkIfHit); + return workAcrossWorlds(checkIfHit); } @override @@ -42,8 +42,8 @@ base class CirclePainter @override void paint(Canvas canvas, Size size) { - helper.setSize(size); - canvas.clipRect(helper.screenRect); + super.paint(canvas, size); + canvas.clipRect(viewportRect); // Let's calculate all the points grouped by color and radius final points = >>{}; @@ -54,16 +54,17 @@ base class CirclePainter final radiusWithBorder = _getRadiusInPixel(circle, withBorder: true); final initialCenter = _getOffset(circle.point); - /// Draws on a "single-world". Returns true if visible. - bool drawIfVisible(double shift) { - bool result = false; + /// Draws on a "single-world" + bool? drawIfVisible(double shift) { + bool? result; final center = initialCenter + Offset(shift, 0); bool isVisible(double radius) { if (_isVisible(center: center, radiusInPixel: radius)) { - return result = true; + result = false; + return true; } - return false; + return true; } if (isVisible(radiusWithoutBorder)) { @@ -90,10 +91,11 @@ base class CirclePainter .add(center); } } + return result; } - helper.drawInTheWorlds(drawIfVisible); + workAcrossWorlds(drawIfVisible); } // Now that all the points are grouped, let's draw them @@ -157,7 +159,7 @@ base class CirclePainter double _getRadiusInPixel(CircleMarker circle, {required bool withBorder}) => (withBorder ? circle.borderStrokeWidth / 2 : 0) + (circle.useRadiusInMeter - ? helper.getPixelWidthFromMeters(circle.point, circle.radius) + ? metersToScreenPixels(circle.point, circle.radius) : circle.radius); /// Returns true if a centered circle with this radius is on the screen. @@ -165,7 +167,6 @@ base class CirclePainter required Offset center, required double radiusInPixel, }) => - helper.screenRect.overlaps( - Rect.fromCircle(center: center, radius: radiusInPixel), - ); + viewportRect + .overlaps(Rect.fromCircle(center: center, radius: radiusInPixel)); } diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index b7958659e..995ffd51b 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -4,7 +4,7 @@ part of 'polygon_layer.dart'; /// the [PolygonLayer]. base class _PolygonPainter extends HitDetectablePainter> - with HitTestRequiresCameraOrigin { + with HitTestRequiresCameraOrigin, MultiWorldLayerHelper { /// Reference to the list of [_ProjectedPolygon]s final List<_ProjectedPolygon> polygons; @@ -59,7 +59,6 @@ base class _PolygonPainter // continue; // } - /// Returns null if invisible, true if hit, false if not hit. bool? checkIfHit(double shift) { final projectedCoords = getOffsetsXY( camera: camera, @@ -67,7 +66,7 @@ base class _PolygonPainter points: projectedPolygon.points, shift: shift, ); - if (!helper.isVisible(projectedCoords)) { + if (!areOffsetsVisible(projectedCoords)) { return null; } if (projectedCoords.first != projectedCoords.last) { @@ -101,7 +100,7 @@ base class _PolygonPainter return (isInPolygon && !isInHole) || (!isInPolygon && isInHole); } - return helper.checkIfHitInTheWorlds(checkIfHit); + return workAcrossWorlds(checkIfHit); } @override @@ -110,7 +109,7 @@ base class _PolygonPainter @override void paint(Canvas canvas, Size size) { const checkOpacity = true; // for debugging purposes only, should be true - helper.setSize(size); + super.paint(canvas, size); final trianglePoints = []; @@ -201,10 +200,8 @@ base class _PolygonPainter lastHash = null; } - final origin = helper.origin; - - /// Draws labels on a "single-world". Returns true if visible. - bool drawLabelIfVisible( + /// Draws labels on a "single-world" + bool? drawLabelIfVisible( double shift, _ProjectedPolygon projectedPolygon, ) { @@ -224,14 +221,14 @@ base class _PolygonPainter padding: 20, ); if (painter == null) { - return false; + return null; } // Flush the batch before painting to preserve stacking. drawPaths(); painter(canvas); - return true; + return false; } // Main loop constructing batched fill and border paths from given polygons. @@ -242,8 +239,8 @@ base class _PolygonPainter final polygonTriangles = triangles?[i]; - /// Draws on a "single-world". Returns true if visible. - bool drawIfVisible(double shift) { + /// Draws on a "single-world" + bool? drawIfVisible(double shift) { final fillOffsets = getOffsetsXY( camera: camera, origin: origin, @@ -253,8 +250,8 @@ base class _PolygonPainter shift: shift, ); - if (!helper.isVisible(fillOffsets)) { - return false; + if (!areOffsetsVisible(fillOffsets)) { + return null; } if (debugAltRenderer) { @@ -340,14 +337,14 @@ base class _PolygonPainter filledPath.addPolygon(holeOffsets, true); // TODO: Potentially more efficient and may change the need to do - // opacity checking - needs testing. However, - // https://github.com/flutter/flutter/issues/44572 prevents this. - // Also need to verify if `xor` or `difference` is preferred. + // opacity checking - needs testing. Also need to verify if `xor` or + // `difference` is preferred. + // No longer blocked by lack of HTML support in Flutter 3.29 /*filledPath = Path.combine( - PathOperation.xor, - filledPath, - Path()..addPolygon(holeOffsets, true), - );*/ + PathOperation.xor, + filledPath, + Path()..addPolygon(holeOffsets, true), + );*/ } if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { @@ -372,10 +369,10 @@ base class _PolygonPainter } } - return true; + return false; } - helper.drawInTheWorlds(drawIfVisible); + workAcrossWorlds(drawIfVisible); if (!drawLabelsLast && polygonLabels && polygon.textPainter != null) { // Labels are expensive because: @@ -387,7 +384,7 @@ base class _PolygonPainter // The painter will be null if the layOuting algorithm determined that // there isn't enough space. - helper.drawInTheWorlds( + workAcrossWorlds( (double shift) => drawLabelIfVisible(shift, projectedPolygon), ); } @@ -403,7 +400,7 @@ base class _PolygonPainter if (projectedPolygon.polygon.textPainter == null) { continue; } - helper.drawInTheWorlds( + workAcrossWorlds( (double shift) => drawLabelIfVisible(shift, projectedPolygon), ); } diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 34b289029..28bafe3bc 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -10,6 +10,7 @@ import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_de import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; +import 'package:flutter_map/src/layer/shared/multi_world_layer_helper.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_in_polygon.dart'; import 'package:flutter_map/src/misc/simplify.dart'; diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index c8acc6281..76f9a875a 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -3,7 +3,7 @@ part of 'polyline_layer.dart'; /// [CustomPainter] for [Polyline]s. base class _PolylinePainter extends HitDetectablePainter> - with HitTestRequiresCameraOrigin { + with HitTestRequiresCameraOrigin, MultiWorldLayerHelper { /// Reference to the list of [Polyline]s. final List<_ProjectedPolyline> polylines; @@ -34,7 +34,6 @@ base class _PolylinePainter // continue; // } - /// Returns null if invisible, true if hit, false if not hit. bool? checkIfHit(double shift) { final offsets = getOffsetsXY( camera: camera, @@ -42,11 +41,11 @@ base class _PolylinePainter points: projectedPolyline.points, shift: shift, ); - if (!helper.isVisible(offsets)) { + if (!areOffsetsVisible(offsets)) { return null; } final strokeWidth = polyline.useStrokeWidthInMeter - ? helper.getPixelWidthFromMeters( + ? metersToScreenPixels( projectedPolyline.polyline.points.first, polyline.strokeWidth, ) @@ -69,7 +68,7 @@ base class _PolylinePainter return false; } - return helper.checkIfHitInTheWorlds(checkIfHit); + return workAcrossWorlds(checkIfHit); } @override @@ -77,7 +76,7 @@ base class _PolylinePainter @override void paint(Canvas canvas, Size size) { - helper.setSize(size); + super.paint(canvas, size); var path = ui.Path(); var borderPath = ui.Path(); @@ -93,7 +92,7 @@ base class _PolylinePainter final hasBorder = borderPaint != null && filterPaint != null; if (hasBorder) { if (needsLayerSaving) { - canvas.saveLayer(helper.screenRect, Paint()); + canvas.saveLayer(viewportRect, Paint()); } canvas.drawPath(borderPath, borderPaint!); @@ -114,24 +113,22 @@ base class _PolylinePainter paint = Paint(); } - final origin = helper.origin; - for (final projectedPolyline in polylines) { final polyline = projectedPolyline.polyline; if (polyline.points.isEmpty) { continue; } - /// Draws on a "single-world". Returns true if visible. - bool drawIfVisible(double shift) { + /// Draws on a "single-world" + bool? drawIfVisible(double shift) { final offsets = getOffsetsXY( camera: camera, origin: origin, points: projectedPolyline.points, shift: shift, ); - if (!helper.isVisible(offsets)) { - return false; + if (!areOffsetsVisible(offsets)) { + return null; } final hash = polyline.renderHashCode; @@ -147,7 +144,7 @@ base class _PolylinePainter late final double strokeWidth; if (polyline.useStrokeWidthInMeter) { - strokeWidth = helper.getPixelWidthFromMeters( + strokeWidth = metersToScreenPixels( projectedPolyline.polyline.points.first, polyline.strokeWidth, ); @@ -254,10 +251,11 @@ base class _PolylinePainter } } } - return true; + + return false; } - helper.drawInTheWorlds(drawIfVisible); + workAcrossWorlds(drawIfVisible); } drawPaths(); diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index f905afca1..4a4f08e6c 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -9,6 +9,7 @@ import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_de import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; +import 'package:flutter_map/src/layer/shared/multi_world_layer_helper.dart'; import 'package:flutter_map/src/misc/extensions.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/simplify.dart'; diff --git a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart index cd9536258..e06279b43 100644 --- a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart +++ b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/multi_world_layer_helper.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @@ -25,13 +24,11 @@ abstract base class HitDetectablePainter? hitNotifier; - final MultiWorldLayerHelper helper; - /// Elements that should be possibly be hit tested by [elementHitTest] /// ([hitTest]) /// diff --git a/lib/src/layer/shared/multi_world_layer_helper.dart b/lib/src/layer/shared/multi_world_layer_helper.dart index fef59cddb..377c3a900 100644 --- a/lib/src/layer/shared/multi_world_layer_helper.dart +++ b/lib/src/layer/shared/multi_world_layer_helper.dart @@ -1,70 +1,78 @@ -import 'dart:ui'; - import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; -/// Helper for multi world: e.g. draw and hitTest on all world copies. -class MultiWorldLayerHelper { - /// Helper for multi world. - MultiWorldLayerHelper(this.camera); - +/// Provides utilities for painters and hit testers, especially those which have +/// multi-world support +@internal +mixin MultiWorldLayerHelper on CustomPainter { + abstract final MapCamera camera; static const _distance = Distance(); - /// Sets the screen size. To be called immediately after `paint`. - void setSize(Size size) => _screenRect = Offset.zero & size; - - late Rect _screenRect; - - /// Screen rect. - Rect get screenRect => _screenRect; - - /// Camera. - final MapCamera camera; + /// The rectangle of the canvas on its last paint + /// + /// Must not be retrieved before [paint] has been called. + Rect get viewportRect => _viewportRect; + late Rect _viewportRect; + + @mustCallSuper + @mustBeOverridden + @override + void paint(Canvas canvas, Size size) { + _viewportRect = Offset.zero & size; + } - /// Returns true if the points are visible on the screen. - bool isVisible(List points) { - if (points.isEmpty) { + /// Determine whether the specified offsets are visible within the viewport + /// + /// Always returns `false` if the specified list is empty. + bool areOffsetsVisible(Iterable offsets) { + if (offsets.isEmpty) { return false; } double minX; double maxX; double minY; double maxY; - minX = maxX = points.first.dx; - minY = maxY = points.first.dy; - for (final Offset offset in points) { - if (screenRect.contains(offset)) return true; + minX = maxX = offsets.first.dx; + minY = maxY = offsets.first.dy; + for (final Offset offset in offsets) { + if (viewportRect.contains(offset)) return true; if (minX > offset.dx) minX = offset.dx; if (minY > offset.dy) minY = offset.dy; if (maxX < offset.dx) maxX = offset.dx; if (maxY < offset.dy) maxY = offset.dy; } - return screenRect.overlaps(Rect.fromLTRB(minX, minY, maxX, maxY)); + return viewportRect.overlaps(Rect.fromLTRB(minX, minY, maxX, maxY)); } - /// Returns true if hit in all world copies. + /// Perform the callback in all world copies (until stopped) + /// + /// If the worker returns: + /// * `true`: no more worlds will be tested, and this will return `true` + /// * `false`: more worlds will be tested + /// * `null`: no more worlds will be tested in the current working direction; + /// if both directions have been finished, this will return `false` + /// + /// The worker must return `true` or `null` in some case to prevent an + /// infinite loop. /// - /// Uses a "single-world" method that returns* - /// * null if invisible - /// * true if hit - /// * false if not hit - bool checkIfHitInTheWorlds(bool? Function(double) checkIfHit) { - if (checkIfHit(0) ?? false) { + /// Internally, the worker is invoked in the 'negative' worlds (worlds to the + /// left of the 'primary' world) until repetition is stopped, then in the + /// 'positive' world: <--||-->. + bool workAcrossWorlds(bool? Function(double shift) work) { + if (work(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); + final isHit = work(shift); if (isHit == null) break; if (isHit) return true; } for (double shift = worldWidth;; shift += worldWidth) { - final isHit = checkIfHit(shift); + final isHit = work(shift); if (isHit == null) break; if (isHit) return true; } @@ -72,34 +80,18 @@ class MultiWorldLayerHelper { return false; } - /// Draws in all world copies. - /// - /// Uses a "single-world" method that returns* - /// * true if visible - /// * false if not visible - void drawInTheWorlds(bool Function(double) drawIfVisible) { - drawIfVisible(0); - - if (worldWidth == 0) return; - for (double shift = -worldWidth;; shift -= worldWidth) { - final isVisible = drawIfVisible(shift); - if (!isVisible) break; - } - for (double shift = worldWidth;; shift += worldWidth) { - final isVisible = drawIfVisible(shift); - if (!isVisible) break; - } - } - /// Returns the origin of the camera. Offset get origin => camera.projectAtZoom(camera.center) - camera.size.center(Offset.zero); /// Returns the world size in pixels. + /// + /// Equivalent to [MapCamera.getWorldWidthAtZoom]. double get worldWidth => camera.getWorldWidthAtZoom(); - /// Returns the width in pixel of a width in meters, for a given [point]. - double getPixelWidthFromMeters(LatLng point, double meters) => + /// Converts a distance in meters to the equivalent distance in screen pixels, + /// at the geographic coordinates specified. + double metersToScreenPixels(LatLng point, double meters) => (camera.getOffsetFromOrigin(point) - camera.getOffsetFromOrigin(_distance.offset(point, meters, 180))) .distance; From aecd196e8a1f344d934ee58294441c5160695553 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 13 Feb 2025 18:56:34 +0000 Subject: [PATCH 04/11] Fixed bug --- lib/src/layer/circle_layer/painter.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index be17d603a..1bd394488 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -61,10 +61,11 @@ base class CirclePainter bool isVisible(double radius) { if (_isVisible(center: center, radiusInPixel: radius)) { - result = false; + result = false; // Stop iteration in current direction return true; } - return true; + // Leave `result` null to continue iteration + return false; } if (isVisible(radiusWithoutBorder)) { From a3010a07322082b7b55b273bce6276c03ae0b365 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 13 Feb 2025 19:00:55 +0000 Subject: [PATCH 05/11] Added documentation warning about `PolygonLabelPlacement.centroidWithMultiWorld` usage --- lib/src/layer/polygon_layer/label.dart | 17 +++++++++++++++++ lib/src/layer/polygon_layer/polygon.dart | 18 ++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/src/layer/polygon_layer/label.dart b/lib/src/layer/polygon_layer/label.dart index c7add1a16..ee0ba04c7 100644 --- a/lib/src/layer/polygon_layer/label.dart +++ b/lib/src/layer/polygon_layer/label.dart @@ -118,3 +118,20 @@ LatLng _computePolylabel(List points) { labelPosition.point.x.toDouble(), ); } + +/// Defines the algorithm used to calculate the position of the [Polygon] label. +/// +/// > [!IMPORTANT] +/// > If your project allows users to browse across multiple worlds, and your +/// > polygons may be over the anti-meridan boundary, [centroidWithMultiWorld] +/// > must be used - other algorithms will produce unexpected results. +enum PolygonLabelPlacement { + /// Use the centroid of the [Polygon] outline as position for the label. + centroid, + + /// Use the centroid in a multi-world as position for the label. + centroidWithMultiWorld, + + /// Use the Mapbox Polylabel algorithm as position for the label. + polylabel, +} diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index 8e14bcf91..d9c788f0e 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -1,17 +1,5 @@ part of 'polygon_layer.dart'; -/// Defines the algorithm used to calculate the position of the [Polygon] label. -enum PolygonLabelPlacement { - /// Use the centroid of the [Polygon] outline as position for the label. - centroid, - - /// Use the centroid in a multi-world as position for the label. - centroidWithMultiWorld, - - /// Use the Mapbox Polylabel algorithm as position for the label. - polylabel, -} - /// [Polygon] class, to be used for the [PolygonLayer]. class Polygon { /// The points for the outline of the [Polygon]. @@ -69,6 +57,12 @@ class Polygon { /// [PolygonLabelPlacement.polylabel] can be expensive for some polygons. If /// there is a large lag spike, try using [PolygonLabelPlacement.centroid]. /// + /// > [!IMPORTANT] + /// > If your project allows users to browse across multiple worlds, and your + /// > polygons may be over the anti-meridan boundary, + /// > [PolygonLabelPlacement.centroidWithMultiWorld] must be used - other + /// > algorithms will produce unexpected results. + /// /// Labels will not be drawn if there is not enough space. final PolygonLabelPlacement labelPlacement; From eceab58110d8befffb978d0b1a16e6818d13b139 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 13 Feb 2025 19:17:46 +0000 Subject: [PATCH 06/11] Renamed `MultiWorldLayerHelper` to `FeatureLayerUtils` Removed `HitTestRequiresCameraOrigin` Converted `HitDetectablePainter` to mixin --- lib/src/layer/circle_layer/circle_layer.dart | 2 +- lib/src/layer/circle_layer/painter.dart | 17 +++++--- lib/src/layer/polygon_layer/painter.dart | 24 ++++++----- .../layer/polygon_layer/polygon_layer.dart | 2 +- lib/src/layer/polyline_layer/painter.dart | 23 ++++++----- .../layer/polyline_layer/polyline_layer.dart | 2 +- ...r_helper.dart => feature_layer_utils.dart} | 6 +-- .../internal_hit_detectable.dart | 40 ++++--------------- 8 files changed, 53 insertions(+), 63 deletions(-) rename lib/src/layer/shared/{multi_world_layer_helper.dart => feature_layer_utils.dart} (94%) diff --git a/lib/src/layer/circle_layer/circle_layer.dart b/lib/src/layer/circle_layer/circle_layer.dart index 786282074..72fdbdb58 100644 --- a/lib/src/layer/circle_layer/circle_layer.dart +++ b/lib/src/layer/circle_layer/circle_layer.dart @@ -3,8 +3,8 @@ import 'dart:ui'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; -import 'package:flutter_map/src/layer/shared/multi_world_layer_helper.dart'; import 'package:latlong2/latlong.dart' hide Path; part 'circle_marker.dart'; diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index 1bd394488..3cfe0da78 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -1,18 +1,23 @@ part of 'circle_layer.dart'; -/// The [CustomPainter] used to draw [CircleMarker] for the [CircleLayer]. -base class CirclePainter - extends HitDetectablePainter> - with MultiWorldLayerHelper { +/// The [CustomPainter] used to draw [CircleMarker]s for the [CircleLayer]. +class CirclePainter extends CustomPainter + with HitDetectablePainter>, FeatureLayerUtils { /// Reference to the list of [CircleMarker]s of the [CircleLayer]. final List> circles; + @override + final MapCamera camera; + + @override + final LayerHitNotifier? hitNotifier; + /// Create a [CirclePainter] instance by providing the required /// reference objects. CirclePainter({ required this.circles, - required super.camera, - required super.hitNotifier, + required this.camera, + required this.hitNotifier, }); @override diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 995ffd51b..c857452b8 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -1,10 +1,10 @@ part of 'polygon_layer.dart'; -/// The [_PolygonPainter] class is used to render [Polygon]s for -/// the [PolygonLayer]. -base class _PolygonPainter - extends HitDetectablePainter> - with HitTestRequiresCameraOrigin, MultiWorldLayerHelper { +/// The [CustomPainter] used to draw [Polygon]s for the [PolygonLayer]. +// TODO: We should consider exposing this publicly, as with [CirclePainter] - +// but the projected objects are private at the moment. +class _PolygonPainter extends CustomPainter + with HitDetectablePainter>, FeatureLayerUtils { /// Reference to the list of [_ProjectedPolygon]s final List<_ProjectedPolygon> polygons; @@ -35,15 +35,21 @@ base class _PolygonPainter /// See [PolygonLayer.debugAltRenderer] final bool debugAltRenderer; + @override + final MapCamera camera; + + @override + final LayerHitNotifier? hitNotifier; + /// Create a new [_PolygonPainter] instance. _PolygonPainter({ required this.polygons, required this.triangles, - required super.camera, required this.polygonLabels, required this.drawLabelsLast, required this.debugAltRenderer, - required super.hitNotifier, + required this.camera, + required this.hitNotifier, }) : bounds = camera.visibleBounds; @override @@ -62,7 +68,7 @@ base class _PolygonPainter bool? checkIfHit(double shift) { final projectedCoords = getOffsetsXY( camera: camera, - origin: hitTestCameraOrigin, + origin: origin, points: projectedPolygon.points, shift: shift, ); @@ -81,7 +87,7 @@ base class _PolygonPainter (points) { final projectedHoleCoords = getOffsetsXY( camera: camera, - origin: hitTestCameraOrigin, + origin: origin, points: points, shift: shift, ); diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 28bafe3bc..0b1c6d568 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -6,11 +6,11 @@ import 'package:dart_earcut/dart_earcut.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; -import 'package:flutter_map/src/layer/shared/multi_world_layer_helper.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_in_polygon.dart'; import 'package:flutter_map/src/misc/simplify.dart'; diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index 76f9a875a..8f6f97725 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -1,20 +1,25 @@ part of 'polyline_layer.dart'; -/// [CustomPainter] for [Polyline]s. -base class _PolylinePainter - extends HitDetectablePainter> - with HitTestRequiresCameraOrigin, MultiWorldLayerHelper { - /// Reference to the list of [Polyline]s. +/// The [CustomPainter] used to draw [Polyline]s for the [PolylineLayer]. +// TODO: We should consider exposing this publicly, as with [CirclePainter] - +// but the projected objects are private at the moment. +class _PolylinePainter extends CustomPainter + with HitDetectablePainter>, FeatureLayerUtils { final List<_ProjectedPolyline> polylines; - final double minimumHitbox; + @override + final MapCamera camera; + + @override + final LayerHitNotifier? hitNotifier; + /// Create a new [_PolylinePainter] instance _PolylinePainter({ required this.polylines, required this.minimumHitbox, - required super.camera, - required super.hitNotifier, + required this.camera, + required this.hitNotifier, }); @override @@ -37,7 +42,7 @@ base class _PolylinePainter bool? checkIfHit(double shift) { final offsets = getOffsetsXY( camera: camera, - origin: hitTestCameraOrigin, + origin: origin, points: projectedPolyline.points, shift: shift, ); diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index 4a4f08e6c..0464c3b38 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -5,11 +5,11 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; -import 'package:flutter_map/src/layer/shared/multi_world_layer_helper.dart'; import 'package:flutter_map/src/misc/extensions.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/simplify.dart'; diff --git a/lib/src/layer/shared/multi_world_layer_helper.dart b/lib/src/layer/shared/feature_layer_utils.dart similarity index 94% rename from lib/src/layer/shared/multi_world_layer_helper.dart rename to lib/src/layer/shared/feature_layer_utils.dart index 377c3a900..cfae858c3 100644 --- a/lib/src/layer/shared/multi_world_layer_helper.dart +++ b/lib/src/layer/shared/feature_layer_utils.dart @@ -3,10 +3,10 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; -/// Provides utilities for painters and hit testers, especially those which have -/// multi-world support +/// Provides utilities for 'feature layers' implemented with canvas painters and +/// hit testers, especially those which have multi-world support @internal -mixin MultiWorldLayerHelper on CustomPainter { +mixin FeatureLayerUtils on CustomPainter { abstract final MapCamera camera; static const _distance = Distance(); diff --git a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart index e06279b43..bd876282f 100644 --- a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart +++ b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @@ -19,15 +20,10 @@ mixin HitDetectableElement { } @internal -abstract base class HitDetectablePainter> extends CustomPainter { - HitDetectablePainter({ - required this.camera, - required this.hitNotifier, - }); - - final MapCamera camera; - final LayerHitNotifier? hitNotifier; +mixin HitDetectablePainter> + on CustomPainter { + abstract final MapCamera camera; + abstract final LayerHitNotifier? hitNotifier; /// Elements that should be possibly be hit tested by [elementHitTest] /// ([hitTest]) @@ -48,9 +44,8 @@ abstract base class HitDetectablePainter> on HitDetectablePainter { - /// Calculated [MapCamera] origin, using the following formula: - /// - /// ```dart - /// camera.project(camera.center) - camera.size.center(Offset.zero) - /// ``` - /// - /// Only initialised after [hitTest] is invoked. Recalculated every time - /// [hitTest] is invoked. - late Offset hitTestCameraOrigin; - - @override - bool? hitTest(Offset position) { - hitTestCameraOrigin = - camera.projectAtZoom(camera.center) - camera.size.center(Offset.zero); - return super.hitTest(position); - } -} From fd129c29788acc281c8255e2410a856824d29a9e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 14 Feb 2025 11:08:17 +0000 Subject: [PATCH 07/11] Added `WorldWorkControl` to improve clarity of `FeatureLayerUtils.workAcrossWorlds` --- lib/src/layer/circle_layer/painter.dart | 15 +++-- lib/src/layer/polygon_layer/painter.dart | 26 ++++---- lib/src/layer/polyline_layer/painter.dart | 21 +++--- lib/src/layer/shared/feature_layer_utils.dart | 65 +++++++++++++------ 4 files changed, 74 insertions(+), 53 deletions(-) diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index 3cfe0da78..8fd6b2058 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -29,14 +29,16 @@ class CirclePainter extends CustomPainter final radius = _getRadiusInPixel(element, withBorder: true); final initialCenter = _getOffset(element.point); - bool? checkIfHit(double shift) { + WorldWorkControl checkIfHit(double shift) { final center = initialCenter + Offset(shift, 0); if (!_isVisible(center: center, radiusInPixel: radius)) { - return null; + return WorldWorkControl.invisible; } return pow(point.dx - center.dx, 2) + pow(point.dy - center.dy, 2) <= - radius * radius; + radius * radius + ? WorldWorkControl.hit + : WorldWorkControl.visible; } return workAcrossWorlds(checkIfHit); @@ -60,16 +62,15 @@ class CirclePainter extends CustomPainter final initialCenter = _getOffset(circle.point); /// Draws on a "single-world" - bool? drawIfVisible(double shift) { - bool? result; + WorldWorkControl drawIfVisible(double shift) { + WorldWorkControl result = WorldWorkControl.invisible; final center = initialCenter + Offset(shift, 0); bool isVisible(double radius) { if (_isVisible(center: center, radiusInPixel: radius)) { - result = false; // Stop iteration in current direction + result = WorldWorkControl.visible; return true; } - // Leave `result` null to continue iteration return false; } diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index c857452b8..bbfbea9a0 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -65,7 +65,7 @@ class _PolygonPainter extends CustomPainter // continue; // } - bool? checkIfHit(double shift) { + WorldWorkControl checkIfHit(double shift) { final projectedCoords = getOffsetsXY( camera: camera, origin: origin, @@ -73,8 +73,9 @@ class _PolygonPainter extends CustomPainter shift: shift, ); if (!areOffsetsVisible(projectedCoords)) { - return null; + return WorldWorkControl.invisible; } + if (projectedCoords.first != projectedCoords.last) { projectedCoords.add(projectedCoords.first); } @@ -103,7 +104,9 @@ class _PolygonPainter extends CustomPainter // Second check handles case where polygon outline intersects a hole, // ensuring that the hit matches with the visual representation - return (isInPolygon && !isInHole) || (!isInPolygon && isInHole); + return (isInPolygon && !isInHole) || (!isInPolygon && isInHole) + ? WorldWorkControl.hit + : WorldWorkControl.visible; } return workAcrossWorlds(checkIfHit); @@ -207,7 +210,7 @@ class _PolygonPainter extends CustomPainter } /// Draws labels on a "single-world" - bool? drawLabelIfVisible( + WorldWorkControl drawLabelIfVisible( double shift, _ProjectedPolygon projectedPolygon, ) { @@ -226,15 +229,13 @@ class _PolygonPainter extends CustomPainter rotate: polygon.rotateLabel, padding: 20, ); - if (painter == null) { - return null; - } + if (painter == null) return WorldWorkControl.invisible; // Flush the batch before painting to preserve stacking. drawPaths(); painter(canvas); - return false; + return WorldWorkControl.visible; } // Main loop constructing batched fill and border paths from given polygons. @@ -246,7 +247,7 @@ class _PolygonPainter extends CustomPainter final polygonTriangles = triangles?[i]; /// Draws on a "single-world" - bool? drawIfVisible(double shift) { + WorldWorkControl drawIfVisible(double shift) { final fillOffsets = getOffsetsXY( camera: camera, origin: origin, @@ -255,10 +256,7 @@ class _PolygonPainter extends CustomPainter polygonTriangles != null ? projectedPolygon.holePoints : null, shift: shift, ); - - if (!areOffsetsVisible(fillOffsets)) { - return null; - } + if (!areOffsetsVisible(fillOffsets)) return WorldWorkControl.invisible; if (debugAltRenderer) { const offsetsLabelStyle = TextStyle( @@ -375,7 +373,7 @@ class _PolygonPainter extends CustomPainter } } - return false; + return WorldWorkControl.visible; } workAcrossWorlds(drawIfVisible); diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index 8f6f97725..661b5afe9 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -39,16 +39,15 @@ class _PolylinePainter extends CustomPainter // continue; // } - bool? checkIfHit(double shift) { + WorldWorkControl checkIfHit(double shift) { final offsets = getOffsetsXY( camera: camera, origin: origin, points: projectedPolyline.points, shift: shift, ); - if (!areOffsetsVisible(offsets)) { - return null; - } + if (!areOffsetsVisible(offsets)) return WorldWorkControl.invisible; + final strokeWidth = polyline.useStrokeWidthInMeter ? metersToScreenPixels( projectedPolyline.polyline.points.first, @@ -67,10 +66,12 @@ class _PolylinePainter extends CustomPainter final distanceSq = getSqSegDist(point.dx, point.dy, o1.dx, o1.dy, o2.dx, o2.dy); - if (distanceSq <= hittableDistance * hittableDistance) return true; + if (distanceSq <= hittableDistance * hittableDistance) { + return WorldWorkControl.hit; + } } - return false; + return WorldWorkControl.visible; } return workAcrossWorlds(checkIfHit); @@ -125,16 +126,14 @@ class _PolylinePainter extends CustomPainter } /// Draws on a "single-world" - bool? drawIfVisible(double shift) { + WorldWorkControl drawIfVisible(double shift) { final offsets = getOffsetsXY( camera: camera, origin: origin, points: projectedPolyline.points, shift: shift, ); - if (!areOffsetsVisible(offsets)) { - return null; - } + if (!areOffsetsVisible(offsets)) return WorldWorkControl.invisible; final hash = polyline.renderHashCode; if (needsLayerSaving || (lastHash != null && lastHash != hash)) { @@ -257,7 +256,7 @@ class _PolylinePainter extends CustomPainter } } - return false; + return WorldWorkControl.visible; } workAcrossWorlds(drawIfVisible); diff --git a/lib/src/layer/shared/feature_layer_utils.dart b/lib/src/layer/shared/feature_layer_utils.dart index cfae858c3..789e9969f 100644 --- a/lib/src/layer/shared/feature_layer_utils.dart +++ b/lib/src/layer/shared/feature_layer_utils.dart @@ -48,36 +48,37 @@ mixin FeatureLayerUtils on CustomPainter { /// Perform the callback in all world copies (until stopped) /// - /// If the worker returns: - /// * `true`: no more worlds will be tested, and this will return `true` - /// * `false`: more worlds will be tested - /// * `null`: no more worlds will be tested in the current working direction; - /// if both directions have been finished, this will return `false` - /// - /// The worker must return `true` or `null` in some case to prevent an - /// infinite loop. + /// See [WorldWorkControl] for information about the callback return types. + /// Returns `true` if any result is [WorldWorkControl.hit]. /// /// Internally, the worker is invoked in the 'negative' worlds (worlds to the /// left of the 'primary' world) until repetition is stopped, then in the - /// 'positive' world: <--||-->. - bool workAcrossWorlds(bool? Function(double shift) work) { - if (work(0) ?? false) { - return true; - } + /// 'positive' worlds: <--||-->. + bool workAcrossWorlds(WorldWorkControl Function(double shift) work) { + if (work(0) == WorldWorkControl.hit) return true; if (worldWidth == 0) return false; + + negativeWorldsLoop: for (double shift = -worldWidth;; shift -= worldWidth) { - final isHit = work(shift); - if (isHit == null) break; - if (isHit) return true; + switch (work(shift)) { + case WorldWorkControl.hit: + return true; + case WorldWorkControl.invisible: + break negativeWorldsLoop; + case WorldWorkControl.visible: + } } + for (double shift = worldWidth;; shift += worldWidth) { - final isHit = work(shift); - if (isHit == null) break; - if (isHit) return true; + switch (work(shift)) { + case WorldWorkControl.hit: + return true; + case WorldWorkControl.invisible: + return false; + case WorldWorkControl.visible: + } } - - return false; } /// Returns the origin of the camera. @@ -96,3 +97,25 @@ mixin FeatureLayerUtils on CustomPainter { camera.getOffsetFromOrigin(_distance.offset(point, meters, 180))) .distance; } + +/// Return type for the callback argument of +/// [FeatureLayerUtils.workAcrossWorlds], which indicates how to control the +/// iteration across worlds & how to return from the method +/// +/// The callback must return [hit] or [invisible] in some case to prevent an +/// infinite loop. +@internal +enum WorldWorkControl { + /// Immediately stop iteration across all further worlds, and return `true` + /// + /// This is useful for hit testing for efficiency purposes, where hitting any + /// one element is enough to determine a hit testing result. + hit, + + /// Keep iterating across worlds in the current direction + visible, + + /// Stop iterating across worlds in the current direction; if both directions + /// have been completed without a [hit], returns `false` + invisible, +} From 8903fc1f2bb73c8829a628088cfe6709ffdc021f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 15 Feb 2025 12:35:00 +0000 Subject: [PATCH 08/11] Added internal `ProjectedHittableElement` & `InteractiveMultiWorldProjectableFeatureLayerPainter` Internal refactoring of feature layers & hit detection --- lib/flutter_map.dart | 4 +- lib/src/layer/circle_layer/circle_layer.dart | 4 +- lib/src/layer/circle_layer/painter.dart | 5 +- lib/src/layer/polygon_layer/painter.dart | 99 +++++++------------ lib/src/layer/polygon_layer/polygon.dart | 4 +- .../layer/polygon_layer/polygon_layer.dart | 16 +-- .../polygon_layer/projected_polygon.dart | 12 +-- lib/src/layer/polyline_layer/painter.dart | 90 ++++++----------- lib/src/layer/polyline_layer/polyline.dart | 4 +- .../layer/polyline_layer/polyline_layer.dart | 21 ++-- .../polyline_layer/projected_polyline.dart | 12 +-- ...rld_projectable_feature_layer_painter.dart | 77 +++++++++++++++ .../internal_hit_detectable.dart | 18 +--- .../interactivity}/layer_hit_notifier.dart | 2 +- .../interactivity}/layer_hit_result.dart | 0 .../projected_hittable_element.dart | 17 ++++ .../projection_simplification}/state.dart | 6 +- .../projection_simplification}/widget.dart | 2 +- .../utils.dart} | 4 +- test/full_coverage_test.dart | 2 +- 20 files changed, 213 insertions(+), 186 deletions(-) create mode 100644 lib/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart rename lib/src/layer/shared/{layer_interactivity => feature_layer/interactivity}/internal_hit_detectable.dart (81%) rename lib/src/layer/shared/{layer_interactivity => feature_layer/interactivity}/layer_hit_notifier.dart (90%) rename lib/src/layer/shared/{layer_interactivity => feature_layer/interactivity}/layer_hit_result.dart (100%) create mode 100644 lib/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart rename lib/src/layer/shared/{layer_projection_simplification => feature_layer/projection_simplification}/state.dart (96%) rename lib/src/layer/shared/{layer_projection_simplification => feature_layer/projection_simplification}/widget.dart (92%) rename lib/src/layer/shared/{feature_layer_utils.dart => feature_layer/utils.dart} (99%) diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index acb68995f..40e61786c 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -33,8 +33,8 @@ export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.da export 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; export 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; export 'package:flutter_map/src/layer/scalebar/scalebar.dart'; -export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_notifier.dart'; -export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_result.dart'; +export 'package:flutter_map/src/layer/shared/feature_layer/interactivity/layer_hit_notifier.dart'; +export 'package:flutter_map/src/layer/shared/feature_layer/interactivity/layer_hit_result.dart'; export 'package:flutter_map/src/layer/shared/line_patterns/stroke_pattern.dart'; export 'package:flutter_map/src/layer/shared/mobile_layer_transformer.dart'; export 'package:flutter_map/src/layer/shared/translucent_pointer.dart'; diff --git a/lib/src/layer/circle_layer/circle_layer.dart b/lib/src/layer/circle_layer/circle_layer.dart index 72fdbdb58..085e345e5 100644 --- a/lib/src/layer/circle_layer/circle_layer.dart +++ b/lib/src/layer/circle_layer/circle_layer.dart @@ -3,8 +3,8 @@ import 'dart:ui'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; -import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; import 'package:latlong2/latlong.dart' hide Path; part 'circle_marker.dart'; diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index 8fd6b2058..9411829a8 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -23,8 +23,7 @@ class CirclePainter extends CustomPainter @override bool elementHitTest( CircleMarker element, { - required Offset point, - required LatLng coordinate, + required Offset offset, }) { final radius = _getRadiusInPixel(element, withBorder: true); final initialCenter = _getOffset(element.point); @@ -35,7 +34,7 @@ class CirclePainter extends CustomPainter return WorldWorkControl.invisible; } - return pow(point.dx - center.dx, 2) + pow(point.dy - center.dy, 2) <= + return pow(offset.dx - center.dx, 2) + pow(offset.dy - center.dy, 2) <= radius * radius ? WorldWorkControl.hit : WorldWorkControl.visible; diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index bbfbea9a0..7adb322be 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -1,10 +1,11 @@ part of 'polygon_layer.dart'; /// The [CustomPainter] used to draw [Polygon]s for the [PolygonLayer]. -// TODO: We should consider exposing this publicly, as with [CirclePainter] - -// but the projected objects are private at the moment. -class _PolygonPainter extends CustomPainter - with HitDetectablePainter>, FeatureLayerUtils { +// TODO: Consider exposing this publicly, as with [CirclePainter] - but the +// projected objects are private at the moment. +base class _PolygonPainter + extends InteractiveMultiWorldProjectableFeatureLayerPainter> { /// Reference to the list of [_ProjectedPolygon]s final List<_ProjectedPolygon> polygons; @@ -35,12 +36,6 @@ class _PolygonPainter extends CustomPainter /// See [PolygonLayer.debugAltRenderer] final bool debugAltRenderer; - @override - final MapCamera camera; - - @override - final LayerHitNotifier? hitNotifier; - /// Create a new [_PolygonPainter] instance. _PolygonPainter({ required this.polygons, @@ -48,68 +43,44 @@ class _PolygonPainter extends CustomPainter required this.polygonLabels, required this.drawLabelsLast, required this.debugAltRenderer, - required this.camera, - required this.hitNotifier, + required super.camera, + required super.hitNotifier, }) : bounds = camera.visibleBounds; @override - bool elementHitTest( - _ProjectedPolygon projectedPolygon, { - required Offset point, - required LatLng coordinate, + bool elementHitTestInWorld( + _ProjectedPolygon element, { + required List coords, + required Offset offset, + required double shift, }) { - // TODO: We should check the bounding box here, for efficiency - // However, we need to account for map rotation - // - // if (!polygon.boundingBox.contains(touch)) { - // continue; - // } - - WorldWorkControl checkIfHit(double shift) { - final projectedCoords = getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolygon.points, - shift: shift, - ); - if (!areOffsetsVisible(projectedCoords)) { - return WorldWorkControl.invisible; - } - - if (projectedCoords.first != projectedCoords.last) { - projectedCoords.add(projectedCoords.first); - } + // Ensure closed polygon + if (coords.first != coords.last) coords.add(coords.first); - final isValidPolygon = projectedCoords.length >= 3; - final isInPolygon = - isValidPolygon && isPointInPolygon(point, projectedCoords); - - final isInHole = projectedPolygon.holePoints.any( - (points) { - final projectedHoleCoords = getOffsetsXY( - camera: camera, - origin: origin, - points: points, - shift: shift, - ); - if (projectedHoleCoords.first != projectedHoleCoords.last) { - projectedHoleCoords.add(projectedHoleCoords.first); - } + final isValidPolygon = coords.length >= 3; + final isInPolygon = isValidPolygon && isPointInPolygon(offset, coords); - final isValidHolePolygon = projectedHoleCoords.length >= 3; - return isValidHolePolygon && - isPointInPolygon(point, projectedHoleCoords); - }, - ); + final isInHole = element.holePoints.any( + (points) { + final projectedHoleCoords = getOffsetsXY( + camera: camera, + origin: origin, + points: points, + shift: shift, + ); + if (projectedHoleCoords.first != projectedHoleCoords.last) { + projectedHoleCoords.add(projectedHoleCoords.first); + } - // Second check handles case where polygon outline intersects a hole, - // ensuring that the hit matches with the visual representation - return (isInPolygon && !isInHole) || (!isInPolygon && isInHole) - ? WorldWorkControl.hit - : WorldWorkControl.visible; - } + final isValidHolePolygon = projectedHoleCoords.length >= 3; + return isValidHolePolygon && + isPointInPolygon(offset, projectedHoleCoords); + }, + ); - return workAcrossWorlds(checkIfHit); + // Second check handles case where polygon outline intersects a hole, + // ensuring that the hit matches with the visual representation + return (isInPolygon && !isInHole) || (!isInPolygon && isInHole); } @override diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index d9c788f0e..ecedd7ac4 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -1,7 +1,7 @@ part of 'polygon_layer.dart'; /// [Polygon] class, to be used for the [PolygonLayer]. -class Polygon { +class Polygon with HitDetectableElement { /// The points for the outline of the [Polygon]. final List points; @@ -70,7 +70,7 @@ class Polygon { /// it remains upright final bool rotateLabel; - /// {@macro fm.hde.hitValue} + @override final R? hitValue; /// Designates whether the given polygon points follow a clock or diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 0b1c6d568..c41fdbd09 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -6,10 +6,12 @@ import 'package:dart_earcut/dart_earcut.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; -import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; -import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; -import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/state.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/widget.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_in_polygon.dart'; @@ -95,14 +97,14 @@ class _PolygonLayerState extends State> required Projection projection, required Polygon element, }) => - _ProjectedPolygon._fromPolygon(projection, element); + _ProjectedPolygon.fromPolygon(projection, element); @override _ProjectedPolygon simplifyProjectedElement({ required _ProjectedPolygon projectedElement, required double tolerance, }) => - _ProjectedPolygon._( + _ProjectedPolygon( polygon: projectedElement.polygon, points: simplifyPoints( points: projectedElement.points, @@ -121,7 +123,7 @@ class _PolygonLayerState extends State> ); @override - Iterable> getElements(PolygonLayer widget) => widget.polygons; + Iterable> get elements => widget.polygons; @override Widget build(BuildContext context) { diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index 5796c42bc..ef26647aa 100644 --- a/lib/src/layer/polygon_layer/projected_polygon.dart +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -1,22 +1,22 @@ part of 'polygon_layer.dart'; @immutable -class _ProjectedPolygon with HitDetectableElement { +final class _ProjectedPolygon + extends ProjectedHittableElement { final Polygon polygon; - final List points; final List> holePoints; @override R? get hitValue => polygon.hitValue; - const _ProjectedPolygon._({ + const _ProjectedPolygon({ required this.polygon, - required this.points, + required super.points, required this.holePoints, }); - _ProjectedPolygon._fromPolygon(Projection projection, Polygon polygon) - : this._( + _ProjectedPolygon.fromPolygon(Projection projection, Polygon polygon) + : this( polygon: polygon, points: projection.projectList(polygon.points), holePoints: () { diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index 661b5afe9..c1466bba7 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -1,80 +1,50 @@ part of 'polyline_layer.dart'; /// The [CustomPainter] used to draw [Polyline]s for the [PolylineLayer]. -// TODO: We should consider exposing this publicly, as with [CirclePainter] - -// but the projected objects are private at the moment. -class _PolylinePainter extends CustomPainter - with HitDetectablePainter>, FeatureLayerUtils { +// TODO: Consider exposing this publicly, as with [CirclePainter] - but the +// projected objects are private at the moment. +base class _PolylinePainter + extends InteractiveMultiWorldProjectableFeatureLayerPainter> { final List<_ProjectedPolyline> polylines; final double minimumHitbox; - @override - final MapCamera camera; - - @override - final LayerHitNotifier? hitNotifier; - /// Create a new [_PolylinePainter] instance _PolylinePainter({ required this.polylines, required this.minimumHitbox, - required this.camera, - required this.hitNotifier, + required super.camera, + required super.hitNotifier, }); @override - bool elementHitTest( - _ProjectedPolyline projectedPolyline, { - required Offset point, - required LatLng coordinate, + bool elementHitTestInWorld( + _ProjectedPolyline element, { + required List coords, + required Offset offset, + required double shift, }) { - final polyline = projectedPolyline.polyline; - - // TODO: We should check the bounding box here, for efficiency - // However, we need to account for: - // * map rotation - // * extended bbox that accounts for `minimumHitbox` - // - // if (!polyline.boundingBox.contains(touch)) { - // continue; - // } - - WorldWorkControl checkIfHit(double shift) { - final offsets = getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolyline.points, - shift: shift, - ); - if (!areOffsetsVisible(offsets)) return WorldWorkControl.invisible; - - final strokeWidth = polyline.useStrokeWidthInMeter - ? metersToScreenPixels( - projectedPolyline.polyline.points.first, - polyline.strokeWidth, - ) - : polyline.strokeWidth; - final hittableDistance = math.max( - strokeWidth / 2 + polyline.borderStrokeWidth / 2, - minimumHitbox, - ); - - for (int i = 0; i < offsets.length - 1; i++) { - final o1 = offsets[i]; - final o2 = offsets[i + 1]; - - final distanceSq = - getSqSegDist(point.dx, point.dy, o1.dx, o1.dy, o2.dx, o2.dy); - - if (distanceSq <= hittableDistance * hittableDistance) { - return WorldWorkControl.hit; - } - } + final polyline = element.polyline; + + final strokeWidth = polyline.useStrokeWidthInMeter + ? metersToScreenPixels(polyline.points.first, polyline.strokeWidth) + : polyline.strokeWidth; + final hittableDistance = math.max( + strokeWidth / 2 + polyline.borderStrokeWidth / 2, + minimumHitbox, + ); + + for (int i = 0; i < coords.length - 1; i++) { + final o1 = coords[i]; + final o2 = coords[i + 1]; + + final distanceSq = + getSqSegDist(offset.dx, offset.dy, o1.dx, o1.dy, o2.dx, o2.dy); - return WorldWorkControl.visible; + if (distanceSq <= hittableDistance * hittableDistance) return true; } - return workAcrossWorlds(checkIfHit); + return false; } @override diff --git a/lib/src/layer/polyline_layer/polyline.dart b/lib/src/layer/polyline_layer/polyline.dart index 0ab84ccfc..b5712da66 100644 --- a/lib/src/layer/polyline_layer/polyline.dart +++ b/lib/src/layer/polyline_layer/polyline.dart @@ -1,7 +1,7 @@ part of 'polyline_layer.dart'; /// [Polyline] (aka. LineString) class, to be used for the [PolylineLayer]. -class Polyline { +class Polyline with HitDetectableElement { /// The list of coordinates for the [Polyline]. final List points; @@ -39,7 +39,7 @@ class Polyline { /// Set to true if the width of the stroke should have meters as unit. final bool useStrokeWidthInMeter; - /// {@macro fm.hde.hitValue} + @override final R? hitValue; LatLngBounds? _boundingBox; diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index 0464c3b38..e4555a23d 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -5,10 +5,12 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; -import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; -import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; -import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/state.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/widget.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; import 'package:flutter_map/src/misc/extensions.dart'; import 'package:flutter_map/src/misc/offsets.dart'; @@ -70,14 +72,14 @@ class _PolylineLayerState extends State> required Projection projection, required Polyline element, }) => - _ProjectedPolyline._fromPolyline(projection, element); + _ProjectedPolyline.fromPolyline(projection, element); @override _ProjectedPolyline simplifyProjectedElement({ required _ProjectedPolyline projectedElement, required double tolerance, }) => - _ProjectedPolyline._( + _ProjectedPolyline( polyline: projectedElement.polyline, points: simplifyPoints( points: projectedElement.points, @@ -87,8 +89,7 @@ class _PolylineLayerState extends State> ); @override - Iterable> getElements(PolylineLayer widget) => - widget.polylines; + Iterable> get elements => widget.polylines; @override Widget build(BuildContext context) { @@ -190,7 +191,7 @@ class _PolylineLayerState extends State> } else { // If we cannot see this segment but have seen previous ones, flush the last polyline fragment. if (start != -1) { - yield _ProjectedPolyline._( + yield _ProjectedPolyline( polyline: polyline, points: projectedPolyline.points.sublist(start, i + 1), ); @@ -206,7 +207,7 @@ class _PolylineLayerState extends State> if (containsSegment) { yield start == 0 ? projectedPolyline - : _ProjectedPolyline._( + : _ProjectedPolyline( polyline: polyline, // Special case: the entire polyline is visible points: projectedPolyline.points.sublist(start), diff --git a/lib/src/layer/polyline_layer/projected_polyline.dart b/lib/src/layer/polyline_layer/projected_polyline.dart index eb41ae2fc..a1094a0ab 100644 --- a/lib/src/layer/polyline_layer/projected_polyline.dart +++ b/lib/src/layer/polyline_layer/projected_polyline.dart @@ -1,20 +1,20 @@ part of 'polyline_layer.dart'; @immutable -class _ProjectedPolyline with HitDetectableElement { +final class _ProjectedPolyline + extends ProjectedHittableElement { final Polyline polyline; - final List points; @override R? get hitValue => polyline.hitValue; - const _ProjectedPolyline._({ + const _ProjectedPolyline({ required this.polyline, - required this.points, + required super.points, }); - _ProjectedPolyline._fromPolyline(Projection projection, Polyline polyline) - : this._( + _ProjectedPolyline.fromPolyline(Projection projection, Polyline polyline) + : this( polyline: polyline, points: projection.projectList(polyline.points), ); diff --git a/lib/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart b/lib/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart new file mode 100644 index 000000000..5f7c62c7d --- /dev/null +++ b/lib/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart @@ -0,0 +1,77 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; +import 'package:flutter_map/src/misc/offsets.dart'; +import 'package:meta/meta.dart'; + +/// Mixes [HitDetectablePainter] & [FeatureLayerUtils] into a [CustomPainter] to +/// provide a base framework for hit testing elements in a feature layer across +/// multiple worlds where the elements are projectable +/// ([ProjectedHittableElement]) +@internal +abstract base class InteractiveMultiWorldProjectableFeatureLayerPainter< + R extends Object, E extends ProjectedHittableElement> + extends CustomPainter with HitDetectablePainter, FeatureLayerUtils { + @override + final MapCamera camera; + + @override + final LayerHitNotifier? hitNotifier; + + /// Inheritable constructor which sets up the map camera and hit notifier + InteractiveMultiWorldProjectableFeatureLayerPainter({ + required this.camera, + required this.hitNotifier, + }); + + /// Invoked on each element in every visible world by [elementHitTest] + /// + /// [coords] have been pre-[shift]ed for each world. [getOffsetsXY] may be + /// used with the original element and the [shift] to project and shift more + /// coordinates if necessary. + /// + /// See documentation on [elementHitTest] for good practises and more + /// information. + /// + /// Should return whether the element has been hit. + bool elementHitTestInWorld( + E element, { + required List coords, + required Offset offset, + required double shift, + }); + + // TODO: Introduce bbox culling to skip testing coords in easy cases. Note + // that this needs to support map rotation, multi-worlds, and `minimumHitbox` + // for polylines. + @override + bool elementHitTest( + E element, { + required Offset offset, + }) => + workAcrossWorlds( + (shift) { + final projectedCoords = getOffsetsXY( + camera: camera, + origin: origin, + points: element.points, + shift: shift, + ); + + if (!areOffsetsVisible(projectedCoords)) { + return WorldWorkControl.invisible; + } + + return elementHitTestInWorld( + element, + coords: projectedCoords, + offset: offset, + shift: shift, + ) + ? WorldWorkControl.hit + : WorldWorkControl.visible; + }, + ); +} diff --git a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart b/lib/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart similarity index 81% rename from lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart rename to lib/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart index bd876282f..1bc76e99b 100644 --- a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart +++ b/lib/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart @@ -1,12 +1,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; -import 'package:latlong2/latlong.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; import 'package:meta/meta.dart'; @internal mixin HitDetectableElement { - /// {@template fm.hde.hitValue} /// Value to notify layer's `hitNotifier` with (such as /// [PolygonLayer.hitNotifier]) /// @@ -15,7 +13,6 @@ mixin HitDetectableElement { /// /// The object should have a valid & useful equality, as it may be used /// by FM internals. - /// {@endtemplate} R? get hitValue; } @@ -38,9 +35,6 @@ mixin HitDetectablePainter> /// a hit has already been found on another element, and the /// [HitDetectableElement.hitValue] is `null` on this element. /// - /// [Offset] and [coordinate] - /// ([MapCamera.screenOffsetToLatLng]) are provided for simplicity. - /// /// Avoid performing calculations that are not dependent on [element]. Instead, /// override [hitTest], store the necessary calculation results in /// (`late` non-`null`able) members, and call `super.hitTest(position)` at the @@ -50,8 +44,7 @@ mixin HitDetectablePainter> /// Should return whether an element has been hit. bool elementHitTest( E element, { - required Offset point, - required LatLng coordinate, + required Offset offset, }); final _hits = []; // Avoids repetitive memory reallocation @@ -62,13 +55,12 @@ mixin HitDetectablePainter> _hits.clear(); bool hasHit = false; - final point = position; - final coordinate = camera.screenOffsetToLatLng(point); + final coordinate = camera.screenOffsetToLatLng(position); for (int i = elements.length - 1; i >= 0; i--) { final element = elements.elementAt(i); if (hasHit && element.hitValue == null) continue; - if (elementHitTest(element, point: point, coordinate: coordinate)) { + if (elementHitTest(element, offset: position)) { if (element.hitValue != null) _hits.add(element.hitValue!); hasHit = true; } @@ -82,7 +74,7 @@ mixin HitDetectablePainter> hitNotifier?.value = LayerHitResult( hitValues: _hits, coordinate: coordinate, - point: point, + point: position, ); return true; } diff --git a/lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart b/lib/src/layer/shared/feature_layer/interactivity/layer_hit_notifier.dart similarity index 90% rename from lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart rename to lib/src/layer/shared/feature_layer/interactivity/layer_hit_notifier.dart index 076d118bc..596801115 100644 --- a/lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart +++ b/lib/src/layer/shared/feature_layer/interactivity/layer_hit_notifier.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_result.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/layer_hit_result.dart'; /// A [ValueNotifier] that notifies: /// diff --git a/lib/src/layer/shared/layer_interactivity/layer_hit_result.dart b/lib/src/layer/shared/feature_layer/interactivity/layer_hit_result.dart similarity index 100% rename from lib/src/layer/shared/layer_interactivity/layer_hit_result.dart rename to lib/src/layer/shared/feature_layer/interactivity/layer_hit_result.dart diff --git a/lib/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart b/lib/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart new file mode 100644 index 000000000..92c066963 --- /dev/null +++ b/lib/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart @@ -0,0 +1,17 @@ +import 'package:flutter/painting.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; +import 'package:meta/meta.dart'; + +@internal +@immutable +abstract base class ProjectedHittableElement + with HitDetectableElement { + const ProjectedHittableElement({required this.points}); + + /// Projected coordinates of the element + /// + /// Some elements may have more than one set of projected points. However, + /// these [points] are used to determine whether the element is visible in + /// a world when hit testing. + final List points; +} diff --git a/lib/src/layer/shared/layer_projection_simplification/state.dart b/lib/src/layer/shared/feature_layer/projection_simplification/state.dart similarity index 96% rename from lib/src/layer/shared/layer_projection_simplification/state.dart rename to lib/src/layer/shared/feature_layer/projection_simplification/state.dart index b65cb0cd6..e030f3001 100644 --- a/lib/src/layer/shared/layer_projection_simplification/state.dart +++ b/lib/src/layer/shared/feature_layer/projection_simplification/state.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/widget.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:meta/meta.dart'; @@ -34,7 +34,7 @@ mixin ProjectionSimplificationManagement< /// Return the individual elements given the /// [ProjectionSimplificationManagementSupportedWidget] - Iterable getElements(W widget); + Iterable get elements; /// An iterable of simplified [ProjectedElement]s, which is always ready /// after the [build] method has been invoked, and should then be used in the @@ -64,8 +64,6 @@ mixin ProjectionSimplificationManagement< Widget build(BuildContext context) { final camera = MapCamera.of(context); - final elements = getElements(widget); - final projected = _cachedProjectedElements ??= List.generate( elements.length, (i) => projectElement( diff --git a/lib/src/layer/shared/layer_projection_simplification/widget.dart b/lib/src/layer/shared/feature_layer/projection_simplification/widget.dart similarity index 92% rename from lib/src/layer/shared/layer_projection_simplification/widget.dart rename to lib/src/layer/shared/feature_layer/projection_simplification/widget.dart index 2cd3b927f..8c6ae2f5e 100644 --- a/lib/src/layer/shared/layer_projection_simplification/widget.dart +++ b/lib/src/layer/shared/feature_layer/projection_simplification/widget.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/state.dart'; /// A [StatefulWidget] that includes the properties used by the [State] component /// which mixes [ProjectionSimplificationManagement] in diff --git a/lib/src/layer/shared/feature_layer_utils.dart b/lib/src/layer/shared/feature_layer/utils.dart similarity index 99% rename from lib/src/layer/shared/feature_layer_utils.dart rename to lib/src/layer/shared/feature_layer/utils.dart index 789e9969f..c18c34727 100644 --- a/lib/src/layer/shared/feature_layer_utils.dart +++ b/lib/src/layer/shared/feature_layer/utils.dart @@ -5,9 +5,10 @@ import 'package:meta/meta.dart'; /// Provides utilities for 'feature layers' implemented with canvas painters and /// hit testers, especially those which have multi-world support -@internal mixin FeatureLayerUtils on CustomPainter { + /// [MapCamera] reference abstract final MapCamera camera; + static const _distance = Distance(); /// The rectangle of the canvas on its last paint @@ -104,7 +105,6 @@ mixin FeatureLayerUtils on CustomPainter { /// /// The callback must return [hit] or [invisible] in some case to prevent an /// infinite loop. -@internal enum WorldWorkControl { /// Immediately stop iteration across all further worlds, and return `true` /// diff --git a/test/full_coverage_test.dart b/test/full_coverage_test.dart index 3e1f687bd..f780a005d 100644 --- a/test/full_coverage_test.dart +++ b/test/full_coverage_test.dart @@ -17,7 +17,7 @@ import 'package:flutter_map/src/layer/marker_layer/marker_layer.dart'; import 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart'; import 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; import 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; -import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; import 'package:flutter_map/src/layer/shared/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/shared/translucent_pointer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile.dart'; From d2b60c0b5d9aa2d5029163e62d3edd52f0b58cd5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 16 Feb 2025 12:35:10 +0000 Subject: [PATCH 09/11] Revert "Added internal `ProjectedHittableElement` & `InteractiveMultiWorldProjectableFeatureLayerPainter`" This reverts commit 8903fc1f2bb73c8829a628088cfe6709ffdc021f. --- lib/flutter_map.dart | 4 +- lib/src/layer/circle_layer/circle_layer.dart | 4 +- lib/src/layer/circle_layer/painter.dart | 5 +- lib/src/layer/polygon_layer/painter.dart | 99 ++++++++++++------- lib/src/layer/polygon_layer/polygon.dart | 4 +- .../layer/polygon_layer/polygon_layer.dart | 16 ++- .../polygon_layer/projected_polygon.dart | 12 +-- lib/src/layer/polyline_layer/painter.dart | 90 +++++++++++------ lib/src/layer/polyline_layer/polyline.dart | 4 +- .../layer/polyline_layer/polyline_layer.dart | 21 ++-- .../polyline_layer/projected_polyline.dart | 12 +-- ...rld_projectable_feature_layer_painter.dart | 77 --------------- .../projected_hittable_element.dart | 17 ---- .../utils.dart => feature_layer_utils.dart} | 4 +- .../internal_hit_detectable.dart | 18 +++- .../layer_hit_notifier.dart | 2 +- .../layer_hit_result.dart | 0 .../state.dart | 6 +- .../widget.dart | 2 +- test/full_coverage_test.dart | 2 +- 20 files changed, 186 insertions(+), 213 deletions(-) delete mode 100644 lib/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart delete mode 100644 lib/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart rename lib/src/layer/shared/{feature_layer/utils.dart => feature_layer_utils.dart} (99%) rename lib/src/layer/shared/{feature_layer/interactivity => layer_interactivity}/internal_hit_detectable.dart (81%) rename lib/src/layer/shared/{feature_layer/interactivity => layer_interactivity}/layer_hit_notifier.dart (90%) rename lib/src/layer/shared/{feature_layer/interactivity => layer_interactivity}/layer_hit_result.dart (100%) rename lib/src/layer/shared/{feature_layer/projection_simplification => layer_projection_simplification}/state.dart (96%) rename lib/src/layer/shared/{feature_layer/projection_simplification => layer_projection_simplification}/widget.dart (92%) diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 40e61786c..acb68995f 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -33,8 +33,8 @@ export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.da export 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; export 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; export 'package:flutter_map/src/layer/scalebar/scalebar.dart'; -export 'package:flutter_map/src/layer/shared/feature_layer/interactivity/layer_hit_notifier.dart'; -export 'package:flutter_map/src/layer/shared/feature_layer/interactivity/layer_hit_result.dart'; +export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_notifier.dart'; +export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_result.dart'; export 'package:flutter_map/src/layer/shared/line_patterns/stroke_pattern.dart'; export 'package:flutter_map/src/layer/shared/mobile_layer_transformer.dart'; export 'package:flutter_map/src/layer/shared/translucent_pointer.dart'; diff --git a/lib/src/layer/circle_layer/circle_layer.dart b/lib/src/layer/circle_layer/circle_layer.dart index 085e345e5..72fdbdb58 100644 --- a/lib/src/layer/circle_layer/circle_layer.dart +++ b/lib/src/layer/circle_layer/circle_layer.dart @@ -3,8 +3,8 @@ import 'dart:ui'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; +import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; import 'package:latlong2/latlong.dart' hide Path; part 'circle_marker.dart'; diff --git a/lib/src/layer/circle_layer/painter.dart b/lib/src/layer/circle_layer/painter.dart index 9411829a8..8fd6b2058 100644 --- a/lib/src/layer/circle_layer/painter.dart +++ b/lib/src/layer/circle_layer/painter.dart @@ -23,7 +23,8 @@ class CirclePainter extends CustomPainter @override bool elementHitTest( CircleMarker element, { - required Offset offset, + required Offset point, + required LatLng coordinate, }) { final radius = _getRadiusInPixel(element, withBorder: true); final initialCenter = _getOffset(element.point); @@ -34,7 +35,7 @@ class CirclePainter extends CustomPainter return WorldWorkControl.invisible; } - return pow(offset.dx - center.dx, 2) + pow(offset.dy - center.dy, 2) <= + return pow(point.dx - center.dx, 2) + pow(point.dy - center.dy, 2) <= radius * radius ? WorldWorkControl.hit : WorldWorkControl.visible; diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 7adb322be..bbfbea9a0 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -1,11 +1,10 @@ part of 'polygon_layer.dart'; /// The [CustomPainter] used to draw [Polygon]s for the [PolygonLayer]. -// TODO: Consider exposing this publicly, as with [CirclePainter] - but the -// projected objects are private at the moment. -base class _PolygonPainter - extends InteractiveMultiWorldProjectableFeatureLayerPainter> { +// TODO: We should consider exposing this publicly, as with [CirclePainter] - +// but the projected objects are private at the moment. +class _PolygonPainter extends CustomPainter + with HitDetectablePainter>, FeatureLayerUtils { /// Reference to the list of [_ProjectedPolygon]s final List<_ProjectedPolygon> polygons; @@ -36,6 +35,12 @@ base class _PolygonPainter /// See [PolygonLayer.debugAltRenderer] final bool debugAltRenderer; + @override + final MapCamera camera; + + @override + final LayerHitNotifier? hitNotifier; + /// Create a new [_PolygonPainter] instance. _PolygonPainter({ required this.polygons, @@ -43,44 +48,68 @@ base class _PolygonPainter required this.polygonLabels, required this.drawLabelsLast, required this.debugAltRenderer, - required super.camera, - required super.hitNotifier, + required this.camera, + required this.hitNotifier, }) : bounds = camera.visibleBounds; @override - bool elementHitTestInWorld( - _ProjectedPolygon element, { - required List coords, - required Offset offset, - required double shift, + bool elementHitTest( + _ProjectedPolygon projectedPolygon, { + required Offset point, + required LatLng coordinate, }) { - // Ensure closed polygon - if (coords.first != coords.last) coords.add(coords.first); + // TODO: We should check the bounding box here, for efficiency + // However, we need to account for map rotation + // + // if (!polygon.boundingBox.contains(touch)) { + // continue; + // } + + WorldWorkControl checkIfHit(double shift) { + final projectedCoords = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + shift: shift, + ); + if (!areOffsetsVisible(projectedCoords)) { + return WorldWorkControl.invisible; + } - final isValidPolygon = coords.length >= 3; - final isInPolygon = isValidPolygon && isPointInPolygon(offset, coords); + if (projectedCoords.first != projectedCoords.last) { + projectedCoords.add(projectedCoords.first); + } - final isInHole = element.holePoints.any( - (points) { - final projectedHoleCoords = getOffsetsXY( - camera: camera, - origin: origin, - points: points, - shift: shift, - ); - if (projectedHoleCoords.first != projectedHoleCoords.last) { - projectedHoleCoords.add(projectedHoleCoords.first); - } + final isValidPolygon = projectedCoords.length >= 3; + final isInPolygon = + isValidPolygon && isPointInPolygon(point, projectedCoords); + + final isInHole = projectedPolygon.holePoints.any( + (points) { + final projectedHoleCoords = getOffsetsXY( + camera: camera, + origin: origin, + points: points, + shift: shift, + ); + if (projectedHoleCoords.first != projectedHoleCoords.last) { + projectedHoleCoords.add(projectedHoleCoords.first); + } - final isValidHolePolygon = projectedHoleCoords.length >= 3; - return isValidHolePolygon && - isPointInPolygon(offset, projectedHoleCoords); - }, - ); + final isValidHolePolygon = projectedHoleCoords.length >= 3; + return isValidHolePolygon && + isPointInPolygon(point, projectedHoleCoords); + }, + ); + + // Second check handles case where polygon outline intersects a hole, + // ensuring that the hit matches with the visual representation + return (isInPolygon && !isInHole) || (!isInPolygon && isInHole) + ? WorldWorkControl.hit + : WorldWorkControl.visible; + } - // Second check handles case where polygon outline intersects a hole, - // ensuring that the hit matches with the visual representation - return (isInPolygon && !isInHole) || (!isInPolygon && isInHole); + return workAcrossWorlds(checkIfHit); } @override diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index ecedd7ac4..d9c788f0e 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -1,7 +1,7 @@ part of 'polygon_layer.dart'; /// [Polygon] class, to be used for the [PolygonLayer]. -class Polygon with HitDetectableElement { +class Polygon { /// The points for the outline of the [Polygon]. final List points; @@ -70,7 +70,7 @@ class Polygon with HitDetectableElement { /// it remains upright final bool rotateLabel; - @override + /// {@macro fm.hde.hitValue} final R? hitValue; /// Designates whether the given polygon points follow a clock or diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index c41fdbd09..0b1c6d568 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -6,12 +6,10 @@ import 'package:dart_earcut/dart_earcut.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/state.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/widget.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; +import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_in_polygon.dart'; @@ -97,14 +95,14 @@ class _PolygonLayerState extends State> required Projection projection, required Polygon element, }) => - _ProjectedPolygon.fromPolygon(projection, element); + _ProjectedPolygon._fromPolygon(projection, element); @override _ProjectedPolygon simplifyProjectedElement({ required _ProjectedPolygon projectedElement, required double tolerance, }) => - _ProjectedPolygon( + _ProjectedPolygon._( polygon: projectedElement.polygon, points: simplifyPoints( points: projectedElement.points, @@ -123,7 +121,7 @@ class _PolygonLayerState extends State> ); @override - Iterable> get elements => widget.polygons; + Iterable> getElements(PolygonLayer widget) => widget.polygons; @override Widget build(BuildContext context) { diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index ef26647aa..5796c42bc 100644 --- a/lib/src/layer/polygon_layer/projected_polygon.dart +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -1,22 +1,22 @@ part of 'polygon_layer.dart'; @immutable -final class _ProjectedPolygon - extends ProjectedHittableElement { +class _ProjectedPolygon with HitDetectableElement { final Polygon polygon; + final List points; final List> holePoints; @override R? get hitValue => polygon.hitValue; - const _ProjectedPolygon({ + const _ProjectedPolygon._({ required this.polygon, - required super.points, + required this.points, required this.holePoints, }); - _ProjectedPolygon.fromPolygon(Projection projection, Polygon polygon) - : this( + _ProjectedPolygon._fromPolygon(Projection projection, Polygon polygon) + : this._( polygon: polygon, points: projection.projectList(polygon.points), holePoints: () { diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index c1466bba7..661b5afe9 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -1,50 +1,80 @@ part of 'polyline_layer.dart'; /// The [CustomPainter] used to draw [Polyline]s for the [PolylineLayer]. -// TODO: Consider exposing this publicly, as with [CirclePainter] - but the -// projected objects are private at the moment. -base class _PolylinePainter - extends InteractiveMultiWorldProjectableFeatureLayerPainter> { +// TODO: We should consider exposing this publicly, as with [CirclePainter] - +// but the projected objects are private at the moment. +class _PolylinePainter extends CustomPainter + with HitDetectablePainter>, FeatureLayerUtils { final List<_ProjectedPolyline> polylines; final double minimumHitbox; + @override + final MapCamera camera; + + @override + final LayerHitNotifier? hitNotifier; + /// Create a new [_PolylinePainter] instance _PolylinePainter({ required this.polylines, required this.minimumHitbox, - required super.camera, - required super.hitNotifier, + required this.camera, + required this.hitNotifier, }); @override - bool elementHitTestInWorld( - _ProjectedPolyline element, { - required List coords, - required Offset offset, - required double shift, + bool elementHitTest( + _ProjectedPolyline projectedPolyline, { + required Offset point, + required LatLng coordinate, }) { - final polyline = element.polyline; - - final strokeWidth = polyline.useStrokeWidthInMeter - ? metersToScreenPixels(polyline.points.first, polyline.strokeWidth) - : polyline.strokeWidth; - final hittableDistance = math.max( - strokeWidth / 2 + polyline.borderStrokeWidth / 2, - minimumHitbox, - ); - - for (int i = 0; i < coords.length - 1; i++) { - final o1 = coords[i]; - final o2 = coords[i + 1]; - - final distanceSq = - getSqSegDist(offset.dx, offset.dy, o1.dx, o1.dy, o2.dx, o2.dy); + final polyline = projectedPolyline.polyline; + + // TODO: We should check the bounding box here, for efficiency + // However, we need to account for: + // * map rotation + // * extended bbox that accounts for `minimumHitbox` + // + // if (!polyline.boundingBox.contains(touch)) { + // continue; + // } + + WorldWorkControl checkIfHit(double shift) { + final offsets = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolyline.points, + shift: shift, + ); + if (!areOffsetsVisible(offsets)) return WorldWorkControl.invisible; + + final strokeWidth = polyline.useStrokeWidthInMeter + ? metersToScreenPixels( + projectedPolyline.polyline.points.first, + polyline.strokeWidth, + ) + : polyline.strokeWidth; + final hittableDistance = math.max( + strokeWidth / 2 + polyline.borderStrokeWidth / 2, + minimumHitbox, + ); + + for (int i = 0; i < offsets.length - 1; i++) { + final o1 = offsets[i]; + final o2 = offsets[i + 1]; + + final distanceSq = + getSqSegDist(point.dx, point.dy, o1.dx, o1.dy, o2.dx, o2.dy); + + if (distanceSq <= hittableDistance * hittableDistance) { + return WorldWorkControl.hit; + } + } - if (distanceSq <= hittableDistance * hittableDistance) return true; + return WorldWorkControl.visible; } - return false; + return workAcrossWorlds(checkIfHit); } @override diff --git a/lib/src/layer/polyline_layer/polyline.dart b/lib/src/layer/polyline_layer/polyline.dart index b5712da66..0ab84ccfc 100644 --- a/lib/src/layer/polyline_layer/polyline.dart +++ b/lib/src/layer/polyline_layer/polyline.dart @@ -1,7 +1,7 @@ part of 'polyline_layer.dart'; /// [Polyline] (aka. LineString) class, to be used for the [PolylineLayer]. -class Polyline with HitDetectableElement { +class Polyline { /// The list of coordinates for the [Polyline]. final List points; @@ -39,7 +39,7 @@ class Polyline with HitDetectableElement { /// Set to true if the width of the stroke should have meters as unit. final bool useStrokeWidthInMeter; - @override + /// {@macro fm.hde.hitValue} final R? hitValue; LatLngBounds? _boundingBox; diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index e4555a23d..0464c3b38 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -5,12 +5,10 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/state.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/widget.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; +import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; import 'package:flutter_map/src/misc/extensions.dart'; import 'package:flutter_map/src/misc/offsets.dart'; @@ -72,14 +70,14 @@ class _PolylineLayerState extends State> required Projection projection, required Polyline element, }) => - _ProjectedPolyline.fromPolyline(projection, element); + _ProjectedPolyline._fromPolyline(projection, element); @override _ProjectedPolyline simplifyProjectedElement({ required _ProjectedPolyline projectedElement, required double tolerance, }) => - _ProjectedPolyline( + _ProjectedPolyline._( polyline: projectedElement.polyline, points: simplifyPoints( points: projectedElement.points, @@ -89,7 +87,8 @@ class _PolylineLayerState extends State> ); @override - Iterable> get elements => widget.polylines; + Iterable> getElements(PolylineLayer widget) => + widget.polylines; @override Widget build(BuildContext context) { @@ -191,7 +190,7 @@ class _PolylineLayerState extends State> } else { // If we cannot see this segment but have seen previous ones, flush the last polyline fragment. if (start != -1) { - yield _ProjectedPolyline( + yield _ProjectedPolyline._( polyline: polyline, points: projectedPolyline.points.sublist(start, i + 1), ); @@ -207,7 +206,7 @@ class _PolylineLayerState extends State> if (containsSegment) { yield start == 0 ? projectedPolyline - : _ProjectedPolyline( + : _ProjectedPolyline._( polyline: polyline, // Special case: the entire polyline is visible points: projectedPolyline.points.sublist(start), diff --git a/lib/src/layer/polyline_layer/projected_polyline.dart b/lib/src/layer/polyline_layer/projected_polyline.dart index a1094a0ab..eb41ae2fc 100644 --- a/lib/src/layer/polyline_layer/projected_polyline.dart +++ b/lib/src/layer/polyline_layer/projected_polyline.dart @@ -1,20 +1,20 @@ part of 'polyline_layer.dart'; @immutable -final class _ProjectedPolyline - extends ProjectedHittableElement { +class _ProjectedPolyline with HitDetectableElement { final Polyline polyline; + final List points; @override R? get hitValue => polyline.hitValue; - const _ProjectedPolyline({ + const _ProjectedPolyline._({ required this.polyline, - required super.points, + required this.points, }); - _ProjectedPolyline.fromPolyline(Projection projection, Polyline polyline) - : this( + _ProjectedPolyline._fromPolyline(Projection projection, Polyline polyline) + : this._( polyline: polyline, points: projection.projectList(polyline.points), ); diff --git a/lib/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart b/lib/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart deleted file mode 100644 index 5f7c62c7d..000000000 --- a/lib/src/layer/shared/feature_layer/interactive_multi_world_projectable_feature_layer_painter.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; -import 'package:flutter_map/src/misc/offsets.dart'; -import 'package:meta/meta.dart'; - -/// Mixes [HitDetectablePainter] & [FeatureLayerUtils] into a [CustomPainter] to -/// provide a base framework for hit testing elements in a feature layer across -/// multiple worlds where the elements are projectable -/// ([ProjectedHittableElement]) -@internal -abstract base class InteractiveMultiWorldProjectableFeatureLayerPainter< - R extends Object, E extends ProjectedHittableElement> - extends CustomPainter with HitDetectablePainter, FeatureLayerUtils { - @override - final MapCamera camera; - - @override - final LayerHitNotifier? hitNotifier; - - /// Inheritable constructor which sets up the map camera and hit notifier - InteractiveMultiWorldProjectableFeatureLayerPainter({ - required this.camera, - required this.hitNotifier, - }); - - /// Invoked on each element in every visible world by [elementHitTest] - /// - /// [coords] have been pre-[shift]ed for each world. [getOffsetsXY] may be - /// used with the original element and the [shift] to project and shift more - /// coordinates if necessary. - /// - /// See documentation on [elementHitTest] for good practises and more - /// information. - /// - /// Should return whether the element has been hit. - bool elementHitTestInWorld( - E element, { - required List coords, - required Offset offset, - required double shift, - }); - - // TODO: Introduce bbox culling to skip testing coords in easy cases. Note - // that this needs to support map rotation, multi-worlds, and `minimumHitbox` - // for polylines. - @override - bool elementHitTest( - E element, { - required Offset offset, - }) => - workAcrossWorlds( - (shift) { - final projectedCoords = getOffsetsXY( - camera: camera, - origin: origin, - points: element.points, - shift: shift, - ); - - if (!areOffsetsVisible(projectedCoords)) { - return WorldWorkControl.invisible; - } - - return elementHitTestInWorld( - element, - coords: projectedCoords, - offset: offset, - shift: shift, - ) - ? WorldWorkControl.hit - : WorldWorkControl.visible; - }, - ); -} diff --git a/lib/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart b/lib/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart deleted file mode 100644 index 92c066963..000000000 --- a/lib/src/layer/shared/feature_layer/interactivity/projected_hittable_element.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/painting.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; -import 'package:meta/meta.dart'; - -@internal -@immutable -abstract base class ProjectedHittableElement - with HitDetectableElement { - const ProjectedHittableElement({required this.points}); - - /// Projected coordinates of the element - /// - /// Some elements may have more than one set of projected points. However, - /// these [points] are used to determine whether the element is visible in - /// a world when hit testing. - final List points; -} diff --git a/lib/src/layer/shared/feature_layer/utils.dart b/lib/src/layer/shared/feature_layer_utils.dart similarity index 99% rename from lib/src/layer/shared/feature_layer/utils.dart rename to lib/src/layer/shared/feature_layer_utils.dart index c18c34727..789e9969f 100644 --- a/lib/src/layer/shared/feature_layer/utils.dart +++ b/lib/src/layer/shared/feature_layer_utils.dart @@ -5,10 +5,9 @@ import 'package:meta/meta.dart'; /// Provides utilities for 'feature layers' implemented with canvas painters and /// hit testers, especially those which have multi-world support +@internal mixin FeatureLayerUtils on CustomPainter { - /// [MapCamera] reference abstract final MapCamera camera; - static const _distance = Distance(); /// The rectangle of the canvas on its last paint @@ -105,6 +104,7 @@ mixin FeatureLayerUtils on CustomPainter { /// /// The callback must return [hit] or [invisible] in some case to prevent an /// infinite loop. +@internal enum WorldWorkControl { /// Immediately stop iteration across all further worlds, and return `true` /// diff --git a/lib/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart similarity index 81% rename from lib/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart rename to lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart index 1bc76e99b..bd876282f 100644 --- a/lib/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart +++ b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart @@ -1,10 +1,12 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/utils.dart'; +import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart'; +import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @internal mixin HitDetectableElement { + /// {@template fm.hde.hitValue} /// Value to notify layer's `hitNotifier` with (such as /// [PolygonLayer.hitNotifier]) /// @@ -13,6 +15,7 @@ mixin HitDetectableElement { /// /// The object should have a valid & useful equality, as it may be used /// by FM internals. + /// {@endtemplate} R? get hitValue; } @@ -35,6 +38,9 @@ mixin HitDetectablePainter> /// a hit has already been found on another element, and the /// [HitDetectableElement.hitValue] is `null` on this element. /// + /// [Offset] and [coordinate] + /// ([MapCamera.screenOffsetToLatLng]) are provided for simplicity. + /// /// Avoid performing calculations that are not dependent on [element]. Instead, /// override [hitTest], store the necessary calculation results in /// (`late` non-`null`able) members, and call `super.hitTest(position)` at the @@ -44,7 +50,8 @@ mixin HitDetectablePainter> /// Should return whether an element has been hit. bool elementHitTest( E element, { - required Offset offset, + required Offset point, + required LatLng coordinate, }); final _hits = []; // Avoids repetitive memory reallocation @@ -55,12 +62,13 @@ mixin HitDetectablePainter> _hits.clear(); bool hasHit = false; - final coordinate = camera.screenOffsetToLatLng(position); + final point = position; + final coordinate = camera.screenOffsetToLatLng(point); for (int i = elements.length - 1; i >= 0; i--) { final element = elements.elementAt(i); if (hasHit && element.hitValue == null) continue; - if (elementHitTest(element, offset: position)) { + if (elementHitTest(element, point: point, coordinate: coordinate)) { if (element.hitValue != null) _hits.add(element.hitValue!); hasHit = true; } @@ -74,7 +82,7 @@ mixin HitDetectablePainter> hitNotifier?.value = LayerHitResult( hitValues: _hits, coordinate: coordinate, - point: position, + point: point, ); return true; } diff --git a/lib/src/layer/shared/feature_layer/interactivity/layer_hit_notifier.dart b/lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart similarity index 90% rename from lib/src/layer/shared/feature_layer/interactivity/layer_hit_notifier.dart rename to lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart index 596801115..076d118bc 100644 --- a/lib/src/layer/shared/feature_layer/interactivity/layer_hit_notifier.dart +++ b/lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/layer_hit_result.dart'; +import 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_result.dart'; /// A [ValueNotifier] that notifies: /// diff --git a/lib/src/layer/shared/feature_layer/interactivity/layer_hit_result.dart b/lib/src/layer/shared/layer_interactivity/layer_hit_result.dart similarity index 100% rename from lib/src/layer/shared/feature_layer/interactivity/layer_hit_result.dart rename to lib/src/layer/shared/layer_interactivity/layer_hit_result.dart diff --git a/lib/src/layer/shared/feature_layer/projection_simplification/state.dart b/lib/src/layer/shared/layer_projection_simplification/state.dart similarity index 96% rename from lib/src/layer/shared/feature_layer/projection_simplification/state.dart rename to lib/src/layer/shared/layer_projection_simplification/state.dart index e030f3001..b65cb0cd6 100644 --- a/lib/src/layer/shared/feature_layer/projection_simplification/state.dart +++ b/lib/src/layer/shared/layer_projection_simplification/state.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/widget.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:meta/meta.dart'; @@ -34,7 +34,7 @@ mixin ProjectionSimplificationManagement< /// Return the individual elements given the /// [ProjectionSimplificationManagementSupportedWidget] - Iterable get elements; + Iterable getElements(W widget); /// An iterable of simplified [ProjectedElement]s, which is always ready /// after the [build] method has been invoked, and should then be used in the @@ -64,6 +64,8 @@ mixin ProjectionSimplificationManagement< Widget build(BuildContext context) { final camera = MapCamera.of(context); + final elements = getElements(widget); + final projected = _cachedProjectedElements ??= List.generate( elements.length, (i) => projectElement( diff --git a/lib/src/layer/shared/feature_layer/projection_simplification/widget.dart b/lib/src/layer/shared/layer_projection_simplification/widget.dart similarity index 92% rename from lib/src/layer/shared/feature_layer/projection_simplification/widget.dart rename to lib/src/layer/shared/layer_projection_simplification/widget.dart index 8c6ae2f5e..2cd3b927f 100644 --- a/lib/src/layer/shared/feature_layer/projection_simplification/widget.dart +++ b/lib/src/layer/shared/layer_projection_simplification/widget.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/projection_simplification/state.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; /// A [StatefulWidget] that includes the properties used by the [State] component /// which mixes [ProjectionSimplificationManagement] in diff --git a/test/full_coverage_test.dart b/test/full_coverage_test.dart index f780a005d..3e1f687bd 100644 --- a/test/full_coverage_test.dart +++ b/test/full_coverage_test.dart @@ -17,7 +17,7 @@ import 'package:flutter_map/src/layer/marker_layer/marker_layer.dart'; import 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart'; import 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; import 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; -import 'package:flutter_map/src/layer/shared/feature_layer/interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; import 'package:flutter_map/src/layer/shared/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/shared/translucent_pointer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile.dart'; From b077c47f8f04a122a7764775acbb857c5512251d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 16 Feb 2025 12:38:39 +0000 Subject: [PATCH 10/11] Mix `HitDetectableElement` into `Polygon` & `Polyline` --- lib/src/layer/polygon_layer/polygon.dart | 4 ++-- lib/src/layer/polyline_layer/polyline.dart | 4 ++-- .../shared/layer_interactivity/internal_hit_detectable.dart | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index d9c788f0e..ecedd7ac4 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -1,7 +1,7 @@ part of 'polygon_layer.dart'; /// [Polygon] class, to be used for the [PolygonLayer]. -class Polygon { +class Polygon with HitDetectableElement { /// The points for the outline of the [Polygon]. final List points; @@ -70,7 +70,7 @@ class Polygon { /// it remains upright final bool rotateLabel; - /// {@macro fm.hde.hitValue} + @override final R? hitValue; /// Designates whether the given polygon points follow a clock or diff --git a/lib/src/layer/polyline_layer/polyline.dart b/lib/src/layer/polyline_layer/polyline.dart index 0ab84ccfc..b5712da66 100644 --- a/lib/src/layer/polyline_layer/polyline.dart +++ b/lib/src/layer/polyline_layer/polyline.dart @@ -1,7 +1,7 @@ part of 'polyline_layer.dart'; /// [Polyline] (aka. LineString) class, to be used for the [PolylineLayer]. -class Polyline { +class Polyline with HitDetectableElement { /// The list of coordinates for the [Polyline]. final List points; @@ -39,7 +39,7 @@ class Polyline { /// Set to true if the width of the stroke should have meters as unit. final bool useStrokeWidthInMeter; - /// {@macro fm.hde.hitValue} + @override final R? hitValue; LatLngBounds? _boundingBox; diff --git a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart index bd876282f..85e2e89c9 100644 --- a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart +++ b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart @@ -6,7 +6,6 @@ import 'package:meta/meta.dart'; @internal mixin HitDetectableElement { - /// {@template fm.hde.hitValue} /// Value to notify layer's `hitNotifier` with (such as /// [PolygonLayer.hitNotifier]) /// @@ -15,7 +14,6 @@ mixin HitDetectableElement { /// /// The object should have a valid & useful equality, as it may be used /// by FM internals. - /// {@endtemplate} R? get hitValue; } From 2baf1fd18bd3dbab9538e46d6e377bd4084ff1bf Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 16 Feb 2025 12:40:05 +0000 Subject: [PATCH 11/11] Convert `ProjectionSimplificationManagement.getElements` to getter --- lib/src/layer/polygon_layer/polygon_layer.dart | 2 +- lib/src/layer/polyline_layer/polyline_layer.dart | 3 +-- .../layer/shared/layer_projection_simplification/state.dart | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 0b1c6d568..6cf7172b5 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -121,7 +121,7 @@ class _PolygonLayerState extends State> ); @override - Iterable> getElements(PolygonLayer widget) => widget.polygons; + List> get elements => widget.polygons; @override Widget build(BuildContext context) { diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index 0464c3b38..f46b981b4 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -87,8 +87,7 @@ class _PolylineLayerState extends State> ); @override - Iterable> getElements(PolylineLayer widget) => - widget.polylines; + List> get elements => widget.polylines; @override Widget build(BuildContext context) { diff --git a/lib/src/layer/shared/layer_projection_simplification/state.dart b/lib/src/layer/shared/layer_projection_simplification/state.dart index b65cb0cd6..524815ce2 100644 --- a/lib/src/layer/shared/layer_projection_simplification/state.dart +++ b/lib/src/layer/shared/layer_projection_simplification/state.dart @@ -34,7 +34,7 @@ mixin ProjectionSimplificationManagement< /// Return the individual elements given the /// [ProjectionSimplificationManagementSupportedWidget] - Iterable getElements(W widget); + List get elements; /// An iterable of simplified [ProjectedElement]s, which is always ready /// after the [build] method has been invoked, and should then be used in the @@ -64,8 +64,6 @@ mixin ProjectionSimplificationManagement< Widget build(BuildContext context) { final camera = MapCamera.of(context); - final elements = getElements(widget); - final projected = _cachedProjectedElements ??= List.generate( elements.length, (i) => projectElement(