Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: added PolylinePattern with support for solid, dotted, dashed styles #1855

Merged
merged 8 commits into from
Mar 27, 2024
62 changes: 38 additions & 24 deletions example/lib/pages/polyline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class _PolylinePageState extends State<PolylinePage> {
strokeWidth: 8,
color: const Color(0xFF60399E),
hitValue: (
title: 'Elizabeth Line',
title: 'Purple Line',
subtitle: 'Nothing really special here...',
),
),
Expand Down Expand Up @@ -77,22 +77,7 @@ class _PolylinePageState extends State<PolylinePage> {
borderStrokeWidth: 20,
borderColor: Colors.red.withOpacity(0.4),
hitValue: (
title: 'BlueRed Line',
subtitle: 'Solid translucent color fill, with different color outline',
),
),
Polyline(
points: const [
LatLng(50.2, -0.08),
LatLng(51.2498, -10.2603),
LatLng(54.8566, -9.3522),
],
strokeWidth: 20,
color: Colors.black.withOpacity(0.2),
borderStrokeWidth: 20,
borderColor: Colors.white30,
hitValue: (
title: 'BlackWhite Line',
title: 'Bordered Line',
subtitle: 'Solid translucent color fill, with different color outline',
),
),
Expand All @@ -107,7 +92,7 @@ class _PolylinePageState extends State<PolylinePage> {
borderStrokeWidth: 10,
borderColor: Colors.blue.withOpacity(0.5),
hitValue: (
title: 'YellowBlue Line',
title: 'BorderedLine 2',
subtitle: 'Solid translucent color fill, with different color outline',
),
),
Expand All @@ -118,15 +103,44 @@ class _PolylinePageState extends State<PolylinePage> {
LatLng(35.566530, 5.584283),
],
strokeWidth: 10,
color: Colors.blueAccent,
isDotted: true,
segmentSpacingFactor: 3,
color: Colors.orange,
pattern: const PolylinePattern.dotted(spacingFactor: 3),
borderStrokeWidth: 8,
borderColor: Colors.blue.withOpacity(0.5),
hitValue: (
title: 'Blue Dotted Line with Custom Spacing',
subtitle:
'Dotted line with segment spacing controlled by `segmentSpacingFactor`',
title: 'Orange line',
subtitle: 'Dotted pattern',
),
),
// Paris-Nice TGV
Polyline(
points: const [
// Paris
LatLng(48.8567, 2.3519),
// Lyon
LatLng(45.7256, 5.0811),
// Avignon
LatLng(43.95, 4.8169),
// Aix-en-Provence
LatLng(43.5311, 5.4539),
// Marseille
LatLng(43.2964, 5.37),
// Toulon
LatLng(43.1222, 5.93),
// Cannes
LatLng(43.5514, 7.0128),
// Antibes
LatLng(43.5808, 7.1239),
// Nice
LatLng(43.6958, 7.2714),
],
strokeWidth: 6,
color: Colors.green[900]!,
pattern: PolylinePattern.dashed(segments: const [50, 20, 30, 20]),
borderStrokeWidth: 6,
hitValue: (
title: 'Green Line',
subtitle: 'Dashed line',
),
),
];
Expand Down
83 changes: 81 additions & 2 deletions lib/src/layer/polyline_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ class _PolylinePainter<R extends Object> extends CustomPainter {
strokeWidth = polyline.strokeWidth;
}

final isDotted = polyline.isDotted;
final isDashed = polyline.pattern.segments != null;
final isDotted = polyline.pattern.spacingFactor != null;

paint = Paint()
..strokeWidth = strokeWidth
..strokeCap = polyline.strokeCap
Expand Down Expand Up @@ -200,12 +202,19 @@ class _PolylinePainter<R extends Object> extends CustomPainter {
final borderRadius = (borderPaint?.strokeWidth ?? 0) / 2;

if (isDotted) {
final spacing = strokeWidth * polyline.segmentSpacingFactor;
final spacing = strokeWidth * polyline.pattern.spacingFactor!;
if (borderPaint != null && filterPaint != null) {
_paintDottedLine(borderPath, offsets, borderRadius, spacing);
_paintDottedLine(filterPath, offsets, radius, spacing);
}
_paintDottedLine(path, offsets, radius, spacing);
} else if (isDashed) {
if (borderPaint != null && filterPaint != null) {
_paintDashedLine(borderPath, offsets, polyline.pattern.segments!);
_paintDashedLine(filterPath, offsets, polyline.pattern.segments!);
}
_paintDashedLine(path, offsets, polyline.pattern.segments!);
// TODO: Check the returned value and display the last point if relevant
} else {
if (borderPaint != null && filterPaint != null) {
_paintLine(borderPath, offsets);
Expand Down Expand Up @@ -240,6 +249,76 @@ class _PolylinePainter<R extends Object> extends CustomPainter {
path.addOval(Rect.fromCircle(center: offsets.last, radius: radius));
}

/// Returns true if the last point was a space.
///
/// We may need that info if we want to do something special when the last
/// point was not displayed, like putting artificially a dot at this location.
bool? _paintDashedLine(
ui.Path path,
List<Offset> offsets,
List<double> dashValues,
) {
if (offsets.length < 2) {
return null;
}
if (dashValues.isEmpty) {
return null;
}
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved
if (dashValues.length.isOdd) {
return null;
}

int dashValueIndex = 0;
int offsetIndex = 0;

double remaining = dashValues[dashValueIndex];
Offset offset0 = offsets[offsetIndex++];
Offset offset1 = offsets[offsetIndex++];
bool nextIndexPlease = false;

/// Returns the offset on segment [A,B] that matches the remaining distance.
Offset getDistanceOffset(final Offset offsetA, final Offset offsetB) {
final segmentDistance = (offsetA - offsetB).distance;
if (remaining >= segmentDistance) {
remaining -= segmentDistance;
nextIndexPlease = true;
return offsetB;
}
final fB = remaining / segmentDistance;
final fA = 1.0 - fB;
remaining = 0;
return Offset(
offsetA.dx * fA + offsetB.dx * fB,
offsetA.dy * fA + offsetB.dy * fB,
);
}

path.moveTo(offset0.dx, offset0.dy);
while (true) {
final Offset newOffset = getDistanceOffset(offset0, offset1);
if (dashValueIndex.isEven) {
path.lineTo(newOffset.dx, newOffset.dy);
} else {
// TODO optim: remove useless `moveTo`s, potentially many of them
path.moveTo(newOffset.dx, newOffset.dy);
}
if (nextIndexPlease) {
nextIndexPlease = false;
if (offsetIndex >= offsets.length) {
return dashValueIndex.isEven;
}
offset0 = offset1;
offset1 = offsets[offsetIndex++];
} else {
offset0 = newOffset;
}
if (remaining == 0) {
dashValueIndex++;
remaining = dashValues[dashValueIndex % dashValues.length];
}
}
}

void _paintLine(ui.Path path, List<Offset> offsets) {
if (offsets.isEmpty) {
return;
Expand Down
85 changes: 85 additions & 0 deletions lib/src/layer/polyline_layer/pattern.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
part of 'polyline_layer.dart';

/// Determines whether a [Polyline] should be solid, dotted, or dashed, and
/// the exact characteristics of each
@immutable
class PolylinePattern {
/// Solid/unbroken
const PolylinePattern.solid()
: spacingFactor = null,
segments = null;

/// Circular dots, spaced with [spacingFactor]
///
/// [spacingFactor] is
/// {@macro fm.polylinePattern.spacingFactor}
const PolylinePattern.dotted({double this.spacingFactor = 1.5})
: segments = null;

/// 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.
///
/// ---
///
/// [segments] is
/// {@macro fm.polylinePattern.segments}
const PolylinePattern.dashed({required List<double> this.segments})
: assert(
segments.length >= 2,
'`segments` must contain at least two items',
),
assert(
JaffaKetchup marked this conversation as resolved.
Show resolved Hide resolved
// ignore: use_is_even_rather_than_modulo
segments.length % 2 == 0,
'`segments` must have an even length',
),
spacingFactor = null;

/// {@template fm.polylinePattern.spacingFactor}
/// The multiplier used to calculate the spacing between segments in a
/// dotted/dashed polyline, with respect to [Polyline.strokeWidth]. A value of
/// 1.0 will result in spacing equal to the `strokeWidth`. Increasing the value
/// increases the spacing with the same scaling. It defaults to 1.5.
/// {@endtemplate}
final double? spacingFactor;

/// {@template fm.polylinePattern.segments}
/// A list of even length with a minimum of 2, in the form of
/// `[a₁, b₁, (a₂, b₂, ...)]`, where `a` should be the length of segments in
/// pixels, and `b` the length of the space after each segment in pixels. Both
/// values must be strictly positive.
///
/// If more than two items are specified, then each segments will
/// alternate/iterate through the values.
///
/// For example, `[50, 10]` will cause:
/// * a segment of length 50px
/// * followed by a space of 10px
/// * followed by a segment of length 50px
/// * followed by a space of 10px
/// * etc...
///
/// For example, `[50, 10, 10, 10]` will cause:
/// * a segment of length 50px
/// * followed by a space of 10px
/// * followed by a segment of length 10px
/// * followed by a space of 10px
/// * followed by a segment of length of 50px
/// * followed by a space of 10px
/// * etc...
/// {@endtemplate}
final List<double>? segments;

@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is PolylinePattern &&
spacingFactor == other.spacingFactor &&
((segments == null && other.segments == null) ||
listEquals(segments, other.segments)));

@override
int get hashCode => Object.hash(spacingFactor, segments);
}
33 changes: 16 additions & 17 deletions lib/src/layer/polyline_layer/polyline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ class Polyline<R extends Object> {
/// The width of the stroke
final double strokeWidth;

/// The multiplier used to calculate the spacing between segments in a dotted/
/// dashed polyline
/// Determines whether this should be solid, dotted, or dashed, and the exact
/// characteristics of each
///
/// A value of 1.0 will result in spacing equal to the `strokeWidth`.
/// Increasing the value increases the spacing with respect to `strokeWidth`.
///
/// Defaults to 1.5.
final double segmentSpacingFactor;
/// Defaults to being a solid/unbroken line ([PolylinePattern.solid]).
final PolylinePattern pattern;

/// The color of the line stroke.
final Color color;
Expand All @@ -33,9 +30,6 @@ class Polyline<R extends Object> {
/// The stops for the gradient colors.
final List<double>? colorsStop;

/// Set to true if the line stroke should be rendered as a dotted line.
final bool isDotted;

/// Styles to use for line endings.
final StrokeCap strokeCap;

Expand All @@ -57,18 +51,25 @@ class Polyline<R extends Object> {
Polyline({
required this.points,
this.strokeWidth = 1.0,
PolylinePattern pattern = const PolylinePattern.solid(),
this.color = const Color(0xFF00FF00),
this.borderStrokeWidth = 0.0,
this.borderColor = const Color(0xFFFFFF00),
this.gradientColors,
this.colorsStop,
this.isDotted = false,
this.strokeCap = StrokeCap.round,
this.strokeJoin = StrokeJoin.round,
this.useStrokeWidthInMeter = false,
this.hitValue,
this.segmentSpacingFactor = 1.5,
});
@Deprecated(
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved
'Prefer setting `pattern` to toggle dotting. '
'This parameter will be replaced by `pattern`, which supports further '
'customization & dashed lines through a single, less complex, external API. '
'Enabling this parameter will override `pattern` with `dotted(1.5)`. '
'This feature was deprecated after v7.',
)
bool isDotted = false,
}) : pattern = isDotted ? const PolylinePattern.dotted() : pattern;

@override
bool operator ==(Object other) =>
Expand All @@ -78,8 +79,7 @@ class Polyline<R extends Object> {
color == other.color &&
borderStrokeWidth == other.borderStrokeWidth &&
borderColor == other.borderColor &&
isDotted == other.isDotted &&
segmentSpacingFactor == other.segmentSpacingFactor &&
pattern == other.pattern &&
strokeCap == other.strokeCap &&
strokeJoin == other.strokeJoin &&
useStrokeWidthInMeter == other.useStrokeWidthInMeter &&
Expand All @@ -100,8 +100,7 @@ class Polyline<R extends Object> {
borderColor,
gradientColors,
colorsStop,
isDotted,
segmentSpacingFactor,
pattern,
strokeCap,
strokeJoin,
useStrokeWidthInMeter,
Expand Down
1 change: 1 addition & 0 deletions lib/src/layer/polyline_layer/polyline_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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';

Expand Down
Loading