From e9ae79d8f12f3200fd30a91bfdfbfbc54a3ec2f3 Mon Sep 17 00:00:00 2001 From: David Green Date: Sat, 21 Aug 2021 21:59:20 -0700 Subject: [PATCH] improve label overlap detection --- lib/src/context.dart | 5 +- lib/src/features/label_space.dart | 19 +++-- lib/src/features/symbol_line_renderer.dart | 82 ++++++++++++--------- lib/src/features/symbol_point_renderer.dart | 17 +++-- lib/src/features/text_renderer.dart | 15 ++-- lib/src/renderer.dart | 12 +-- 6 files changed, 90 insertions(+), 60 deletions(-) diff --git a/lib/src/context.dart b/lib/src/context.dart index 78aa871..246cc54 100644 --- a/lib/src/context.dart +++ b/lib/src/context.dart @@ -13,10 +13,11 @@ class Context { final VectorTile tile; final double zoomScaleFactor; final double zoom; + final Rect tileSpace; final Rect tileClip; late final LabelSpace labelSpace; Context(this.logger, this.canvas, this.featureRenderer, this.tile, - this.zoomScaleFactor, this.zoom, this.tileClip) - : labelSpace = LabelSpace(tileClip); + this.zoomScaleFactor, this.zoom, this.tileSpace, this.tileClip) + : labelSpace = LabelSpace(tileSpace); } diff --git a/lib/src/features/label_space.dart b/lib/src/features/label_space.dart index 754b2ba..442eab3 100644 --- a/lib/src/features/label_space.dart +++ b/lib/src/features/label_space.dart @@ -2,18 +2,21 @@ import 'dart:ui'; class LabelSpace { final Rect space; - final List occupied = []; + final List<_LabelRect> occupied = []; + final Set texts = Set(); LabelSpace(this.space); - bool canOccupy(Rect rect) => + bool canOccupy(String text, Rect rect) => + !texts.contains(text) && space.containsCompletely(rect) && - !occupied.any((existing) => existing.overlaps(rect)); + !occupied.any((existing) => existing.space.overlaps(rect)); - void occupy(Rect box) { + void occupy(String text, Rect box) { final boxWithMargin = Rect.fromLTRB(box.left - margin, box.top - margin, box.right + (2 * margin), box.bottom + (2 * margin)); - occupied.add(boxWithMargin); + occupied.add(_LabelRect(text, boxWithMargin)); + texts.add(text); } } @@ -23,3 +26,9 @@ extension _RectExtension on Rect { } final margin = 2.0; + +class _LabelRect { + final Rect space; + final String text; + _LabelRect(this.text, this.space); +} diff --git a/lib/src/features/symbol_line_renderer.dart b/lib/src/features/symbol_line_renderer.dart index e90c2a1..5491d5d 100644 --- a/lib/src/features/symbol_line_renderer.dart +++ b/lib/src/features/symbol_line_renderer.dart @@ -54,8 +54,9 @@ class SymbolLineRenderer extends FeatureRenderer { if (metrics.length > 0) { final abbreviated = TextAbbreviator().abbreviate(text); final renderer = TextRenderer(context, style, abbreviated); - final tangent = _findMiddleMetric(context, metrics, renderer); - if (tangent != null) { + final renderBox = _findMiddleMetric(context, metrics, renderer); + if (renderBox != null) { + final tangent = renderBox.tangent; final rotate = (tangent.angle >= 0.01 || tangent.angle <= -0.01); if (rotate) { context.canvas.save(); @@ -75,76 +76,81 @@ class SymbolLineRenderer extends FeatureRenderer { } } - Tangent? _findMiddleMetric( + _RenderBox? _findMiddleMetric( Context context, List metrics, TextRenderer renderer) { final midpoint = metrics.length ~/ 2; for (int x = 0; x <= (midpoint + 1); ++x) { int lower = midpoint - x; if (lower >= 0 && metrics[lower].length > _minPathMetricSize) { - final tangent = _occupyLabelSpace(context, renderer, metrics[lower]); - if (tangent != null) { - return tangent; + final renderBox = _occupyLabelSpace(context, renderer, metrics[lower]); + if (renderBox != null) { + return renderBox; } } int upper = midpoint + x; if (upper != lower && upper < metrics.length && metrics[upper].length > _minPathMetricSize) { - final tangent = _occupyLabelSpace(context, renderer, metrics[upper]); - if (tangent != null) { - return tangent; + final renderBox = _occupyLabelSpace(context, renderer, metrics[upper]); + if (renderBox != null) { + return renderBox; } } } return _occupyLabelSpace(context, renderer, metrics[midpoint]); } - Tangent? _occupyLabelSpace( + _RenderBox? _occupyLabelSpace( Context context, TextRenderer renderer, PathMetric metric) { Tangent? tangent = metric.getTangentForOffset(metric.length / 2); + _RenderBox? renderBox; if (tangent != null) { - tangent = _occupyLabelSpaceAtTangent(context, renderer, tangent); - if (tangent == null) { + renderBox = _occupyLabelSpaceAtTangent(context, renderer, tangent); + if (renderBox == null) { tangent = metric.getTangentForOffset(metric.length / 4); if (tangent != null) { - tangent = _occupyLabelSpaceAtTangent(context, renderer, tangent); - if (tangent == null) { + renderBox = _occupyLabelSpaceAtTangent(context, renderer, tangent); + if (renderBox == null) { tangent = metric.getTangentForOffset(metric.length * 3 / 4); if (tangent != null) { - tangent = _occupyLabelSpaceAtTangent(context, renderer, tangent); + renderBox = + _occupyLabelSpaceAtTangent(context, renderer, tangent); } } } } } - return tangent; + return renderBox; } - Tangent? _occupyLabelSpaceAtTangent( + _RenderBox? _occupyLabelSpaceAtTangent( Context context, TextRenderer renderer, Tangent tangent) { - Rect? box = renderer.labelBox(tangent.position); + Rect? box = renderer.labelBox(tangent.position, translated: false); if (box != null) { - if (tangent.angle != 0) { - if (_isApproximatelyVertical(tangent.angle)) { - box = Rect.fromLTWH(box.left, box.top, box.height, box.width); - } else { - final size = max(box.width, box.height); - box = Rect.fromLTWH(box.left, box.top, size, size); - } - if (context.labelSpace.canOccupy(box)) { - context.labelSpace.occupy(box); - return tangent; - } + final angle = _rightSideUpAngle(tangent.angle); + final hWidth = (box.height * cos(angle + _ninetyDegrees)).abs(); + final width = hWidth + (box.width * cos(angle)).abs(); + final wHeight = (box.width * sin(angle)).abs(); + final height = (box.height * sin(angle + _ninetyDegrees)).abs() + wHeight; + var xOffset = 0.0; + var yOffset = 0.0; + final translation = renderer.translation; + if (translation != null) { + xOffset = translation.dx * cos(angle) - + (translation.dy * cos(angle + _ninetyDegrees)).abs(); + yOffset = (translation.dy * sin(angle + _ninetyDegrees)) - + (translation.dx * sin(angle)).abs(); + } + Rect textSpace = + Rect.fromLTWH(box.left + xOffset, box.top + yOffset, width, height); + if (context.labelSpace.canOccupy(renderer.text, textSpace)) { + context.labelSpace.occupy(renderer.text, textSpace); + return _RenderBox(textSpace, tangent); } } return null; } - bool _isApproximatelyVertical(double radians) { - return (radians >= 1.5 && radians <= 1.65) || - (radians >= 4.6 && radians <= 4.8); - } - double _rightSideUpAngle(double radians) { if (radians > _rotationShiftUpper || radians < _rotationShiftLower) { return radians + _rotationShift; @@ -153,6 +159,13 @@ class SymbolLineRenderer extends FeatureRenderer { } } +class _RenderBox { + final Rect box; + final Tangent tangent; + + _RenderBox(this.box, this.tangent); +} + final _minPathMetricSize = 100.0; final _degToRad = pi / 180.0; @@ -160,3 +173,4 @@ final _rotationOfershot = 3; final _rotationShiftUpper = (90 + _rotationOfershot) * _degToRad; final _rotationShiftLower = -(90 + _rotationOfershot) * _degToRad; final _rotationShift = (180 * _degToRad); +final _ninetyDegrees = 90 * _degToRad; diff --git a/lib/src/features/symbol_point_renderer.dart b/lib/src/features/symbol_point_renderer.dart index 9375d55..aca1ded 100644 --- a/lib/src/features/symbol_point_renderer.dart +++ b/lib/src/features/symbol_point_renderer.dart @@ -1,18 +1,18 @@ +import 'dart:ui'; + import 'package:flutter/rendering.dart'; import 'package:vector_tile/vector_tile.dart'; import 'package:vector_tile/vector_tile_feature.dart'; -import 'text_abbreviator.dart'; -import 'text_renderer.dart'; - -import 'dart:ui'; import '../../vector_tile_renderer.dart'; +import '../constants.dart'; import '../context.dart'; import '../logger.dart'; -import '../constants.dart'; import '../themes/style.dart'; import 'feature_geometry.dart'; import 'feature_renderer.dart'; +import 'text_abbreviator.dart'; +import 'text_renderer.dart'; class SymbolPointRenderer extends FeatureRenderer { final Logger logger; @@ -42,9 +42,10 @@ class SymbolPointRenderer extends FeatureRenderer { } final x = (point[0] / layer.extent) * tileSize; final y = (point[1] / layer.extent) * tileSize; - final box = textRenderer.labelBox(Offset(x, y)); - if (box != null && context.labelSpace.canOccupy(box)) { - context.labelSpace.occupy(box); + final box = textRenderer.labelBox(Offset(x, y), translated: true); + if (box != null && + context.labelSpace.canOccupy(textRenderer.text, box)) { + context.labelSpace.occupy(textRenderer.text, box); textRenderer.render(Offset(x, y)); } }); diff --git a/lib/src/features/text_renderer.dart b/lib/src/features/text_renderer.dart index 3e1040f..ddd218a 100644 --- a/lib/src/features/text_renderer.dart +++ b/lib/src/features/text_renderer.dart @@ -8,22 +8,26 @@ import '../context.dart'; class TextRenderer { final Context context; final Style style; + final String text; late final TextPainter? _painter; late final Offset? _translation; - TextRenderer(this.context, this.style, String text) { + TextRenderer(this.context, this.style, this.text) { _painter = _createTextPainter(context, style, text); _translation = _layout(); } - Rect? labelBox(Offset offset) { + double get textHeight => _painter!.height; + Offset? get translation => _translation; + + Rect? labelBox(Offset offset, {required bool translated}) { if (_painter == null) { return null; } double x = offset.dx; double y = offset.dy; - if (_translation != null) { - x += _translation!.dx; - y += _translation!.dy; + if (_translation != null && translated) { + x += (_translation!.dx); + y += (_translation!.dy); } return Rect.fromLTWH(x, y, _painter!.width, _painter!.height); } @@ -33,6 +37,7 @@ class TextRenderer { if (painter == null) { return; } + if (_translation != null) { context.canvas.save(); context.canvas.translate(_translation!.dx, _translation!.dy); diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart index d999681..925c7a1 100644 --- a/lib/src/renderer.dart +++ b/lib/src/renderer.dart @@ -27,13 +27,13 @@ class Renderer { /// via `minzoom` and `maxzoom`. Value must be >= 0 and <= 24 void render(Canvas canvas, VectorTile tile, {Rect? clip, required double zoomScaleFactor, required double zoom}) { + final tileSpace = + Rect.fromLTWH(0, 0, tileSize.toDouble(), tileSize.toDouble()); canvas.save(); - canvas.clipRect( - Rect.fromLTRB(0, 0, tileSize.toDouble(), tileSize.toDouble())); - final tileClip = - clip ?? Rect.fromLTWH(0, 0, tileSize.toDouble(), tileSize.toDouble()); - final context = Context( - logger, canvas, featureRenderer, tile, zoomScaleFactor, zoom, tileClip); + canvas.clipRect(tileSpace); + final tileClip = clip ?? tileSpace; + final context = Context(logger, canvas, featureRenderer, tile, + zoomScaleFactor, zoom, tileSpace, tileClip); final effectiveTheme = theme.atZoom(zoom); effectiveTheme.layers.forEach((themeLayer) { logger.log(() => 'rendering theme layer ${themeLayer.id}');