Skip to content

Commit

Permalink
support symbols rotated to align with viewport
Browse files Browse the repository at this point in the history
add initial support for rotated symbols (text and icons)
that align with viewport.
  • Loading branch information
greensopinion committed Sep 23, 2023
1 parent 1d9f5f5 commit 17c50b5
Show file tree
Hide file tree
Showing 18 changed files with 182 additions and 77 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,7 @@
## 5.0.1

* support labels and icon rotation aligned with viewport

## 4.0.0

* add support for sprites
Expand Down
3 changes: 2 additions & 1 deletion example/lib/tile_painter.dart
Expand Up @@ -27,7 +27,8 @@ class TilePainter extends CustomPainter {
Renderer(theme: theme).render(canvas, TileSource(tileset: tileset),
clip: Rect.fromLTWH(0, 0, size.width, size.height),
zoomScaleFactor: pow(2, options.scale).toDouble(),
zoom: options.zoom);
zoom: options.zoom,
rotation: 0.0);
}
canvas.restore();
}
Expand Down
11 changes: 6 additions & 5 deletions example/macos/Runner.xcodeproj/project.pbxproj
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {

/* Begin PBXAggregateTarget section */
Expand Down Expand Up @@ -182,7 +182,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
33CC10EC2044A3C60003C045 = {
Expand Down Expand Up @@ -235,6 +235,7 @@
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down Expand Up @@ -344,7 +345,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down Expand Up @@ -423,7 +424,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
Expand Down Expand Up @@ -470,7 +471,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
40 changes: 20 additions & 20 deletions example/pubspec.lock
Expand Up @@ -37,10 +37,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev"
source: hosted
version: "1.17.1"
version: "1.17.2"
cupertino_icons:
dependency: "direct main"
description:
Expand Down Expand Up @@ -75,30 +75,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
matcher:
dependency: transitive
description:
name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev"
source: hosted
version: "0.12.15"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.5.0"
meta:
dependency: transitive
description:
Expand Down Expand Up @@ -132,10 +124,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.10.0"
stack_trace:
dependency: transitive
description:
Expand Down Expand Up @@ -172,10 +164,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "0.6.0"
vector_math:
dependency: transitive
description:
Expand All @@ -198,7 +190,15 @@ packages:
path: ".."
relative: true
source: path
version: "4.0.0"
version: "5.0.0"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
sdks:
dart: ">=3.0.0-0 <4.0.0"
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=2.14.0"
4 changes: 4 additions & 0 deletions lib/src/context.dart
Expand Up @@ -17,6 +17,9 @@ class Context {
final TileSource tileSource;
final double zoomScaleFactor;
final double zoom;

/// rotation in radians
final double rotation;
final Rect tileSpace;
final Rect tileClip;
final LabelSpace labelSpace;
Expand All @@ -32,6 +35,7 @@ class Context {
required this.tileSource,
required this.zoomScaleFactor,
required this.zoom,
required this.rotation,
required this.tileSpace,
required this.tileClip,
required this.optimizations,
Expand Down
32 changes: 28 additions & 4 deletions lib/src/features/icon_renderer.dart
Expand Up @@ -15,17 +15,20 @@ class IconRenderer extends SymbolIcon {
final Image atlas;
final double size;
final LayoutAnchor anchor;
final RotationAlignment rotationAlignment;
final double? rotate;

IconRenderer(this.context,
{required this.sprite,
required this.atlas,
required this.size,
required this.anchor,
required this.rotationAlignment,
required this.rotate});

@override
RenderedIcon? render(Offset offset, {required Size contentSize}) {
RenderedIcon? render(Offset offset,
{required Size contentSize, required bool withRotation}) {
final paint = Paint()..isAntiAlias = true;

double scale = sprite.pixelRatio == 1 ? 1 : (1 / (sprite.pixelRatio));
Expand All @@ -41,15 +44,33 @@ class IconRenderer extends SymbolIcon {
.firstOrNull
?.translate(renderedArea.left, renderedArea.top) ??
renderedArea;
var rotation =
withRotation && rotationAlignment == RotationAlignment.viewport
? context.rotation
: 0.0;
if (rotate != null) {
rotation += (rotate! * pi / 180.0);
}
final anchorOffset = anchor.offset(renderedArea.size);
if (rotation != 0.0) {
context.canvas.save();
final rotationOffset = Offset(
offset.dx + anchorOffset.dx + (renderedArea.width / 2.0),
offset.dy + anchorOffset.dy + (renderedArea.height / 2.0));
context.canvas.translate(rotationOffset.dx, rotationOffset.dy);
context.canvas.rotate(-rotation);
context.canvas.translate(-rotationOffset.dx, -rotationOffset.dy);
}
context.canvas.drawAtlas(
atlas,
segments
.map((e) => RSTransform.fromComponents(
rotation: rotate == null ? 0 : (rotate! * pi / 180.0),
rotation: 0.0,
scale: e.scale,
anchorX: rotate == null ? 0 : offset.dx + anchorOffset.dx,
anchorY: rotate == null ? 0 : offset.dy + anchorOffset.dy,
anchorX:
0, // rotation == 0.0 ? 0 : offset.dx + anchorOffset.dx,
anchorY:
0, //rotation == 0.0 ? 0 : offset.dy + anchorOffset.dy,
translateX: offset.dx + anchorOffset.dx,
translateY: offset.dy + anchorOffset.dy))
.toList(),
Expand All @@ -58,6 +79,9 @@ class IconRenderer extends SymbolIcon {
null,
null,
paint);
if (rotation != 0.0) {
context.canvas.restore();
}
return RenderedIcon(
overlapsText: sprite.content != null,
area: renderedArea,
Expand Down
3 changes: 2 additions & 1 deletion lib/src/features/symbol_icon.dart
@@ -1,7 +1,8 @@
import 'dart:ui';

abstract class SymbolIcon {
RenderedIcon? render(Offset offset, {required Size contentSize});
RenderedIcon? render(Offset offset,
{required Size contentSize, required bool withRotation});
}

class RenderedIcon {
Expand Down
10 changes: 8 additions & 2 deletions lib/src/features/symbol_layout_extension.dart
@@ -1,11 +1,14 @@
import 'symbol_rotation.dart';

import '../context.dart';
import '../themes/expression/expression.dart';
import '../themes/style.dart';
import 'icon_renderer.dart';
import 'symbol_icon.dart';

extension SymbolLayoutExtension on SymbolLayout {
SymbolIcon? getIcon(Context context, EvaluationContext evaluationContext) {
SymbolIcon? getIcon(Context context, EvaluationContext evaluationContext,
{required LayoutPlacement layoutPlacement}) {
final iconName = icon?.icon.evaluate(evaluationContext);
SymbolIcon? iconRenderer;
if (iconName != null) {
Expand All @@ -16,12 +19,15 @@ extension SymbolLayoutExtension on SymbolLayout {
final anchor =
icon?.anchor.evaluate(evaluationContext) ?? LayoutAnchor.DEFAULT;
final rotate = icon?.rotate?.evaluate(evaluationContext);
final rotationAlignment = iconRotationAlignment(evaluationContext,
layoutPlacement: layoutPlacement);
iconRenderer = IconRenderer(context,
sprite: sprite,
atlas: atlas,
size: size,
anchor: anchor,
rotate: rotate);
rotate: rotate,
rotationAlignment: rotationAlignment);
} else {
context.logger.warn(() => 'missing sprite: $icon');
}
Expand Down
48 changes: 23 additions & 25 deletions lib/src/features/symbol_line_renderer.dart
Expand Up @@ -8,6 +8,7 @@ import '../themes/style.dart';
import 'extensions.dart';
import 'feature_renderer.dart';
import 'symbol_layout_extension.dart';
import 'symbol_rotation.dart';
import 'text_abbreviator.dart';
import 'text_renderer.dart';

Expand Down Expand Up @@ -48,12 +49,18 @@ class SymbolLineRenderer extends FeatureRenderer {
hasImage: context.hasImage);

final text = symbolLayout.text?.text.evaluate(evaluationContext);
final icon = symbolLayout.getIcon(context, evaluationContext);
final icon = symbolLayout.getIcon(context, evaluationContext,
layoutPlacement: LayoutPlacement.line);
if (text == null) {
logger.warn(() => 'line with no text');
return;
}
bool rotate = _shouldRotate(symbolLayout, evaluationContext);

final rotationAlignment = symbolLayout.textRotationAlignment(
evaluationContext,
layoutPlacement: LayoutPlacement.line);
bool rotateWithLine =
_shouldRotateWithLine(rotationAlignment, evaluationContext);
final textAbbreviation = TextAbbreviator().abbreviate(text);
if (!context.labelSpace.canAccept(textAbbreviation)) {
return;
Expand All @@ -66,24 +73,28 @@ class SymbolLineRenderer extends FeatureRenderer {

final metrics = path.pathMetrics;
final renderBox =
_findMiddleMetric(context, metrics, textApproximation, rotate);
_findMiddleMetric(context, metrics, textApproximation, rotateWithLine);
if (renderBox == null || !textApproximation.renderer.canPaint) {
return;
}

context.tileSpaceMapper.drawInPixelSpace(() {
final tangentPosition = renderBox.tangent.position;
final tangentAngle = renderBox.tangent.angle;
final rotate = (tangentAngle >= 0.01 || tangentAngle <= -0.01);
final saveState = rotate;
final rotateWithLine = (tangentAngle >= 0.01 || tangentAngle <= -0.01);
final saveState =
rotateWithLine || rotationAlignment == RotationAlignment.viewport;
if (saveState) {
final rotation = rotationAlignment == RotationAlignment.viewport
? context.rotation
: _rightSideUpAngle(tangentAngle);
context.canvas.save();
context.canvas.translate(tangentPosition.dx, tangentPosition.dy);
context.canvas.rotate(-_rightSideUpAngle(tangentAngle));
context.canvas.rotate(-rotation);
context.canvas.translate(-tangentPosition.dx, -tangentPosition.dy);
}
final occupied = icon?.render(tangentPosition,
contentSize: textApproximation.renderer.size);
contentSize: textApproximation.renderer.size, withRotation: false);
var textPosition = tangentPosition;
if (occupied != null &&
occupied.overlapsText &&
Expand All @@ -100,25 +111,12 @@ class SymbolLineRenderer extends FeatureRenderer {
});
}

bool _shouldRotate(
SymbolLayout symbolLayout, EvaluationContext evaluationContext) {
var textRotationAlignment =
symbolLayout.text?.rotationAlignment?.evaluate(evaluationContext) ??
RotationAlignment.auto;
var rotate = true;
if (textRotationAlignment == RotationAlignment.auto) {
final placement = symbolLayout.placement.evaluate(evaluationContext) ??
LayoutPlacement.point;
if (placement == LayoutPlacement.point) {
textRotationAlignment = RotationAlignment.viewport;
} else {
textRotationAlignment = RotationAlignment.map;
}
}
if (textRotationAlignment == RotationAlignment.viewport) {
rotate = false;
bool _shouldRotateWithLine(
RotationAlignment alignment, EvaluationContext evaluationContext) {
if (alignment == RotationAlignment.viewport) {
return false;
}
return rotate;
return true;
}

_RenderBox? _findMiddleMetric(Context context, List<PathMetric> metrics,
Expand Down

0 comments on commit 17c50b5

Please sign in to comment.