Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

Commit

Permalink
HARP-12685: Optimize label placement
Browse files Browse the repository at this point in the history
Signed-off-by: stefan.dachwitz <stefan.dachwitz@here.com>
  • Loading branch information
StefanDachwitz committed Nov 3, 2020
1 parent d8b171f commit 3a6a034
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 18 deletions.
31 changes: 31 additions & 0 deletions @here/harp-mapview/lib/ScreenProjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,37 @@ export class ScreenProjector {
return undefined;
}

/**
* Test if the area around the specified point is visible on the screen.
*
* @param {(Vector3Like)} source The centered source vector to project.
* @param {(Number)} width Half of the width of the area in NDC space [0..1].
* @param {(Number)} height Half of the height of the area in NDC space [0..1].
* @param {THREE.Vector2} target The target vector.
* @returns {THREE.Vector2} The projected vector (the parameter 'target') or undefined if
* the area is completely outside the screen.
*/
projectAreaToScreen(
source: Vector3Like,
width: number,
height: number,
target: THREE.Vector2 = new THREE.Vector2()
): THREE.Vector2 | undefined {
width *= 2;
height *= 2;
const p = this.projectVector(source, ScreenProjector.tempV3);
if (
isInRange(p) &&
p.x + width >= -1 &&
p.x - width <= 1 &&
p.y + height >= -1 &&
p.y - height <= 1
) {
return this.ndcToScreen(p, target);
}
return undefined;
}

/**
* Apply current projectionViewMatrix of the camera to project the source vector into
* screen coordinates. The z component between -1 and 1 is also returned.
Expand Down
41 changes: 23 additions & 18 deletions @here/harp-mapview/lib/text/TextElementsRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { LineMarkerTechnique } from "@here/harp-datasource-protocol";
import { TileKey } from "@here/harp-geoutils";
import { TileKey, Vector3Like } from "@here/harp-geoutils";
import {
AdditionParameters,
DEFAULT_TEXT_CANVAS_LAYER,
Expand Down Expand Up @@ -1778,15 +1778,11 @@ export class TextElementsRenderer {
this.m_tmpVector3
);

const projectionResult = this.m_screenProjector.projectToScreen(
worldPosition,
tempScreenPosition
);

// Only process labels that are potentially within the frustum.
if (projectionResult === undefined) {
if (!this.labelPotentiallyVisible(worldPosition, tempScreenPosition)) {
return false;
}

// Add this POI as a point label.
return this.addPointLabel(
labelState,
Expand Down Expand Up @@ -1839,12 +1835,7 @@ export class TextElementsRenderer {
const point = path[pointIndex];

// Only process potentially visible labels
const projectionResult = this.m_screenProjector.projectToScreen(
point,
tempScreenPosition
);

if (projectionResult !== undefined) {
if (this.labelPotentiallyVisible(point, tempScreenPosition)) {
// Find a suitable location for the lineMarker to be placed at.
let tooClose = false;
for (let j = 0; j < shieldGroup.length; j += 2) {
Expand Down Expand Up @@ -1885,11 +1876,7 @@ export class TextElementsRenderer {
for (let pointIndex = 0; pointIndex < path.length; ++pointIndex) {
const point = path[pointIndex];
// Only process potentially visible labels
const projectionResult = this.m_screenProjector.projectToScreen(
point,
tempScreenPosition
);
if (projectionResult !== undefined) {
if (this.labelPotentiallyVisible(point, tempScreenPosition)) {
this.addPointLabel(
labelState,
point,
Expand Down Expand Up @@ -2049,4 +2036,22 @@ export class TextElementsRenderer {
this.m_overloaded = newOverloaded;
return this.m_overloaded;
}

/**
* Project point to screen and check if it is on screen or within a fixed distance to the
* border.
*
* @param point center point of label.
* @param outPoint projected screen point of label.
*/
private labelPotentiallyVisible(point: Vector3Like, outPoint: THREE.Vector2): boolean {
const maxDistance = THREE.MathUtils.clamp(this.m_options.maxPoiDistanceToBorder ?? 0, 0, 1);
const projectionResult = this.m_screenProjector.projectAreaToScreen(
point,
maxDistance,
maxDistance,
outPoint
);
return projectionResult !== undefined;
}
}
17 changes: 17 additions & 0 deletions @here/harp-mapview/lib/text/TextElementsRendererOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ const DEFAULT_LABEL_DISTANCE_SCALE_MIN = 0.7;
*/
const DEFAULT_LABEL_DISTANCE_SCALE_MAX = 1.5;

// Allowed distance to screen border for early rejection of POIs during placement. Its range is
// [0..1] of screen size.
// A value of 0 will lead to POI labels popping in at the border of the screen. A large value will
// lead to many labels being placed outside the screen, with all the required actions for measuring
// and loading glyphs impacting performance.
const DEFAULT_MAX_DISTANCE_TO_BORDER = 0.2;

const MIN_GLYPH_COUNT = 1024;

const MAX_GLYPH_COUNT = 32768;
Expand Down Expand Up @@ -113,6 +120,12 @@ export interface TextElementsRendererOptions {
* @default `false`
*/
showReplacementGlyphs?: boolean;

/**
* The maximum distance to the screen border as a fraction of screen size [0..1].
* @default [[DEFAULT_MAX_DISTANCE_TO_BORDER]].
*/
maxPoiDistanceToBorder?: number;
}

/**
Expand Down Expand Up @@ -161,4 +174,8 @@ export function initializeDefaultOptions(options: TextElementsRendererOptions) {
if (options.showReplacementGlyphs === undefined) {
options.showReplacementGlyphs = false;
}

if (options.maxPoiDistanceToBorder === undefined) {
options.maxPoiDistanceToBorder = DEFAULT_MAX_DISTANCE_TO_BORDER;
}
}
220 changes: 220 additions & 0 deletions @here/harp-mapview/test/ScreenProjectorTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,224 @@ describe("ScreenProjector", () => {
expect(result).not.deep.equal(new THREE.Vector2());
});
});

describe("projectAreaToScreen", () => {
let result: THREE.Vector2;
const halfScreenSize: number = screenSize / 2;

before(() => {
camera = new THREE.OrthographicCamera(
-halfScreenSize,
halfScreenSize,
halfScreenSize,
-halfScreenSize,
near,
far
);
sp = new ScreenProjector(camera);
sp.update(camera, screenSize, screenSize);
});

beforeEach(() => {
result = new THREE.Vector2();
});

it("returns projected vector for areas within frustum", () => {
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(-halfScreenSize + eps, 0, -near - eps),
0,
0,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(halfScreenSize - eps, 0, -far + eps),
0,
0,
result
)
);
});

it("returns projected vector for areas within near/far planes on other frustum\
planes", () => {
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(-halfScreenSize, halfScreenSize, -near - eps),
0,
0,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(halfScreenSize, -halfScreenSize, -far + eps),
0,
0,
result
)
);
});

it("returns undefined for infinitesimal areas outside of frustum", () => {
assert.notExists(
sp.projectAreaToScreen(
new THREE.Vector3(-halfScreenSize - eps, 0, -near - eps),
0,
0,
result
)
);

assert.notExists(
sp.projectAreaToScreen(
new THREE.Vector3(halfScreenSize + eps, 0, -near - eps),
0,
0,
result
)
);

assert.notExists(
sp.projectAreaToScreen(
new THREE.Vector3(0, -halfScreenSize - eps, -near - eps),
0,
0,
result
)
);

assert.notExists(
sp.projectAreaToScreen(
new THREE.Vector3(0, halfScreenSize + eps, -near - eps),
0,
0,
result
)
);
});

it("returns projected vector for areas within near/far planes but outside of\
frustum", () => {
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(-halfScreenSize - eps, 0, -near - eps),
eps,
eps,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(halfScreenSize + eps, 1, -near - eps),
eps,
eps,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(0, -halfScreenSize - eps, -near - eps),
eps,
eps,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(0, halfScreenSize + eps, -near - eps),
eps,
eps,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
});

it("returns projected vector for areas partially inside of the frustum", () => {
const radius = 0.2;
const radiusInScreenSize = radius * screenSize;
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(-halfScreenSize - radiusInScreenSize + eps, 0, -near - eps),
radius,
radius,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(halfScreenSize + radiusInScreenSize - eps, 1, -near - eps),
radius,
radius,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(0, -halfScreenSize - radiusInScreenSize + eps, -near - eps),
radius,
radius,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
assert.exists(
sp.projectAreaToScreen(
new THREE.Vector3(0, halfScreenSize + radiusInScreenSize - eps, -near - eps),
radius,
radius,
result
)
);
expect(result).not.deep.equal(new THREE.Vector2());
});

it("returns undefined for areas within near/far planes but completely outside of\
frustum", () => {
const radius = 0.2;
const radiusInScreenSize = radius * screenSize;
assert.notExists(
sp.projectAreaToScreen(
new THREE.Vector3(-halfScreenSize - radiusInScreenSize - eps, 0, -near - eps),
radius,
radius,
result
)
);
assert.notExists(
sp.projectAreaToScreen(
new THREE.Vector3(halfScreenSize + radiusInScreenSize + eps, 1, -near - eps),
radius,
radius,
result
)
);
assert.notExists(
sp.projectAreaToScreen(
new THREE.Vector3(0, -halfScreenSize - radiusInScreenSize - eps, -near - eps),
radius,
radius,
result
)
);
assert.notExists(
sp.projectAreaToScreen(
new THREE.Vector3(0, halfScreenSize + radiusInScreenSize + eps, -near - eps),
radius,
radius,
result
)
);
});
});
});

0 comments on commit 3a6a034

Please sign in to comment.