From 99ab770c6d4d4cc356b298f3f5a4bb54d7584ea4 Mon Sep 17 00:00:00 2001 From: Krystian Kostecki Date: Wed, 22 Apr 2020 19:23:01 +0200 Subject: [PATCH] HARP-9898: Solver screen space text offsets for alternative text placements. This is follow-up commit that provides support for screen space defined (not icon relative) text offsets defined for point labels: - xOffset, - yOffset attributes in theme (style) definition. Provided solution ensures that the icon-text relation (manhattan distance) is preserved regardless of placement used. So in simplest case offset is mirrored (left-right alternative placement), but it may be also projected on Y axis when alternative placement changes text position vertically. Signed-off-by: Krystian Kostecki --- @here/harp-mapview/lib/text/Placement.ts | 94 ++++++++++++++----- .../lib/text/TextElementsRenderer.ts | 1 + @here/harp-mapview/test/PlacementTest.ts | 19 ++++ 3 files changed, 92 insertions(+), 22 deletions(-) diff --git a/@here/harp-mapview/lib/text/Placement.ts b/@here/harp-mapview/lib/text/Placement.ts index 3c6444ee83..b32e112ce8 100644 --- a/@here/harp-mapview/lib/text/Placement.ts +++ b/@here/harp-mapview/lib/text/Placement.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Env } from "@here/harp-datasource-protocol"; +import { Env, getPropertyValue } from "@here/harp-datasource-protocol"; import { ProjectionType } from "@here/harp-geoutils"; import { HorizontalAlignment, @@ -219,16 +219,16 @@ export function checkReadyForPlacement( * Computes the offset for a point text accordingly to text alignment (and icon, if any). * @param textElement The text element of which the offset will computed. It must be a point * label with [[layoutStyle]] and [[bounds]] already computed. - * @param hAlign Text horizontal alignment. - * @param vAlign The vertical alignment. + * @param placement The relative anchor placement (may be different then original alignment). * @param scale The scaling factor (due to distance, etc.). + * @param env The [[Env]] used to evaluate technique attributes. * @param offset The offset result. */ function computePointTextOffset( textElement: TextElement, - hAlign: HorizontalAlignment, - vAlign: VerticalAlignment, + placement: AnchorPlacement, scale: number, + env: Env, offset: THREE.Vector2 = new THREE.Vector2() ): THREE.Vector2 { assert(textElement.type === TextElementType.PoiLabel); @@ -236,17 +236,14 @@ function computePointTextOffset( assert(textElement.bounds !== undefined); offset.x = textElement.xOffset; + offset.y = textElement.yOffset; - switch (vAlign) { - case VerticalAlignment.Below: - offset.y = textElement.yOffset; - break; + switch (placement.v) { case VerticalAlignment.Above: - offset.y = textElement.yOffset - textElement.bounds!.min.y; + offset.y -= textElement.bounds!.min.y; break; - default: - offset.y = - textElement.yOffset - 0.5 * (textElement.bounds!.max.y + textElement.bounds!.min.y); + case VerticalAlignment.Center: + offset.y -= 0.5 * (textElement.bounds!.max.y + textElement.bounds!.min.y); break; } @@ -254,8 +251,55 @@ function computePointTextOffset( assert(textElement.poiInfo.computedWidth !== undefined); assert(textElement.poiInfo.computedHeight !== undefined); - offset.x += textElement.poiInfo.computedWidth! * (0.5 + hAlign); - offset.y += textElement.poiInfo.computedHeight! * (0.5 + vAlign); + // Apply offset moving text out of the icon + offset.x += textElement.poiInfo.computedWidth! * (0.5 + placement.h); + offset.y += textElement.poiInfo.computedHeight! * (0.5 + placement.v); + + // Reverse, mirror or project offsets on different axis depending on the placement + // required only for alternative placements. + const hAlign = textElement.layoutStyle!.horizontalAlignment; + const vAlign = textElement.layoutStyle!.verticalAlignment; + if (hAlign !== placement.h || vAlign !== placement.v) { + // Read icon offset used. + const technique = textElement.poiInfo.technique; + let iconXOffset = getPropertyValue(technique.iconXOffset, env); + let iconYOffset = getPropertyValue(technique.iconYOffset, env); + iconXOffset = typeof iconXOffset === "number" ? iconXOffset : 0; + iconYOffset = typeof iconYOffset === "number" ? iconYOffset : 0; + + // Now mirror the text offset relative to icon so manhattan distance is preserved, when + // alternative position is taken, this ensures that text-icon relative position is + // the same as in base alignment. + const hAlignDiff = hAlign - placement.h; + const vAlignDiff = vAlign - placement.v; + const relOffsetX = iconXOffset - textElement.xOffset; + const relOffsetY = iconYOffset - textElement.yOffset; + const centerBased = + hAlign === HorizontalAlignment.Center || vAlign === VerticalAlignment.Center; + if (centerBased) { + // Center based alternative placements. + offset.x += 2 * Math.abs(hAlignDiff) * relOffsetX; + offset.y -= 2 * vAlignDiff * Math.abs(relOffsetX); + + offset.y += 2 * Math.abs(vAlignDiff) * relOffsetY; + offset.x -= 2 * hAlignDiff * Math.abs(relOffsetY); + } else { + // Corner alternative placements + offset.x += 2 * Math.min(Math.abs(hAlignDiff), 0.5) * relOffsetX; + offset.y -= + 2 * + Math.sign(vAlignDiff) * + Math.min(Math.abs(vAlignDiff), 0.5) * + Math.abs(relOffsetX); + + offset.y += 2 * Math.min(Math.abs(vAlignDiff), 0.5) * relOffsetY; + offset.x -= + 2 * + Math.sign(hAlignDiff) * + Math.min(Math.abs(hAlignDiff), 0.5) * + Math.abs(relOffsetY); + } + } } offset.multiplyScalar(scale); @@ -318,6 +362,7 @@ export function placeIcon( * @param screenPosition Position of the label in screen coordinates. * @param scale Scale factor to be applied to label dimensions. * @param textCanvas The text canvas where the label will be placed. + * @param env The [[Env]] used to evaluate technique attributes. * @param screenCollisions Used to check collisions with other labels. * @param isRejected Whether the label is already rejected (e.g. because its icon was rejected). If * `true`, text won't be checked for collision, result will be either `PlacementResult.Invisible` @@ -336,6 +381,7 @@ export function placePointLabel( screenPosition: THREE.Vector2, scale: number, textCanvas: TextCanvas, + env: Env, screenCollisions: ScreenCollisions, isRejected: boolean, outScreenPosition: THREE.Vector3, @@ -364,6 +410,7 @@ export function placePointLabel( screenPosition, scale, textCanvas, + env, screenCollisions, isRejected, outScreenPosition @@ -376,6 +423,7 @@ export function placePointLabel( screenPosition, scale, textCanvas, + env, screenCollisions, outScreenPosition ); @@ -392,6 +440,7 @@ export function placePointLabel( * @param screenPosition Position of the label in screen coordinates. * @param scale Scale factor to be applied to label dimensions. * @param textCanvas The text canvas where the label will be placed. + * @param env The [[Env]] used to evaluate technique attributes. * @param screenCollisions Used to check collisions with other labels. * @param outScreenPosition The final label screen position after applying any offsets. * @returns `PlacementResult.Ok` if label can be placed at the base or optional anchor point, @@ -406,6 +455,7 @@ function placePointLabelChoosingAnchor( screenPosition: THREE.Vector2, scale: number, textCanvas: TextCanvas, + env: Env, screenCollisions: ScreenCollisions, outScreenPosition: THREE.Vector3 ): PlacementResult { @@ -449,6 +499,7 @@ function placePointLabelChoosingAnchor( anchorPlacement, scale, textCanvas, + env, screenCollisions, false, !isLastPlacement, @@ -503,6 +554,7 @@ function placePointLabelChoosingAnchor( * @param screenPosition Position of the label in screen coordinates. * @param scale Scale factor to be applied to label dimensions. * @param textCanvas The text canvas where the label will be placed. + * @param env The [[Env]] used to evaluate technique attributes. * @param screenCollisions Used to check collisions with other labels. * @param isRejected Whether the label is already rejected (e.g. because its icon was rejected). If * `true`, text won't be checked for collision, result will be either `PlacementResult.Invisible` or @@ -519,6 +571,7 @@ function placePointLabelAtCurrentAnchor( screenPosition: THREE.Vector2, scale: number, textCanvas: TextCanvas, + env: Env, screenCollisions: ScreenCollisions, isRejected: boolean, outScreenPosition: THREE.Vector3 @@ -533,6 +586,7 @@ function placePointLabelAtCurrentAnchor( lastPlacement, scale, textCanvas, + env, screenCollisions, isRejected, false, @@ -550,6 +604,7 @@ function placePointLabelAtCurrentAnchor( * @param placement Text placement relative to the label position. * @param scale Scale factor to be applied to label dimensions. * @param textCanvas The text canvas where the label will be placed. + * @param env The [[Env]] used to evaluate technique attributes. * @param screenCollisions Used to check collisions with other labels. * @param isRejected Whether the label is already rejected (e.g. because its icon was rejected). If * `true`, text won't be checked for collision, result will be either `PlacementResult.Invisible` or @@ -569,6 +624,7 @@ function placePointLabelAtAnchor( placement: AnchorPlacement, scale: number, textCanvas: TextCanvas, + env: Env, screenCollisions: ScreenCollisions, isRejected: boolean, forceInvalidation: boolean, @@ -597,13 +653,7 @@ function placePointLabelAtAnchor( } // Compute text offset from the anchor point - const textOffset = computePointTextOffset( - label, - placement.h, - placement.v, - scale, - tmpTextOffset - ); + const textOffset = computePointTextOffset(label, placement, scale, env, tmpTextOffset); textOffset.add(screenPosition); tmpBox.copy(label.bounds!); tmpBox.min.multiplyScalar(scale); diff --git a/@here/harp-mapview/lib/text/TextElementsRenderer.ts b/@here/harp-mapview/lib/text/TextElementsRenderer.ts index 92eb4a5d31..3798fa6b42 100644 --- a/@here/harp-mapview/lib/text/TextElementsRenderer.ts +++ b/@here/harp-mapview/lib/text/TextElementsRenderer.ts @@ -1619,6 +1619,7 @@ export class TextElementsRenderer { tempScreenPosition, distanceScaleFactor, textCanvas, + this.m_viewState.env, this.m_screenCollisions, iconRejected, tempPosition, diff --git a/@here/harp-mapview/test/PlacementTest.ts b/@here/harp-mapview/test/PlacementTest.ts index ef4f3e177b..05e59aaffa 100644 --- a/@here/harp-mapview/test/PlacementTest.ts +++ b/@here/harp-mapview/test/PlacementTest.ts @@ -265,6 +265,7 @@ describe("Placement", function() { new THREE.Vector2(0, 0), 1.0, textCanvas, + new Env(), screenCollisions, false, outPosition @@ -301,6 +302,7 @@ describe("Placement", function() { new THREE.Vector2(0, 0), 1.0, textCanvas, + new Env(), screenCollisions, false, position @@ -334,6 +336,7 @@ describe("Placement", function() { new THREE.Vector2(0, 0), 1.0, textCanvas, + new Env(), screenCollisions, false, position @@ -367,6 +370,7 @@ describe("Placement", function() { new THREE.Vector2(0, 0), 1.0, textCanvas, + new Env(), screenCollisions, false, position @@ -403,6 +407,7 @@ describe("Placement", function() { new THREE.Vector2(0, 0), 1.0, textCanvas, + new Env(), screenCollisions, false, position @@ -439,6 +444,7 @@ describe("Placement", function() { new THREE.Vector2(0, 0), 0.8, textCanvas, + new Env(), screenCollisions, false, position @@ -598,6 +604,7 @@ describe("Placement", function() { inPosition, 1.0, textCanvas, + new Env(), screenCollisions, false, outPosition, @@ -616,6 +623,7 @@ describe("Placement", function() { inPosition, 1.0, textCanvas, + new Env(), screenCollisions, false, outPosition, @@ -798,6 +806,7 @@ describe("Placement", function() { inPosition, 1.0, textCanvas, + new Env(), screenCollisions, false, outPosition, @@ -821,6 +830,7 @@ describe("Placement", function() { inPosition, 1.0, textCanvas, + new Env(), screenCollisions, false, outPosition, @@ -865,6 +875,7 @@ describe("Placement", function() { const offset = 5; // Need to keep some offset because of internal margin const inPositions = [new THREE.Vector2(-offset, 0), new THREE.Vector2(offset, 0)]; const outPositions = [new THREE.Vector3(), new THREE.Vector3()]; + const env = new Env(); // Place each text element sequentially without multi-placement support const results: PlacementResult[] = [0, 0]; @@ -876,6 +887,7 @@ describe("Placement", function() { inPositions[i], 1.0, textCanvas, + env, screenCollisions, false, outPositions[i] @@ -900,6 +912,7 @@ describe("Placement", function() { inPositions[i], 1.0, textCanvas, + env, screenCollisions, false, outPositions[i], @@ -938,6 +951,7 @@ describe("Placement", function() { const offset = 5; // Need to keep some offset because of internal margin const inPositions = [new THREE.Vector2(-offset, 0), new THREE.Vector2(offset, 0)]; const outPositions = [new THREE.Vector3(), new THREE.Vector3()]; + const env = new Env(); // Place each text element sequentially without multi-placement support const results: PlacementResult[] = [0, 0]; @@ -949,6 +963,7 @@ describe("Placement", function() { inPositions[i], 1.0, textCanvas, + env, screenCollisions, false, outPositions[i], @@ -984,6 +999,7 @@ describe("Placement", function() { let offset = 50; const inPositions = [new THREE.Vector2(-offset, 0), new THREE.Vector2(offset, 0)]; const outPositions = [new THREE.Vector3(), new THREE.Vector3()]; + const env = new Env(); // Place each text element sequentially. const results: PlacementResult[] = [0, 0]; @@ -995,6 +1011,7 @@ describe("Placement", function() { inPositions[i], 1.0, textCanvas, + env, screenCollisions, false, outPositions[i], @@ -1033,6 +1050,7 @@ describe("Placement", function() { inPositions[i], 1.0, textCanvas, + env, screenCollisions, false, outPositions[i], @@ -1074,6 +1092,7 @@ describe("Placement", function() { inPositions[i], 1.0, textCanvas, + env, screenCollisions, false, outPositions[i],