From 930e8b60e125e630e3c85ab373c0ebb8e9b08d70 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Wed, 10 Apr 2024 15:23:56 +0200 Subject: [PATCH] feat!: support of solid, dotted, dashed styles for polygons, with optimized rendering (#1865) * feat!: support of solid, dotted, dashed styles for polygons, with optimized rendering New files: * `pixel_hiker.dart`: Pixel hikers that list the visible items on the way. Code used to be in `polyline_layer/painter.dart`, but was heavily refactored with #1854 in mind * `visible_segment.dart`: Cohen-Sutherland algorithm to clip segments as visible into a canvas. Code used to be in `polygon_layer/painter.dart`, and was lightly refactored. Impacted files: * `polygon_layer/painter.dart`: now using new file `pixel_hiker.dart` for optimized rendering; moved "clip code" to new file `visible_segment.dart`; minor refactoring about parameter order consistency * `polyline_layer/painter.dart`: now using new file `pixel_hiker.dart` for optimized rendering; moved "pixel hiker" to new file `pixel_hiker.dart` * `pages/polygon.dart`: replaced `bool isDotted` with `PolylinePattern pattern` and in one case replaced it with "dashed" * `polygon_layer/polygon.dart`: BREAKING - replaced `bool isDotted` with `PolylinePattern pattern` * `polygon_layer/polygon_layer.dart`: minor refactoring * `polyline_layer/polyline_layer.dart`: minor refactoring * Renamed `PolylinePattern` to `StrokePattern` Re-organised file structure * Review changes Co-authored-by: monsieurtanuki * Update lib/src/layer/general/line_patterns/stroke_pattern.dart Co-authored-by: Joscha <34318751+josxha@users.noreply.github.com> * Update lib/src/layer/general/line_patterns/pixel_hiker.dart Co-authored-by: Joscha <34318751+josxha@users.noreply.github.com> * Update lib/src/layer/general/line_patterns/stroke_pattern.dart Co-authored-by: Joscha <34318751+josxha@users.noreply.github.com> * Minor file re-organisation * Fixed bug --------- Co-authored-by: JaffaKetchup Co-authored-by: Joscha <34318751+josxha@users.noreply.github.com> --- example/lib/pages/polygon.dart | 8 +- example/lib/pages/polyline.dart | 4 +- lib/flutter_map.dart | 7 +- .../{general => misc}/hit_detection.dart | 0 .../layer/misc/line_patterns/pixel_hiker.dart | 344 ++++++++++++++++++ .../line_patterns/stroke_pattern.dart} | 37 +- .../misc/line_patterns/visible_segment.dart | 114 ++++++ .../mobile_layer_transformer.dart | 0 .../translucent_pointer.dart | 0 lib/src/layer/polygon_layer/painter.dart | 201 ++-------- lib/src/layer/polygon_layer/polygon.dart | 18 +- .../layer/polygon_layer/polygon_layer.dart | 1 + lib/src/layer/polyline_layer/painter.dart | 326 ++--------------- lib/src/layer/polyline_layer/polyline.dart | 10 +- .../layer/polyline_layer/polyline_layer.dart | 2 +- test/full_coverage_test.dart | 6 +- 16 files changed, 588 insertions(+), 490 deletions(-) rename lib/src/layer/{general => misc}/hit_detection.dart (100%) create mode 100644 lib/src/layer/misc/line_patterns/pixel_hiker.dart rename lib/src/layer/{polyline_layer/pattern.dart => misc/line_patterns/stroke_pattern.dart} (81%) create mode 100644 lib/src/layer/misc/line_patterns/visible_segment.dart rename lib/src/layer/{general => misc}/mobile_layer_transformer.dart (100%) rename lib/src/layer/{general => misc}/translucent_pointer.dart (100%) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 5eebc35f4..978e5f54f 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -55,12 +55,12 @@ class _PolygonPageState extends State { LatLng(46.22, -0.11), LatLng(44.399, 1.76), ], - isDotted: true, + pattern: StrokePattern.dashed(segments: const [50, 20]), borderStrokeWidth: 4, borderColor: Colors.lightBlue, color: Colors.yellow, hitValue: ( - title: 'Polygon With Dotted Borders', + title: 'Polygon With Dashed Borders', subtitle: '...', ), ), @@ -105,7 +105,7 @@ class _PolygonPageState extends State { LatLng(54, -14), LatLng(54, -18), ].map((latlng) => LatLng(latlng.latitude, latlng.longitude + 8)).toList(), - isDotted: true, + pattern: const StrokePattern.dotted(), holePointsList: [ const [ LatLng(52, -17), @@ -151,7 +151,7 @@ class _PolygonPageState extends State { ] .map((latlng) => LatLng(latlng.latitude - 6, latlng.longitude + 8)) .toList(), - isDotted: true, + pattern: const StrokePattern.dotted(), holePointsList: [ const [ LatLng(52, -17), diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 32fa7314f..60ef904a8 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -104,7 +104,7 @@ class _PolylinePageState extends State { ], strokeWidth: 10, color: Colors.orange, - pattern: const PolylinePattern.dotted( + pattern: const StrokePattern.dotted( spacingFactor: 3, ), borderStrokeWidth: 8, @@ -138,7 +138,7 @@ class _PolylinePageState extends State { ], strokeWidth: 6, color: Colors.green[900]!, - pattern: PolylinePattern.dashed( + pattern: StrokePattern.dashed( segments: const [50, 20, 30, 20], ), borderStrokeWidth: 6, diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 6e309bd07..497b1b52f 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -28,10 +28,11 @@ export 'package:flutter_map/src/layer/attribution_layer/rich/source.dart'; export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart'; export 'package:flutter_map/src/layer/attribution_layer/simple.dart'; export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart'; -export 'package:flutter_map/src/layer/general/hit_detection.dart'; -export 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; -export 'package:flutter_map/src/layer/general/translucent_pointer.dart'; export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart'; +export 'package:flutter_map/src/layer/misc/hit_detection.dart'; +export 'package:flutter_map/src/layer/misc/line_patterns/stroke_pattern.dart'; +export 'package:flutter_map/src/layer/misc/mobile_layer_transformer.dart'; +export 'package:flutter_map/src/layer/misc/translucent_pointer.dart'; export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; export 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; diff --git a/lib/src/layer/general/hit_detection.dart b/lib/src/layer/misc/hit_detection.dart similarity index 100% rename from lib/src/layer/general/hit_detection.dart rename to lib/src/layer/misc/hit_detection.dart diff --git a/lib/src/layer/misc/line_patterns/pixel_hiker.dart b/lib/src/layer/misc/line_patterns/pixel_hiker.dart new file mode 100644 index 000000000..f0e719f08 --- /dev/null +++ b/lib/src/layer/misc/line_patterns/pixel_hiker.dart @@ -0,0 +1,344 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:meta/meta.dart'; + +part 'visible_segment.dart'; + +/// Pixel hiker that lists the visible dots to display on the way. +@internal +class DottedPixelHiker extends _PixelHiker { + /// Standard Dotted Pixel Hiker constructor. + DottedPixelHiker({ + required super.offsets, + required super.closePath, + required super.canvasSize, + required super.patternFit, + required double stepLength, + }) : super(segmentValues: [stepLength]); + + /// Returns all the visible dots. + List getAllVisibleDots() { + final List result = []; + + if (offsets.isEmpty) { + return result; + } + + void addVisibleOffset(final Offset offset) { + if (VisibleSegment.isVisible(offset, canvasSize)) { + result.add(offset); + } + } + + // side-effect of the first dot + addVisibleOffset(offsets.first); + + // normal dots + for (int i = 0; i < offsets.length - 1; i++) { + final List? visibleDots = + _getVisibleDotList(offsets[i], offsets[i + 1]); + if (visibleDots != null) { + result.addAll(visibleDots); + } + } + if (closePath) { + final List? visibleDots = + _getVisibleDotList(offsets.last, offsets.first); + if (visibleDots != null) { + result.addAll(visibleDots); + } + } + + // side-effect of the last dot + if (!closePath) { + if (patternFit != PatternFit.none) { + final last = result.last; + if (last != offsets.last) { + addVisibleOffset(offsets.last); + } + } + } + return result; + } + + /// Returns the visible dots between [offset0] and [offset1]. + /// + /// Most important method of the class. + List? _getVisibleDotList(Offset offset0, Offset offset1) { + final VisibleSegment? visibleSegment = + VisibleSegment.getVisibleSegment(offset0, offset1, canvasSize); + if (visibleSegment == null) { + addDistance(getDistance(offset0, offset1)); + return null; + } + if (offset0 != visibleSegment.begin) { + addDistance(getDistance(offset0, visibleSegment.begin)); + } + Offset start = visibleSegment.begin; + List? result; + + while (true) { + final Offset offsetIntermediary = + getIntermediateOffset(start, visibleSegment.end); + addDistance(_used); + if (_remaining == segmentValues.first) { + result ??= []; + result.add(offsetIntermediary); + nextSegment(); + } + if (offsetIntermediary == visibleSegment.end) { + if (offset1 != visibleSegment.end) { + addDistance(getDistance(visibleSegment.end, offset1)); + } + return result; + } + start = offsetIntermediary; + } + } + + @override + double getFactor() { + if (patternFit != PatternFit.scaleDown && + patternFit != PatternFit.scaleUp) { + return 1; + } + + if (_polylinePixelDistance == 0) { + return 0; + } + + final double stepLength = segmentValues.first; + final double factor = _polylinePixelDistance / stepLength; + + if (patternFit == PatternFit.scaleDown) { + return (factor.ceil() * stepLength + stepLength) / _polylinePixelDistance; + } + return (factor.floor() * stepLength + stepLength) / _polylinePixelDistance; + } +} + +/// Pixel hiker that lists the visible dashed segments to display on the way. +@internal +class DashedPixelHiker extends _PixelHiker { + /// Standard Dashed Pixel Hiker constructor. + DashedPixelHiker({ + required super.offsets, + required super.closePath, + required super.canvasSize, + required super.segmentValues, + required super.patternFit, + }); + + /// Returns all visible segments. + List getAllVisibleSegments() { + final List result = []; + + if (offsets.length < 2 || + segmentValues.length < 2 || + segmentValues.length.isOdd) { + return result; + } + + for (int i = 0; i < offsets.length - 1 + (closePath ? 1 : 0); i++) { + final List? visibleSegments = + _getVisibleSegmentList(offsets[i], offsets[(i + 1) % offsets.length]); + if (visibleSegments != null) { + result.addAll(visibleSegments); + } + } + + // last point side-effect, problematic if we're on a space and not a dash + if (_segmentIndex.isOdd) { + if (patternFit == PatternFit.appendDot) { + if (!closePath) { + if (VisibleSegment.isVisible(offsets.last, canvasSize)) { + result.add(VisibleSegment(offsets.last, offsets.last)); + } + } + } else if (patternFit == PatternFit.extendFinalDash) { + final lastOffset = closePath ? offsets.first : offsets.last; + final lastVisible = result.last.end; + if (lastOffset != lastVisible) { + result.add(VisibleSegment(lastVisible, lastOffset)); + } + } + } + + return result; + } + + /// Returns the visible segments between [offset0] and [offset1]. + /// + /// Most important method of the class. + List? _getVisibleSegmentList( + final Offset offset0, + final Offset offset1, + ) { + final VisibleSegment? visibleSegment = VisibleSegment.getVisibleSegment( + offset0, + offset1, + canvasSize, + ); + if (visibleSegment == null) { + addDistance(getDistance(offset0, offset1)); + return null; + } + if (offset0 != visibleSegment.begin) { + addDistance(getDistance(offset0, visibleSegment.begin)); + } + Offset start = visibleSegment.begin; + List? result; + + while (true) { + final Offset offsetIntermediary = + getIntermediateOffset(start, visibleSegment.end); + if (_segmentIndex.isEven) { + result ??= []; + result.add(VisibleSegment(start, offsetIntermediary)); + } + addDistance(_used); + if (_remaining == 0) { + nextSegment(); + } + if (offsetIntermediary == visibleSegment.end) { + if (offset1 != visibleSegment.end) { + addDistance(getDistance(visibleSegment.end, offset1)); + } + return result; + } + start = offsetIntermediary; + } + } + + /// Returns the factor for offset distances so that the dash pattern fits. + /// + /// The idea is that we need to be able to display the dash pattern completely + /// n times (at least once), plus once the initial dash segment. That's the + /// way we deal with the "ending" side-effect. + @override + double getFactor() { + if (patternFit != PatternFit.scaleDown && + patternFit != PatternFit.scaleUp) { + return 1; + } + + if (_polylinePixelDistance == 0) { + return 0; + } + + final double firstDashDistance = segmentValues.first; + final double factor = _polylinePixelDistance / _totalSegmentDistance; + if (patternFit == PatternFit.scaleDown) { + return (factor.ceil() * _totalSegmentDistance + firstDashDistance) / + _polylinePixelDistance; + } + return (factor.floor() * _totalSegmentDistance + firstDashDistance) / + _polylinePixelDistance; + } +} + +/// Pixel hiker that lists the visible items on the way. +sealed class _PixelHiker { + _PixelHiker({ + required this.offsets, + required this.segmentValues, + required this.closePath, + required this.canvasSize, + required this.patternFit, + }) { + _polylinePixelDistance = _getPolylinePixelDistance(); + _init(); + _factor = getFactor(); + } + + final List offsets; + final bool closePath; + final List segmentValues; + final Size canvasSize; + final PatternFit patternFit; + + /// Factor to be used on offset distances. + late final double _factor; + + late final double _polylinePixelDistance; + + late double _remaining; + late int _segmentIndex; + late final double _totalSegmentDistance; + late double _used; + + /// Returns the factor to apply to offset distances. + @protected + double getFactor(); + + @protected + double getDistance(final Offset offset0, final Offset offset1) => + _factor * (offset0 - offset1).distance; + + @protected + void addDistance(double distance) { + double modulus = distance % _totalSegmentDistance; + if (modulus == 0) { + return; + } + while (modulus >= _remaining) { + modulus -= _remaining; + nextSegment(); + } + _remaining -= modulus; + } + + @protected + void nextSegment() { + _segmentIndex = (_segmentIndex + 1) % segmentValues.length; + _remaining = segmentValues[_segmentIndex]; + } + + void _init() { + _totalSegmentDistance = _getTotalSegmentDistance(segmentValues); + _segmentIndex = segmentValues.length - 1; + _remaining = 0; + nextSegment(); + } + + /// Returns the offset on segment [A,B] that matches the remaining distance. + @protected + Offset getIntermediateOffset(final Offset offsetA, final Offset offsetB) { + final segmentDistance = getDistance(offsetA, offsetB); + if (_remaining >= segmentDistance) { + _used = segmentDistance; + return offsetB; + } + final fB = _remaining / segmentDistance; + final fA = 1.0 - fB; + _used = _remaining; + return Offset( + offsetA.dx * fA + offsetB.dx * fB, + offsetA.dy * fA + offsetB.dy * fB, + ); + } + + double _getPolylinePixelDistance() { + if (offsets.length < 2) { + return 0; + } + double result = 0; + for (int i = 1; i < offsets.length; i++) { + final Offset offsetA = offsets[i - 1]; + final Offset offsetB = offsets[i]; + result += (offsetA - offsetB).distance; + } + if (closePath) { + result += (offsets.last - offsets.first).distance; + } + return result; + } + + double _getTotalSegmentDistance(List segmentValues) { + double result = 0; + for (final double value in segmentValues) { + result += value; + } + return result; + } +} diff --git a/lib/src/layer/polyline_layer/pattern.dart b/lib/src/layer/misc/line_patterns/stroke_pattern.dart similarity index 81% rename from lib/src/layer/polyline_layer/pattern.dart rename to lib/src/layer/misc/line_patterns/stroke_pattern.dart index 5dd910de9..54d804d43 100644 --- a/lib/src/layer/polyline_layer/pattern.dart +++ b/lib/src/layer/misc/line_patterns/stroke_pattern.dart @@ -1,11 +1,14 @@ -part of 'polyline_layer.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_map/flutter_map.dart'; -/// Determines whether a [Polyline] should be solid, dotted, or dashed, and the +/// Determines whether a stroke should be solid, dotted, or dashed, and the /// exact characteristics of each +/// +/// A stroke is either a [Polyline] itself, or the border of a [Polygon]. @immutable -class PolylinePattern { +class StrokePattern { /// Solid/unbroken - const PolylinePattern.solid() + const StrokePattern.solid() : spacingFactor = null, segments = null, patternFit = null; @@ -15,19 +18,20 @@ class PolylinePattern { /// See [spacingFactor] and [PatternFit] for more information about parameters. /// [spacingFactor] defaults to 1.5, and [patternFit] defaults to /// [PatternFit.scaleUp]. - const PolylinePattern.dotted({ + const StrokePattern.dotted({ double this.spacingFactor = 1.5, PatternFit this.patternFit = PatternFit.scaleUp, - }) : segments = null; + }) : segments = null, + assert(spacingFactor > 0, 'spacingFactor must be > 0'); /// Elongated dashes, with length and spacing set by [segments] /// - /// Dashes may not be linear: they may pass through different [Polyline.points] - /// without regard to their relative bearing/direction. + /// Dashes may not be linear: they may pass through different points without + /// regard to their relative bearing/direction. /// /// See [segments] and [PatternFit] for more information about parameters. /// [patternFit] defaults to [PatternFit.scaleUp]. - const PolylinePattern.dashed({ + const StrokePattern.dashed({ required List this.segments, PatternFit this.patternFit = PatternFit.scaleUp, }) : assert( @@ -42,7 +46,8 @@ class PolylinePattern { spacingFactor = null; /// The multiplier used to calculate the spacing between dots in a dotted - /// polyline, with respect to [Polyline.strokeWidth] + /// polyline, with respect to [Polyline.strokeWidth]/ + /// [Polygon.borderStrokeWidth] /// /// A value of 1.0 will result in spacing equal to the `strokeWidth`. /// Increasing the value increases the spacing with the same scaling. @@ -80,7 +85,7 @@ class PolylinePattern { /// * etc... final List? segments; - /// Determines how a non-solid [PolylinePattern] should be fit to a [Polyline] + /// Determines how a non-solid [StrokePattern] should be fit to a line /// when their lengths are not equal or multiples /// /// Defaults to [PatternFit.scaleUp]. @@ -89,7 +94,7 @@ class PolylinePattern { @override bool operator ==(Object other) => identical(this, other) || - (other is PolylinePattern && + (other is StrokePattern && spacingFactor == other.spacingFactor && patternFit == other.patternFit && ((segments == null && other.segments == null) || @@ -99,10 +104,10 @@ class PolylinePattern { int get hashCode => Object.hash(spacingFactor, segments, patternFit); } -/// Determines how a non-solid [PolylinePattern] should be fit to a [Polyline] +/// Determines how a non-solid [StrokePattern] should be fit to a line /// when their lengths are not equal or multiples /// -/// [PolylinePattern.solid]s do not require fitting. +/// [StrokePattern.solid]s do not require fitting. enum PatternFit { /// Don't apply any specific fit to the pattern - repeat exactly as specified, /// and stop when the last point is reached @@ -124,8 +129,8 @@ enum PatternFit { /// last point (there is a gap at that location) appendDot, - /// (Only valid for [PolylinePattern.dashed], equal to [appendDot] for - /// [PolylinePattern.dotted]) + /// (Only valid for [StrokePattern.dashed], equal to [appendDot] for + /// [StrokePattern.dotted]) /// /// Uses the pattern exactly, truncating the final dash if it does not fit, or /// extending the final dash to the last point if it would not normally reach diff --git a/lib/src/layer/misc/line_patterns/visible_segment.dart b/lib/src/layer/misc/line_patterns/visible_segment.dart new file mode 100644 index 000000000..6c255f36a --- /dev/null +++ b/lib/src/layer/misc/line_patterns/visible_segment.dart @@ -0,0 +1,114 @@ +part of 'pixel_hiker.dart'; + +/// Cohen-Sutherland algorithm to clip segments as visible into a canvas. +class VisibleSegment { + /// Segment between [begin] and [end]. + const VisibleSegment(this.begin, this.end); + + /// Begin of the segment. + final Offset begin; + + /// End of the segment. + final Offset end; + + @override + String toString() => 'VisibleSegment($begin, $end)'; + + // OutCodes for the Cohen-Sutherland algorithm + static const _inside = 0; // 0000 + static const _left = 1; // 0001 + static const _right = 2; // 0010 + static const _bottom = 4; // 0100 + static const _top = 8; // 1000 + + static int _computeOutCode( + double x, double y, double xMin, double yMin, double xMax, double yMax) { + int code = _inside; + + if (x < xMin) { + code |= _left; + } else if (x > xMax) { + code |= _right; + } + if (y < yMin) { + code |= _bottom; + } else if (y > yMax) { + code |= _top; + } + + return code; + } + + /// Returns true if the [offset] is inside the [canvasSize]. + static bool isVisible(Offset offset, Size canvasSize) => + _computeOutCode( + offset.dx, offset.dy, 0, 0, canvasSize.width, canvasSize.height) == + _inside; + + /// Clips a line segment to a rectangular area (canvas). + /// + /// Returns null if the segment is invisible. + static VisibleSegment? getVisibleSegment( + Offset p0, Offset p1, Size canvasSize) { + // Function to compute the outCode for a point relative to the canvas + + const double xMin = 0; + const double yMin = 0; + final double xMax = canvasSize.width; + final double yMax = canvasSize.height; + + double x0 = p0.dx; + double y0 = p0.dy; + double x1 = p1.dx; + double y1 = p1.dy; + + int outCode0 = _computeOutCode(x0, y0, xMin, yMin, xMax, yMax); + int outCode1 = _computeOutCode(x1, y1, xMin, yMin, xMax, yMax); + + while (true) { + if ((outCode0 | outCode1) == 0) { + // Both points inside; trivially accept + // Make sure we return the points within the canvas + return VisibleSegment(Offset(x0, y0), Offset(x1, y1)); + } + + if ((outCode0 & outCode1) != 0) { + // Both points share an outside zone; trivially reject + return null; + } + + // Could be partially inside; calculate intersection + final double x; + final double y; + final int outCodeOut = outCode0 != 0 ? outCode0 : outCode1; + + if ((outCodeOut & _top) != 0) { + x = x0 + (x1 - x0) * (yMax - y0) / (y1 - y0); + y = yMax; + } else if ((outCodeOut & _bottom) != 0) { + x = x0 + (x1 - x0) * (yMin - y0) / (y1 - y0); + y = yMin; + } else if ((outCodeOut & _right) != 0) { + y = y0 + (y1 - y0) * (xMax - x0) / (x1 - x0); + x = xMax; + } else if ((outCodeOut & _left) != 0) { + y = y0 + (y1 - y0) * (xMin - x0) / (x1 - x0); + x = xMin; + } else { + // This else block should never be reached. + return null; + } + + // Update the point and outCode + if (outCodeOut == outCode0) { + x0 = x; + y0 = y; + outCode0 = _computeOutCode(x0, y0, xMin, yMin, xMax, yMax); + } else { + x1 = x; + y1 = y; + outCode1 = _computeOutCode(x1, y1, xMin, yMin, xMax, yMax); + } + } + } +} diff --git a/lib/src/layer/general/mobile_layer_transformer.dart b/lib/src/layer/misc/mobile_layer_transformer.dart similarity index 100% rename from lib/src/layer/general/mobile_layer_transformer.dart rename to lib/src/layer/misc/mobile_layer_transformer.dart diff --git a/lib/src/layer/general/translucent_pointer.dart b/lib/src/layer/misc/translucent_pointer.dart similarity index 100% rename from lib/src/layer/general/translucent_pointer.dart rename to lib/src/layer/misc/translucent_pointer.dart diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index a26202bee..4937a90c8 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -28,13 +28,6 @@ class _PolygonPainter extends CustomPainter { final _hits = []; // Avoids repetitive memory reallocation - // OutCodes for the Cohen-Sutherland algorithm - static const _csInside = 0; // 0000 - static const _csLeft = 1; // 0001 - static const _csRight = 2; // 0010 - static const _csBottom = 4; // 0100 - static const _csTop = 8; // 1000 - /// Create a new [_PolygonPainter] instance. _PolygonPainter({ required this.polygons, @@ -220,8 +213,8 @@ class _PolygonPainter extends CustomPainter { points: projectedPolygon.points, ), size, - _getBorderPaint(polygon), canvas, + _getBorderPaint(polygon), ); } @@ -305,7 +298,7 @@ class _PolygonPainter extends CustomPainter { } Paint _getBorderPaint(Polygon polygon) { - final isDotted = polygon.isDotted; + final isDotted = polygon.pattern.spacingFactor != null; return Paint() ..color = polygon.borderColor ..strokeWidth = polygon.borderStrokeWidth @@ -319,14 +312,35 @@ class _PolygonPainter extends CustomPainter { Polygon polygon, List offsets, Size canvasSize, - Paint paint, Canvas canvas, + Paint paint, ) { - if (polygon.isDotted) { - final borderRadius = polygon.borderStrokeWidth / 2; - final spacing = polygon.borderStrokeWidth * 1.5; - _addDottedLineToPath( - canvas, paint, offsets, borderRadius, spacing, canvasSize); + final isDashed = polygon.pattern.segments != null; + final isDotted = polygon.pattern.spacingFactor != null; + if (isDotted) { + final DottedPixelHiker hiker = DottedPixelHiker( + offsets: offsets, + stepLength: polygon.borderStrokeWidth * polygon.pattern.spacingFactor!, + patternFit: polygon.pattern.patternFit!, + closePath: true, + canvasSize: canvasSize, + ); + for (final visibleDot in hiker.getAllVisibleDots()) { + canvas.drawCircle(visibleDot, polygon.borderStrokeWidth / 2, paint); + } + } else if (isDashed) { + final DashedPixelHiker hiker = DashedPixelHiker( + offsets: offsets, + segmentValues: polygon.pattern.segments!, + patternFit: polygon.pattern.patternFit!, + closePath: true, + canvasSize: canvasSize, + ); + + for (final visibleSegment in hiker.getAllVisibleSegments()) { + path.moveTo(visibleSegment.begin.dx, visibleSegment.begin.dy); + path.lineTo(visibleSegment.end.dx, visibleSegment.end.dy); + } } else { _addLineToPath(path, offsets); } @@ -340,154 +354,15 @@ class _PolygonPainter extends CustomPainter { Canvas canvas, Paint paint, ) { - if (polygon.isDotted) { - final borderRadius = polygon.borderStrokeWidth / 2; - final spacing = polygon.borderStrokeWidth * 1.5; - for (final offsets in holeOffsetsList) { - _addDottedLineToPath( - canvas, paint, offsets, borderRadius, spacing, canvasSize); - } - } else { - for (final offsets in holeOffsetsList) { - _addLineToPath(path, offsets); - } - } - } - - // Function to clip a line segment to a rectangular area (canvas) - List? _getVisibleSegment(Offset p0, Offset p1, Size canvasSize) { - // Function to compute the outCode for a point relative to the canvas - int computeOutCode( - double x, - double y, - double xMin, - double yMin, - double xMax, - double yMax, - ) { - int code = _csInside; - - if (x < xMin) { - code |= _csLeft; - } else if (x > xMax) { - code |= _csRight; - } - if (y < yMin) { - code |= _csBottom; - } else if (y > yMax) { - code |= _csTop; - } - - return code; - } - - const double xMin = 0; - const double yMin = 0; - final double xMax = canvasSize.width; - final double yMax = canvasSize.height; - - double x0 = p0.dx; - double y0 = p0.dy; - double x1 = p1.dx; - double y1 = p1.dy; - - int outCode0 = computeOutCode(x0, y0, xMin, yMin, xMax, yMax); - int outCode1 = computeOutCode(x1, y1, xMin, yMin, xMax, yMax); - bool accept = false; - - while (true) { - if ((outCode0 | outCode1) == 0) { - // Both points inside; trivially accept - accept = true; - break; - } else if ((outCode0 & outCode1) != 0) { - // Both points share an outside zone; trivially reject - break; - } else { - // Could be partially inside; calculate intersection - double x; - double y; - final int outCodeOut = outCode0 != 0 ? outCode0 : outCode1; - - if ((outCodeOut & _csTop) != 0) { - x = x0 + (x1 - x0) * (yMax - y0) / (y1 - y0); - y = yMax; - } else if ((outCodeOut & _csBottom) != 0) { - x = x0 + (x1 - x0) * (yMin - y0) / (y1 - y0); - y = yMin; - } else if ((outCodeOut & _csRight) != 0) { - y = y0 + (y1 - y0) * (xMax - x0) / (x1 - x0); - x = xMax; - } else if ((outCodeOut & _csLeft) != 0) { - y = y0 + (y1 - y0) * (xMin - x0) / (x1 - x0); - x = xMin; - } else { - // This else block should never be reached. - break; - } - - // Update the point and outCode - if (outCodeOut == outCode0) { - x0 = x; - y0 = y; - outCode0 = computeOutCode(x0, y0, xMin, yMin, xMax, yMax); - } else { - x1 = x; - y1 = y; - outCode1 = computeOutCode(x1, y1, xMin, yMin, xMax, yMax); - } - } - } - - if (accept) { - // Make sure we return the points within the canvas - return [Offset(x0, y0), Offset(x1, y1)]; - } - return null; - } - - void _addDottedLineToPath( - Canvas canvas, - Paint paint, - List offsets, - double radius, - double stepLength, - Size canvasSize, - ) { - if (offsets.isEmpty) { - return; - } - - // Calculate for all segments, including closing the loop from the last to the first point - final int totalOffsets = offsets.length; - for (int i = 0; i < totalOffsets; i++) { - final Offset start = offsets[i % totalOffsets]; - final Offset end = - offsets[(i + 1) % totalOffsets]; // Wrap around to the first point - - // Attempt to adjust the segment to the visible part of the canvas - final List? visibleSegment = - _getVisibleSegment(start, end, canvasSize); - if (visibleSegment == null) { - continue; // Skip if the segment is completely outside - } - - final Offset adjustedStart = visibleSegment[0]; - final Offset adjustedEnd = visibleSegment[1]; - final double lineLength = (adjustedStart - adjustedEnd).distance; - final Offset stepVector = - (adjustedEnd - adjustedStart) / lineLength * stepLength; - double traveledDistance = 0; - - Offset currentPoint = adjustedStart; - while (traveledDistance < lineLength) { - // Draw the circle if within the canvas bounds (additional check now redundant) - canvas.drawCircle(currentPoint, radius, paint); - - // Move to the next point - currentPoint = currentPoint + stepVector; - traveledDistance += stepLength; - } + for (final offsets in holeOffsetsList) { + _addBorderToPath( + path, + polygon, + offsets, + canvasSize, + canvas, + paint, + ); } } diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index d828cbd4b..af279278e 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -20,7 +20,7 @@ class Polygon { /// The fill color of the [Polygon]. final Color? color; - /// The stroke with of the [Polygon] outline. + /// The stroke width of the [Polygon] outline. final double borderStrokeWidth; /// The color of the [Polygon] outline. @@ -31,9 +31,13 @@ class Polygon { /// Defaults to false (enabled). final bool disableHolesBorder; - /// Set to true if the border of the [Polygon] should be rendered - /// as dotted line. - final bool isDotted; + /// Determines whether the border (if visible) should be solid, dotted, or + /// dashed, and the exact characteristics of each + /// + /// Defaults to being a solid/unbroken line ([StrokePattern.solid]). + /// Note that there is no border by default: increase [borderStrokeWidth] to + /// display it. + final StrokePattern pattern; /// **DEPRECATED** /// @@ -132,7 +136,7 @@ class Polygon { this.borderStrokeWidth = 0, this.borderColor = const Color(0xFFFFFF00), this.disableHolesBorder = false, - this.isDotted = false, + this.pattern = const StrokePattern.solid(), @Deprecated( 'Prefer setting `color` to null to disable filling, or a `Color` to enable filling of that color. ' 'This parameter will be removed to simplify the API, as this was a remnant of pre-null-safety. ' @@ -170,7 +174,7 @@ class Polygon { borderStrokeWidth == other.borderStrokeWidth && borderColor == other.borderColor && disableHolesBorder == other.disableHolesBorder && - isDotted == other.isDotted && + pattern == other.pattern && // ignore: deprecated_member_use_from_same_package isFilled == other.isFilled && strokeCap == other.strokeCap && @@ -193,7 +197,7 @@ class Polygon { borderStrokeWidth, borderColor, disableHolesBorder, - isDotted, + pattern, // ignore: deprecated_member_use_from_same_package isFilled, strokeCap, diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 5f6c7bac9..918ee9156 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -6,6 +6,7 @@ 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/misc/line_patterns/pixel_hiker.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart' hide Path; diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index ba88e78a8..1f7958384 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -1,6 +1,6 @@ part of 'polyline_layer.dart'; -/// [CustomPainter] for [Polygon]s. +/// [CustomPainter] for [Polyline]s. class _PolylinePainter extends CustomPainter { /// Reference to the list of [Polyline]s. final List<_ProjectedPolyline> polylines; @@ -201,162 +201,59 @@ class _PolylinePainter extends CustomPainter { final radius = paint.strokeWidth / 2; final borderRadius = (borderPaint?.strokeWidth ?? 0) / 2; + final List paths = []; + if (borderPaint != null && filterPaint != null) { + paths.add(borderPath); + paths.add(filterPath); + } + paths.add(path); if (isDotted) { - final spacing = strokeWidth * polyline.pattern.spacingFactor!; - if (borderPaint != null && filterPaint != null) { - _paintDottedLine( - borderPath, offsets, borderRadius, spacing, polyline.pattern); - _paintDottedLine( - filterPath, offsets, radius, spacing, polyline.pattern); - } - _paintDottedLine(path, offsets, radius, spacing, polyline.pattern); - } else if (isDashed) { - if (borderPaint != null && filterPaint != null) { - _paintDashedLine(borderPath, offsets, polyline.pattern); - _paintDashedLine(filterPath, offsets, polyline.pattern); - } - _paintDashedLine(path, offsets, polyline.pattern); - } else { + final DottedPixelHiker hiker = DottedPixelHiker( + offsets: offsets, + stepLength: strokeWidth * polyline.pattern.spacingFactor!, + patternFit: polyline.pattern.patternFit!, + closePath: false, + canvasSize: size, + ); + + final List radii = []; if (borderPaint != null && filterPaint != null) { - _paintLine(borderPath, offsets); - _paintLine(filterPath, offsets); + radii.add(borderRadius); + radii.add(radius); } - _paintLine(path, offsets); - } - } - - drawPaths(); - } - - void _paintDottedLine( - ui.Path path, - List offsets, - double radius, - double stepLength, - PolylinePattern pattern, - ) { - final PatternFit patternFit = pattern.patternFit!; - - if (offsets.isEmpty) return; - - if (offsets.length == 1) { - path.addOval(Rect.fromCircle(center: offsets.last, radius: radius)); - return; - } - - int offsetIndex = 0; - - Offset offset0 = offsets[offsetIndex++]; - Offset offset1 = offsets[offsetIndex++]; + radii.add(radius); - final _PixelHiker hiker = _PixelHiker.dotted( - offsets: offsets, - stepLength: stepLength, - patternFit: patternFit, - ); - path.addOval(Rect.fromCircle(center: offsets.first, radius: radius)); - while (true) { - final Offset newOffset = hiker.getIntermediateOffset(offset0, offset1); - - if (hiker.goToNextOffsetIfNeeded()) { - if (offsetIndex >= offsets.length) { - if (patternFit != PatternFit.none) { - path.addOval(Rect.fromCircle(center: newOffset, radius: 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])); } - return; } - offset0 = offset1; - offset1 = offsets[offsetIndex++]; - } else { - offset0 = newOffset; - } - - if (hiker.goToNextSegmentIfNeeded()) { - path.addOval(Rect.fromCircle(center: newOffset, radius: radius)); - } - } - } - - void _paintDashedLine( - ui.Path path, - List offsets, - PolylinePattern pattern, - ) { - final List segmentValues = pattern.segments!; - final PatternFit patternFit = pattern.patternFit!; - - if (offsets.length < 2 || - segmentValues.length < 2 || - segmentValues.length.isOdd) { - return; - } - - int offsetIndex = 0; - - Offset offset0 = offsets[offsetIndex++]; - Offset offset1 = offsets[offsetIndex++]; - - Offset? latestMoveTo; - - void moveTo(final Offset offset) { - latestMoveTo = offset; - } - - void lineTo(final Offset offset) { - if (latestMoveTo != null) { - path.moveTo(latestMoveTo!.dx, latestMoveTo!.dy); - latestMoveTo = null; - } - path.lineTo(offset.dx, offset.dy); - } + } else if (isDashed) { + final DashedPixelHiker hiker = DashedPixelHiker( + offsets: offsets, + segmentValues: polyline.pattern.segments!, + patternFit: polyline.pattern.patternFit!, + closePath: false, + canvasSize: size, + ); - final _PixelHiker hiker = _PixelHiker.dashed( - offsets: offsets, - segmentValues: segmentValues, - patternFit: patternFit, - ); - moveTo(offset0); - while (true) { - final Offset newOffset = hiker.getIntermediateOffset(offset0, offset1); - - if (hiker.segmentIndex.isOdd) { - if (hiker.isLastSegment && patternFit == PatternFit.extendFinalDash) { - lineTo(newOffset); - } else { - moveTo(newOffset); + 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); + } } } else { - lineTo(newOffset); - } - - if (hiker.goToNextOffsetIfNeeded()) { - // was it the last point? - if (offsetIndex >= offsets.length) { - if (hiker.segmentIndex.isOdd) { - // Were we on a "space-dash"? - if (patternFit == PatternFit.appendDot) { - // Add a dot at the new point. - moveTo(newOffset); - lineTo(newOffset); - } + if (offsets.isNotEmpty) { + for (final path in paths) { + path.addPolygon(offsets, false); } - return; } - offset0 = offset1; - offset1 = offsets[offsetIndex++]; - } else { - offset0 = newOffset; } - - hiker.goToNextSegmentIfNeeded(); } - } - void _paintLine(ui.Path path, List offsets) { - if (offsets.isEmpty) { - return; - } - path.addPolygon(offsets, false); + drawPaths(); } ui.Gradient _paintGradient(Polyline polyline, List offsets) => @@ -401,146 +298,3 @@ class _PolylinePainter extends CustomPainter { } const _distance = Distance(); - -class _PixelHiker { - final double _polylinePixelDistance; - final List _segmentValues; - - /// Factor to be used on offset distances. - late final double _factor; - - double _distanceSoFar = 0; - int _segmentIndex = 0; - - _PixelHiker.dotted({ - required List offsets, - required double stepLength, - required PatternFit patternFit, - }) : _polylinePixelDistance = _getPolylinePixelDistance(offsets), - _segmentValues = [stepLength] { - _factor = _getDottedFactor(patternFit); - _setRemaining(_segmentValues[_segmentIndex]); - } - - _PixelHiker.dashed({ - required List offsets, - required List segmentValues, - required PatternFit patternFit, - }) : _polylinePixelDistance = _getPolylinePixelDistance(offsets), - _segmentValues = segmentValues { - _factor = _getDashedFactor(patternFit); - _setRemaining(_segmentValues[_segmentIndex]); - } - - /// Segment pixel length remaining. - late double _remaining; - void _setRemaining(double value) { - _remaining = value; - _distanceSoFar += value; - } - - int get segmentIndex => _segmentIndex; - - bool get isLastSegment => _polylinePixelDistance - _distanceSoFar < 0; - bool _doneWithCurrentOffset = false; - - bool goToNextOffsetIfNeeded() { - if (_doneWithCurrentOffset) { - _doneWithCurrentOffset = false; - return true; - } - return false; - } - - bool goToNextSegmentIfNeeded() { - if (_remaining == 0) { - _segmentIndex++; - _setRemaining(_segmentValues[_segmentIndex % _segmentValues.length]); - return true; - } - return false; - } - - /// Returns the offset on segment [A,B] that matches the remaining distance. - Offset getIntermediateOffset(final Offset offsetA, final Offset offsetB) { - final segmentDistance = _factor * (offsetA - offsetB).distance; - if (_remaining >= segmentDistance) { - _remaining -= segmentDistance; - _doneWithCurrentOffset = true; - return offsetB; - } - final fB = _remaining / segmentDistance; - final fA = 1.0 - fB; - _setRemaining(0); - return Offset( - offsetA.dx * fA + offsetB.dx * fB, - offsetA.dy * fA + offsetB.dy * fB, - ); - } - - static double _getPolylinePixelDistance(List offsets) { - double result = 0; - if (offsets.length < 2) { - return result; - } - for (int i = 1; i < offsets.length; i++) { - final Offset offsetA = offsets[i - 1]; - final Offset offsetB = offsets[i]; - result += (offsetA - offsetB).distance; - } - return result; - } - - double _getDottedFactor(PatternFit patternFit) { - if (patternFit != PatternFit.scaleDown && - patternFit != PatternFit.scaleUp) { - return 1; - } - - if (_polylinePixelDistance == 0) { - return 0; - } - - final double stepLength = _segmentValues.first; - final double factor = _polylinePixelDistance / stepLength; - - if (patternFit == PatternFit.scaleDown) { - return (factor.ceil() * stepLength + stepLength) / _polylinePixelDistance; - } - return (factor.floor() * stepLength + stepLength) / _polylinePixelDistance; - } - - /// Returns the factor for offset distances so that the dash pattern fits. - /// - /// The idea is that we need to be able to display the dash pattern completely - /// n times (at least once), plus once the initial dash segment. That's the - /// way we deal with the "ending" side-effect. - double _getDashedFactor(PatternFit patternFit) { - if (patternFit != PatternFit.scaleDown && - patternFit != PatternFit.scaleUp) { - return 1; - } - - if (_polylinePixelDistance == 0) { - return 0; - } - - double getTotalSegmentDistance(List segmentValues) { - double result = 0; - for (final double value in segmentValues) { - result += value; - } - return result; - } - - final double totalDashDistance = getTotalSegmentDistance(_segmentValues); - final double firstDashDistance = _segmentValues.first; - final double factor = _polylinePixelDistance / totalDashDistance; - if (patternFit == PatternFit.scaleDown) { - return (factor.ceil() * totalDashDistance + firstDashDistance) / - _polylinePixelDistance; - } - return (factor.floor() * totalDashDistance + firstDashDistance) / - _polylinePixelDistance; - } -} diff --git a/lib/src/layer/polyline_layer/polyline.dart b/lib/src/layer/polyline_layer/polyline.dart index 81d15a311..0ddd5af31 100644 --- a/lib/src/layer/polyline_layer/polyline.dart +++ b/lib/src/layer/polyline_layer/polyline.dart @@ -8,11 +8,11 @@ class Polyline { /// The width of the stroke final double strokeWidth; - /// Determines whether this should be solid, dotted, or dashed, and the exact - /// characteristics of each + /// Determines whether the line should be solid, dotted, or dashed, and the + /// exact characteristics of each /// - /// Defaults to being a solid/unbroken line ([PolylinePattern.solid]). - final PolylinePattern pattern; + /// Defaults to being a solid/unbroken line ([StrokePattern.solid]). + final StrokePattern pattern; /// The color of the line stroke. final Color color; @@ -51,7 +51,7 @@ class Polyline { Polyline({ required this.points, this.strokeWidth = 1.0, - this.pattern = const PolylinePattern.solid(), + this.pattern = const StrokePattern.solid(), this.color = const Color(0xFF00FF00), this.borderStrokeWidth = 0.0, this.borderColor = const Color(0xFFFFFF00), diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index 20372a317..ed96eddc5 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -5,12 +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/misc/line_patterns/pixel_hiker.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; part 'painter.dart'; -part 'pattern.dart'; part 'polyline.dart'; part 'projected_polyline.dart'; diff --git a/test/full_coverage_test.dart b/test/full_coverage_test.dart index 1816b0580..41f3a3405 100644 --- a/test/full_coverage_test.dart +++ b/test/full_coverage_test.dart @@ -13,10 +13,10 @@ import 'package:flutter_map/src/layer/attribution_layer/rich/source.dart'; import 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart'; import 'package:flutter_map/src/layer/attribution_layer/simple.dart'; import 'package:flutter_map/src/layer/circle_layer/circle_layer.dart'; -import 'package:flutter_map/src/layer/general/hit_detection.dart'; -import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; -import 'package:flutter_map/src/layer/general/translucent_pointer.dart'; import 'package:flutter_map/src/layer/marker_layer/marker_layer.dart'; +import 'package:flutter_map/src/layer/misc/hit_detection.dart'; +import 'package:flutter_map/src/layer/misc/mobile_layer_transformer.dart'; +import 'package:flutter_map/src/layer/misc/translucent_pointer.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';