Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions lib/src/layer/polyline_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ class _PolylinePainter<R extends Object> extends CustomPainter
);
if (!areOffsetsVisible(offsets)) return WorldWorkControl.invisible;

if (polyline is MulticolorPolyline<R>) {
drawPaths();
lastHash = null;
needsLayerSaving = false;
_drawMulticolorPolyline(
canvas: canvas,
size: size,
projectedPolyline: projectedPolyline,
offsets: offsets,
);
return WorldWorkControl.visible;
}

final hash = polyline.renderHashCode;
if (needsLayerSaving || (lastHash != null && lastHash != hash)) {
drawPaths();
Expand Down Expand Up @@ -265,6 +278,102 @@ class _PolylinePainter<R extends Object> extends CustomPainter
drawPaths();
}

void _drawMulticolorPolyline({
required Canvas canvas,
required Size size,
required _ProjectedPolyline<R> projectedPolyline,
required List<Offset> offsets,
}) {
final polyline = projectedPolyline.polyline as MulticolorPolyline<R>;

final colors = polyline.resolvedVertexColors;
final vertexCount = math.min(offsets.length, colors.length);
if (vertexCount < 2) {
return;
}

final strokeWidth = polyline.useStrokeWidthInMeter
? metersToScreenPixels(
projectedPolyline.polyline.points.first,
polyline.strokeWidth,
)
: polyline.strokeWidth;

final hasBorder = polyline.borderStrokeWidth > 0.0;
final requiresLayerSaving = polyline.hasTransparentVertices;

if (hasBorder) {
final borderPath = ui.Path();
final filterPath = ui.Path();
final paths = <ui.Path>[borderPath];
if (requiresLayerSaving) {
paths.add(filterPath);
}

final SolidPixelHiker borderHiker = SolidPixelHiker(
offsets: offsets,
closePath: false,
canvasSize: size,
strokeWidth: strokeWidth + polyline.borderStrokeWidth,
);
borderHiker.addAllVisibleSegments(paths);

final borderPaint = Paint()
..color = polyline.borderColor
..strokeWidth = strokeWidth + polyline.borderStrokeWidth
..strokeCap = polyline.strokeCap
..strokeJoin = polyline.strokeJoin
..style = PaintingStyle.stroke
..blendMode = BlendMode.srcOver;

final filterPaint = Paint()
..color = polyline.borderColor.withAlpha(255)
..strokeWidth = strokeWidth
..strokeCap = polyline.strokeCap
..strokeJoin = polyline.strokeJoin
..style = PaintingStyle.stroke
..blendMode = BlendMode.dstOut;

if (requiresLayerSaving) {
canvas.saveLayer(viewportRect, Paint());
}

canvas.drawPath(borderPath, borderPaint);

if (requiresLayerSaving) {
canvas.drawPath(filterPath, filterPaint);
canvas.restore();
}
}

final strokePaint = Paint()
..strokeWidth = strokeWidth
..strokeCap = polyline.strokeCap
..strokeJoin = polyline.strokeJoin
..style = PaintingStyle.stroke
..blendMode = BlendMode.srcOver;

final segmentPath = ui.Path();

for (int i = 0; i < vertexCount - 1; i++) {
final start = offsets[i];
final end = offsets[i + 1];
if (start == end) continue;

strokePaint.shader = ui.Gradient.linear(
start,
end,
[colors[i], colors[i + 1]],
);
segmentPath.moveTo(start.dx, start.dy);
segmentPath.lineTo(end.dx, end.dy);
canvas.drawPath(segmentPath, strokePaint);
segmentPath.reset();
}

strokePaint.shader = null;
}

ui.Gradient _paintGradient(Polyline polyline, List<Offset> offsets) =>
ui.Gradient.linear(offsets.first, offsets.last, polyline.gradientColors!,
_getColorsStop(polyline));
Expand Down
94 changes: 94 additions & 0 deletions lib/src/layer/polyline_layer/polyline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,97 @@ class Polyline<R extends Object> with HitDetectableElement<R> {
@override
int get hashCode => _hashCode ??= Object.hashAll([...points, renderHashCode]);
}

/// A [Polyline] variant that can display a different color at each vertex and
/// paints a smooth gradient between consecutive vertices.
class MulticolorPolyline<R extends Object> extends Polyline<R> {
/// The color applied at each vertex in [points].
///
/// When `null` or empty, [defaultColor] is used instead.
final List<Color>? vertexColors;

/// The fallback color used when no [vertexColors] are provided.
final Color defaultColor;

int? _multicolorRenderHashCode;
List<Color>? _resolvedColors;

/// Create a multicolor polyline that interpolates between the supplied
/// [vertexColors].
MulticolorPolyline({
required List<LatLng> points,
List<Color>? vertexColors,
this.defaultColor = const Color(0xFF00FF00),
double strokeWidth = 1.0,
StrokePattern pattern = const StrokePattern.solid(),
double borderStrokeWidth = 0.0,
Color borderColor = const Color(0xFFFFFF00),
StrokeCap strokeCap = StrokeCap.round,
StrokeJoin strokeJoin = StrokeJoin.round,
bool useStrokeWidthInMeter = false,
R? hitValue,
}) : assert(
vertexColors == null ||
vertexColors.isEmpty ||
points.length == vertexColors.length,
'vertexColors length must match points length',
),
assert(points.length >= 2,
'MulticolorPolyline requires at least two points'),
assert(
pattern == const StrokePattern.solid(),
'MulticolorPolyline currently supports only solid stroke patterns.',
),
vertexColors = vertexColors != null && vertexColors.isNotEmpty
? vertexColors
: null,
super(
points: points,
strokeWidth: strokeWidth,
pattern: pattern,
color: (vertexColors != null && vertexColors.isNotEmpty)
? vertexColors.first
: defaultColor,
borderStrokeWidth: borderStrokeWidth,
borderColor: borderColor,
gradientColors: null,
colorsStop: null,
strokeCap: strokeCap,
strokeJoin: strokeJoin,
useStrokeWidthInMeter: useStrokeWidthInMeter,
hitValue: hitValue,
);

/// Returns `true` when any effective vertex color is translucent.
bool get hasTransparentVertices =>
resolvedVertexColors.any((color) => color.alpha < 0xFF);

/// Returns `true` when at least two distinct vertex colors were provided.
bool get hasGradientStops =>
vertexColors != null && vertexColors!.length >= 2;

/// Returns the colors used for painting, falling back to [defaultColor] when
/// custom [vertexColors] are not provided.
List<Color> get resolvedVertexColors => _resolvedColors ??= vertexColors ??
List<Color>.filled(points.length, defaultColor, growable: false);

@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is MulticolorPolyline<R> &&
super == other &&
defaultColor == other.defaultColor &&
_listEqualsNullable(vertexColors, other.vertexColors));

@override
int get renderHashCode => _multicolorRenderHashCode ??= Object.hash(
super.renderHashCode,
defaultColor,
vertexColors == null ? null : Object.hashAll(vertexColors!));
}

bool _listEqualsNullable<T>(List<T>? a, List<T>? b) {
if (identical(a, b)) return true;
if (a == null || b == null) return a == null && b == null;
return listEquals(a, b);
}
29 changes: 18 additions & 11 deletions lib/src/layer/polyline_layer/polyline_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,21 @@ class _PolylineLayerState<R extends Object> extends State<PolylineLayer<R>>
_ProjectedPolyline<R> simplifyProjectedElement({
required _ProjectedPolyline<R> projectedElement,
required double tolerance,
}) =>
_ProjectedPolyline._(
polyline: projectedElement.polyline,
points: simplifyPoints(
points: projectedElement.points,
tolerance: tolerance,
highQuality: true,
),
);
}) {
final polyline = projectedElement.polyline;
if (polyline is MulticolorPolyline<R>) {
return projectedElement;
}

return _ProjectedPolyline._(
polyline: polyline,
points: simplifyPoints(
points: projectedElement.points,
tolerance: tolerance,
highQuality: true,
),
);
}

@override
List<Polyline<R>> get elements => widget.polylines;
Expand Down Expand Up @@ -202,8 +208,9 @@ class _PolylineLayerState<R extends Object> extends State<PolylineLayer<R>>
// First check, bullet-proof, focusing on latitudes.
if (!isOverlappingLatitude()) continue;

// Gradient polylines cannot be easily segmented
if (polyline.gradientColors != null) {
// Gradient or multicolor polylines cannot be easily segmented
if (polyline.gradientColors != null ||
polyline is MulticolorPolyline<R>) {
yield projectedPolyline;
continue;
}
Expand Down
49 changes: 49 additions & 0 deletions test/layer/polyline_layer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,53 @@ void main() {
of: find.byType(PolylineLayer), matching: find.byType(CustomPaint)),
findsOneWidget);
});

testWidgets('multicolor polyline renders without errors', (tester) async {
final polylines = <Polyline>[
MulticolorPolyline(
points: const [
LatLng(50.5, -0.09),
LatLng(51.3498, -6.2603),
LatLng(53.8566, 2.3522),
],
vertexColors: const [
Colors.red,
Colors.orange,
Colors.blue,
],
strokeWidth: 6,
),
];

await tester.pumpWidget(TestApp(polylines: polylines));

expect(find.byType(FlutterMap), findsOneWidget);
expect(find.byType(PolylineLayer), findsOneWidget);
expect(tester.takeException(), isNull);
});

testWidgets('multicolor polyline falls back to defaultColor', (tester) async {
final polylines = <Polyline>[
MulticolorPolyline(
points: const [
LatLng(52.5, -0.09),
LatLng(53.3498, -6.2603),
LatLng(55.8566, 2.3522),
],
defaultColor: Colors.purple,
strokeWidth: 5,
),
];

await tester.pumpWidget(TestApp(polylines: polylines));

expect(find.byType(FlutterMap), findsOneWidget);
expect(find.byType(PolylineLayer), findsOneWidget);
expect(tester.takeException(), isNull);

final polyline = polylines.first as MulticolorPolyline;
expect(polyline.vertexColors, isNull);
expect(polyline.color, equals(Colors.purple));
expect(polyline.resolvedVertexColors, everyElement(equals(Colors.purple)));
});
}