Skip to content
Draft
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
218 changes: 208 additions & 10 deletions lib/src/layer/polyline_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,32 @@ class _PolylinePainter<R extends Object> extends CustomPainter
shift: shift,
);

late final double strokeWidth;
if (polyline.useStrokeWidthInMeter) {
strokeWidth = metersToScreenPixels(
projectedPolyline.polyline.points.first,
polyline.strokeWidth,
);
} else {
strokeWidth = polyline.strokeWidth;
final strokeWidth = polyline.useStrokeWidthInMeter
? metersToScreenPixels(
projectedPolyline.polyline.points.first,
polyline.strokeWidth,
)
: polyline.strokeWidth;

if (!areOffsetsVisible(
offsets,
strokeWidth + polyline.borderStrokeWidth,
)) {
return WorldWorkControl.invisible;
}

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

final hash = polyline.renderHashCode;
Expand Down Expand Up @@ -271,6 +285,190 @@ class _PolylinePainter<R extends Object> extends CustomPainter
drawPaths();
}

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

final colors = polyline._resolvedVertexColors;
final vertexCount = offsets.length;
if (vertexCount < 2) {
return;
}
assert(
projectedPolyline.sourceStartIndex + vertexCount <= colors.length,
'Projected points must retain their source color indices',
);

final hasBorder = polyline.borderStrokeWidth > 0.0;
final requiresLayerSaving = polyline._hasTransparentVertices;
final strokeBlendMode =
requiresLayerSaving ? BlendMode.src : BlendMode.srcOver;

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();
}
}

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

final strokePaint = Paint()
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke
..blendMode = strokeBlendMode;

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;
if (VisibleSegment.getVisibleSegment(
start,
end,
size,
strokeWidth + polyline.borderStrokeWidth,
) ==
null) {
continue;
}

final colorIndex = projectedPolyline.sourceStartIndex + i;

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

strokePaint.shader = null;

final vertexPaint = Paint()
..style = PaintingStyle.fill
..blendMode = strokeBlendMode;
final radius = strokeWidth / 2;

for (int i = 0; i < vertexCount; i++) {
final sourceIndex = projectedPolyline.sourceStartIndex + i;
if (sourceIndex == 0 || sourceIndex == colors.length - 1) continue;
if (!VisibleSegment.isVisible(offsets[i], size, strokeWidth)) continue;
vertexPaint.color = colors[sourceIndex];
canvas.drawCircle(offsets[i], radius, vertexPaint);
}

void drawRoundCap(int index) {
if (!VisibleSegment.isVisible(offsets[index], size, strokeWidth)) return;
vertexPaint.color = colors[projectedPolyline.sourceStartIndex + index];
canvas.drawCircle(offsets[index], radius, vertexPaint);
}

void drawSquareCap(int index, int step) {
final endpoint = offsets[index];
if (!VisibleSegment.isVisible(endpoint, size, strokeWidth)) return;

var neighborIndex = index + step;
while (neighborIndex >= 0 &&
neighborIndex < vertexCount &&
offsets[neighborIndex] == endpoint) {
neighborIndex += step;
}
if (neighborIndex < 0 || neighborIndex >= vertexCount) return;

final outward = endpoint - offsets[neighborIndex];
final distance = outward.distance;
if (distance == 0) return;

final direction = outward / distance;
final normal = Offset(-direction.dy, direction.dx) * radius;
final outer = endpoint + direction * radius;
final capPath = ui.Path()
..addPolygon(
[
endpoint + normal,
outer + normal,
outer - normal,
endpoint - normal,
],
true,
);

vertexPaint.color = colors[projectedPolyline.sourceStartIndex + index];
canvas.drawPath(capPath, vertexPaint);
}

switch (polyline.strokeCap) {
case StrokeCap.butt:
break;
case StrokeCap.round:
if (projectedPolyline.sourceStartIndex == 0) drawRoundCap(0);
if (projectedPolyline.sourceStartIndex + vertexCount == colors.length) {
drawRoundCap(vertexCount - 1);
}
break;
case StrokeCap.square:
if (projectedPolyline.sourceStartIndex == 0) drawSquareCap(0, 1);
if (projectedPolyline.sourceStartIndex + vertexCount == colors.length) {
drawSquareCap(vertexCount - 1, -1);
}
break;
}

if (requiresLayerSaving) {
canvas.restore();
}
}

ui.Gradient _paintGradient(Polyline polyline, List<Offset> offsets) =>
ui.Gradient.linear(offsets.first, offsets.last, polyline.gradientColors!,
_getColorsStop(polyline));
Expand Down
98 changes: 98 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,101 @@ class Polyline<R extends Object> with HitDetectableElement<R> {
@override
int get hashCode => _hashCode ??= Object.hashAll([...points, renderHashCode]);
}

/// A solid [Polyline] that paints a smooth gradient between vertex colors.
///
/// Consecutive segments use round joins. Line endings use [strokeCap].
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;
int? _multicolorHashCode;
List<Color>? _resolvedColors;

/// Create a multicolor polyline that interpolates between the supplied
/// [vertexColors].
// Super parameters cannot initialize `color`, which is derived from
// `vertexColors` and `defaultColor`.
// ignore: use_super_parameters
MulticolorPolyline({
required List<LatLng> points,
List<Color>? vertexColors,
this.defaultColor = const Color(0xFF00FF00),
double strokeWidth = 1.0,
double borderStrokeWidth = 0.0,
Color borderColor = const Color(0xFFFFFF00),
StrokeCap strokeCap = StrokeCap.round,
bool useStrokeWidthInMeter = false,
R? hitValue,
}) : vertexColors = _validateVertexColors(points, vertexColors),
super(
points: points,
strokeWidth: strokeWidth,
pattern: const StrokePattern.solid(),
color: (vertexColors != null && vertexColors.isNotEmpty)
? vertexColors.first
: defaultColor,
borderStrokeWidth: borderStrokeWidth,
borderColor: borderColor,
gradientColors: null,
colorsStop: null,
strokeCap: strokeCap,
strokeJoin: StrokeJoin.round,
useStrokeWidthInMeter: useStrokeWidthInMeter,
hitValue: hitValue,
);

bool get _hasTransparentVertices =>
_resolvedVertexColors.any((color) => color.a < 1);

List<Color> get _resolvedVertexColors => _resolvedColors ??=
vertexColors ?? List<Color>.filled(points.length, defaultColor);

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

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

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

List<Color>? _validateVertexColors(
List<LatLng> points,
List<Color>? vertexColors,
) {
if (points.length < 2) {
throw ArgumentError.value(
points,
'points',
'MulticolorPolyline requires at least two points',
);
}
if (vertexColors == null || vertexColors.isEmpty) return null;
if (points.length != vertexColors.length) {
throw ArgumentError.value(
vertexColors,
'vertexColors',
'length must match points length',
);
}
return List<Color>.unmodifiable(vertexColors);
}
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,7 +208,7 @@ 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
// Global gradients cannot be segmented without changing their shader.
if (polyline.gradientColors != null) {
yield projectedPolyline;
continue;
Expand Down Expand Up @@ -261,6 +267,7 @@ class _PolylineLayerState<R extends Object> extends State<PolylineLayer<R>>
yield _ProjectedPolyline._(
polyline: polyline,
points: projectedPolyline.points.sublist(start, i + 1),
sourceStartIndex: projectedPolyline.sourceStartIndex + start,
);

// Reset start.
Expand All @@ -276,8 +283,8 @@ class _PolylineLayerState<R extends Object> extends State<PolylineLayer<R>>
? projectedPolyline
: _ProjectedPolyline._(
polyline: polyline,
// Special case: the entire polyline is visible
points: projectedPolyline.points.sublist(start),
sourceStartIndex: projectedPolyline.sourceStartIndex + start,
);
}
}
Expand Down
Loading