From df60d0dce2d493fdf6dfe934c15d38190aa9867d Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 7 Apr 2024 14:45:25 +0200 Subject: [PATCH] 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 --- example/lib/pages/polygon.dart | 8 +- lib/src/layer/pixel_hiker.dart | 340 ++++++++++++++++++ lib/src/layer/polygon_layer/painter.dart | 201 ++--------- lib/src/layer/polygon_layer/polygon.dart | 16 +- .../layer/polygon_layer/polygon_layer.dart | 1 + lib/src/layer/polyline_layer/painter.dart | 326 +++-------------- .../layer/polyline_layer/polyline_layer.dart | 1 + lib/src/layer/visible_segment.dart | 114 ++++++ 8 files changed, 547 insertions(+), 460 deletions(-) create mode 100644 lib/src/layer/pixel_hiker.dart create mode 100644 lib/src/layer/visible_segment.dart diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 5eebc35f4..7891bdf2e 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: PolylinePattern.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 PolylinePattern.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 PolylinePattern.dotted(), holePointsList: [ const [ LatLng(52, -17), diff --git a/lib/src/layer/pixel_hiker.dart b/lib/src/layer/pixel_hiker.dart new file mode 100644 index 000000000..2e6d46fe3 --- /dev/null +++ b/lib/src/layer/pixel_hiker.dart @@ -0,0 +1,340 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; +import 'package:flutter_map/src/layer/visible_segment.dart'; + +/// Pixel hiker that lists the visible dots to display on the way. +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. +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. +abstract 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/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..7b4dfb02d 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,11 @@ 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 this should be solid, dotted, or dashed, and the exact + /// characteristics of each + /// + /// Defaults to being a solid/unbroken line ([PolylinePattern.solid]). + final PolylinePattern pattern; /// **DEPRECATED** /// @@ -132,7 +134,7 @@ class Polygon { this.borderStrokeWidth = 0, this.borderColor = const Color(0xFFFFFF00), this.disableHolesBorder = false, - this.isDotted = false, + this.pattern = const PolylinePattern.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 +172,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 +195,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..d5716e2a6 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/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_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index 20372a317..fb06884de 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -5,6 +5,7 @@ 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/pixel_hiker.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/layer/visible_segment.dart b/lib/src/layer/visible_segment.dart new file mode 100644 index 000000000..1ec4753d4 --- /dev/null +++ b/lib/src/layer/visible_segment.dart @@ -0,0 +1,114 @@ +import 'dart:ui'; + +/// 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); + } + } + } +}