Skip to content

Commit

Permalink
improve label overlap detection
Browse files Browse the repository at this point in the history
  • Loading branch information
greensopinion committed Aug 22, 2021
1 parent ddc90c4 commit e9ae79d
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 60 deletions.
5 changes: 3 additions & 2 deletions lib/src/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
19 changes: 14 additions & 5 deletions lib/src/features/label_space.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import 'dart:ui';

class LabelSpace {
final Rect space;
final List<Rect> occupied = [];
final List<_LabelRect> occupied = [];
final Set<String> 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);
}
}

Expand All @@ -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);
}
82 changes: 48 additions & 34 deletions lib/src/features/symbol_line_renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -75,76 +76,81 @@ class SymbolLineRenderer extends FeatureRenderer {
}
}

Tangent? _findMiddleMetric(
_RenderBox? _findMiddleMetric(
Context context, List<PathMetric> 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;
Expand All @@ -153,10 +159,18 @@ 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;
final _rotationOfershot = 3;
final _rotationShiftUpper = (90 + _rotationOfershot) * _degToRad;
final _rotationShiftLower = -(90 + _rotationOfershot) * _degToRad;
final _rotationShift = (180 * _degToRad);
final _ninetyDegrees = 90 * _degToRad;
17 changes: 9 additions & 8 deletions lib/src/features/symbol_point_renderer.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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));
}
});
Expand Down
15 changes: 10 additions & 5 deletions lib/src/features/text_renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -33,6 +37,7 @@ class TextRenderer {
if (painter == null) {
return;
}

if (_translation != null) {
context.canvas.save();
context.canvas.translate(_translation!.dx, _translation!.dy);
Expand Down
12 changes: 6 additions & 6 deletions lib/src/renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}');
Expand Down

0 comments on commit e9ae79d

Please sign in to comment.