UNPKG

@here/harp-mapview

Version:

Functionality needed to render a map.

674 lines 36 kB
"use strict"; /* * Copyright (C) 2019-2021 HERE Europe B.V. * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.getWorldPosition = exports.isPathLabelTooSmall = exports.placePathLabel = exports.placePointLabel = exports.placeIcon = exports.PlacementResult = exports.newPointLabelTextMarginPercent = exports.persistentPointLabelTextMargin = exports.checkReadyForPlacement = exports.PrePlacementResult = exports.getMaxViewDistance = exports.pointToPlaneDistance = exports.computeViewDistance = void 0; const harp_datasource_protocol_1 = require("@here/harp-datasource-protocol"); const harp_geoutils_1 = require("@here/harp-geoutils"); const harp_text_canvas_1 = require("@here/harp-text-canvas"); const harp_utils_1 = require("@here/harp-utils"); const THREE = require("three"); const PoiRenderer_1 = require("../poi/PoiRenderer"); const ScreenCollisions_1 = require("../ScreenCollisions"); const TextElement_1 = require("./TextElement"); const TextElementType_1 = require("./TextElementType"); /** * Minimum number of pixels per character. Used during estimation if there is enough screen space * available to render a text. */ const MIN_AVERAGE_CHAR_WIDTH = 5; /** * Functions related to text element placement. */ const tmpPosition = new THREE.Vector3(0, 0, 0); const tmpCameraDir = new THREE.Vector3(0, 0, 0); const tmpPointDir = new THREE.Vector3(0, 0, 0); const COS_TEXT_ELEMENT_FALLOFF_ANGLE = 0.5877852522924731; // Math.cos(0.3 * Math.PI) /** * Checks whether the distance of the text element to the camera plane meets threshold criteria. * * @param textElement - The textElement of which the view distance will be checked, with coordinates * in world space. * @param poiIndex - If TextElement is a line marker, the index into the line marker positions. * @param eyePos - The eye (or camera) position that will be used as reference to calculate * the distance. * @param eyeLookAt - The eye looking direction - normalized. * @param maxViewDistance - The maximum distance value. * @returns The text element view distance if it's lower than the maximum value, otherwise * `undefined`. */ function checkViewDistance(textElement, poiIndex, eyePos, eyeLookAt, projectionType, maxViewDistance) { const textDistance = computeViewDistance(textElement, poiIndex, eyePos, eyeLookAt); if (projectionType !== harp_geoutils_1.ProjectionType.Spherical) { return textDistance <= maxViewDistance ? textDistance : undefined; } // For sphere projection: Filter labels that are close to the horizon tmpPosition.copy(textElement.position).normalize(); tmpCameraDir.copy(eyePos).normalize(); const cosAlpha = tmpPosition.dot(tmpCameraDir); const viewDistance = cosAlpha > COS_TEXT_ELEMENT_FALLOFF_ANGLE && textDistance <= maxViewDistance ? textDistance : undefined; return viewDistance; } /** * Computes distance of the specified text element to camera plane given with position and normal. * * The distance is measured as projection of the vector between `eyePosition` and text * onto the `eyeLookAt` vector, so it actually computes the distance to plane that * contains `eyePosition` and is described with `eyeLookAt` as normal. * * @note Used for measuring the distances to camera, results in the metric that describes * distance to camera near plane (assuming near = 0). Such metric is better as input for labels * scaling or fading factors then simple euclidean distance because it does not fluctuate during * simple camera panning. * * @param textElement - The textElement of which the view distance will be checked. It must have * coordinates in world space. * @param poiIndex - If TextElement is a line marker, the index into the line marker positions. * @param eyePosition - The world eye coordinates used a reference position to calculate * the distance. * @param eyeLookAt - The eye looking direction or simply said projection plane normal. * @returns The text element view distance. */ function computeViewDistance(textElement, poiIndex, eyePosition, eyeLookAt) { let viewDistance; // Compute the distances as the distance along plane normal. const path = textElement.path; if (path && path.length > 1) { if (poiIndex !== undefined && path && path.length > poiIndex) { viewDistance = pointToPlaneDistance(path[poiIndex], eyePosition, eyeLookAt); } else { const viewDistance0 = pointToPlaneDistance(path[0], eyePosition, eyeLookAt); const viewDistance1 = pointToPlaneDistance(path[path.length - 1], eyePosition, eyeLookAt); viewDistance = Math.min(viewDistance0, viewDistance1); } } else { viewDistance = pointToPlaneDistance(textElement.position, eyePosition, eyeLookAt); } return viewDistance; } exports.computeViewDistance = computeViewDistance; /** * Computes distance between the given point and a plane. * * May be used to measure distance of point labels to the camera projection (near) plane. * * @param pointPos - The position to measure distance to. * @param planePos - The position of any point on the plane. * @param planeNorm - The plane normal vector (have to be normalized already). */ function pointToPlaneDistance(pointPos, planePos, planeNorm) { const labelCamVec = tmpPointDir.copy(pointPos).sub(planePos); return labelCamVec.dot(planeNorm); } exports.pointToPlaneDistance = pointToPlaneDistance; /** * Computes the maximum view distance for text elements as a ratio of the given view's maximum far * plane distance. * @param viewState - The view for which the maximum view distance will be calculated. * @param farDistanceLimitRatio - The ratio to apply to the maximum far plane distance. * @returns Maximum view distance. */ function getMaxViewDistance(viewState, farDistanceLimitRatio) { return viewState.maxVisibilityDist * farDistanceLimitRatio; } exports.getMaxViewDistance = getMaxViewDistance; /** * State of fading. */ var PrePlacementResult; (function (PrePlacementResult) { PrePlacementResult[PrePlacementResult["Ok"] = 0] = "Ok"; PrePlacementResult[PrePlacementResult["NotReady"] = 1] = "NotReady"; PrePlacementResult[PrePlacementResult["Invisible"] = 2] = "Invisible"; PrePlacementResult[PrePlacementResult["TooFar"] = 3] = "TooFar"; PrePlacementResult[PrePlacementResult["Duplicate"] = 4] = "Duplicate"; PrePlacementResult[PrePlacementResult["Count"] = 5] = "Count"; })(PrePlacementResult = exports.PrePlacementResult || (exports.PrePlacementResult = {})); const tmpPlacementPosition = new THREE.Vector3(); /** * Applies early rejection tests for a given text element meant to avoid trying to place labels * that are not visible, not ready, duplicates etc... * @param textElement - The Text element to check. * @param poiIndex - If TextElement is a line marker, the index into the line marker positions * @param viewState - The view for which the text element will be placed. * @param m_poiManager - To prepare pois for rendering. * @param maxViewDistance - If specified, text elements farther than this max distance will be * rejected. * @returns An object with the result code and the text element view distance * ( or `undefined` of the checks failed) as second. */ function checkReadyForPlacement(textElement, poiIndex, viewState, poiManager, maxViewDistance) { // eslint-disable-next-line prefer-const let viewDistance; if (!textElement.visible) { return { result: PrePlacementResult.Invisible, viewDistance }; } // If a PoiTable is specified in the technique, the table is required to be // loaded before the POI can be rendered. if (!poiManager.updatePoiFromPoiTable(textElement)) { // PoiTable has not been loaded, but is required to determine // visibility. return { result: PrePlacementResult.NotReady, viewDistance }; } // Text element visibility and zoom level ranges must be checked after calling // updatePoiFromPoiTable, since that function may change those values. if (!textElement.visible || viewState.zoomLevel === textElement.maxZoomLevel || !harp_utils_1.MathUtils.isClamped(viewState.zoomLevel, textElement.minZoomLevel, textElement.maxZoomLevel)) { return { result: PrePlacementResult.Invisible, viewDistance }; } viewDistance = maxViewDistance === undefined ? computeViewDistance(textElement, poiIndex, viewState.worldCenter, viewState.lookAtVector) : checkViewDistance(textElement, poiIndex, viewState.worldCenter, viewState.lookAtVector, viewState.projection.type, maxViewDistance); if (viewDistance === undefined) { return { result: PrePlacementResult.TooFar, viewDistance }; } return { result: PrePlacementResult.Ok, viewDistance }; } exports.checkReadyForPlacement = 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 textBounds - The text screen bounds. * @param placement - The relative anchor placement (may be different then original alignment). * @param scale - The scaling factor (due to distance, etc.). * @param env - The {@link @here/harp-datasource-protocol#Env} used * to evaluate technique attributes. * @param offset - The offset result. */ function computePointTextOffset(textElement, textBounds, placement, scale, env, offset = new THREE.Vector2()) { harp_utils_1.assert(textElement.type === TextElementType_1.TextElementType.PoiLabel || textElement.type === TextElementType_1.TextElementType.LineMarker); harp_utils_1.assert(textElement.layoutStyle !== undefined); offset.x = textElement.xOffset; offset.y = textElement.yOffset; switch (placement.h) { case harp_text_canvas_1.HorizontalPlacement.Left: // Already accounts for any margin that is already applied to the text element bounds. offset.x -= textBounds.max.x; break; case harp_text_canvas_1.HorizontalPlacement.Right: // Account for any margin applied as above. offset.x -= textBounds.min.x; break; } switch (placement.v) { case harp_text_canvas_1.VerticalPlacement.Top: offset.y -= textBounds.min.y; break; case harp_text_canvas_1.VerticalPlacement.Center: offset.y -= 0.5 * (textBounds.max.y + textBounds.min.y); break; case harp_text_canvas_1.VerticalPlacement.Bottom: // Accounts for vertical margin that may be applied to the text bounds. offset.y -= textBounds.max.y; break; } if (textElement.poiInfo !== undefined && TextElement_1.poiIsRenderable(textElement.poiInfo)) { harp_utils_1.assert(textElement.poiInfo.computedWidth !== undefined); harp_utils_1.assert(textElement.poiInfo.computedHeight !== undefined); // 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 = harp_text_canvas_1.hPlacementFromAlignment(textElement.layoutStyle.horizontalAlignment); const vAlign = harp_text_canvas_1.vPlacementFromAlignment(textElement.layoutStyle.verticalAlignment); if (hAlign !== placement.h || vAlign !== placement.v) { // Read icon offset used. const technique = textElement.poiInfo.technique; let iconXOffset = harp_datasource_protocol_1.getPropertyValue(technique.iconXOffset, env); let iconYOffset = harp_datasource_protocol_1.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 === harp_text_canvas_1.HorizontalPlacement.Center || vAlign === harp_text_canvas_1.VerticalPlacement.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); return offset; } const tmpBox = new THREE.Box2(); const tmpBounds = new THREE.Box2(); const tmpBoxes = []; const tmpMeasurementParams = {}; const tmpCollisionBoxes = []; const tmpCollisionBox = new ScreenCollisions_1.CollisionBox(); const tmpScreenPosition = new THREE.Vector2(); const tmpTextOffset = new THREE.Vector2(); const tmp2DBox = new harp_utils_1.Math2D.Box(); const tmpCenter = new THREE.Vector2(); const tmpSize = new THREE.Vector2(); /** * The margin applied to the text bounds of every point label. */ exports.persistentPointLabelTextMargin = new THREE.Vector2(2, 2); /** * Additional bounds scaling (described as percentage of full size) applied to the new labels. * * This additional scaling (margin) allows to account for slight camera position and * orientation changes, so new labels are placed only if there is enough space around them. * Such margin limits collisions with neighboring labels while doing small camera movements and * thus reduces labels flickering. */ exports.newPointLabelTextMarginPercent = 0.1; var PlacementResult; (function (PlacementResult) { PlacementResult[PlacementResult["Ok"] = 0] = "Ok"; PlacementResult[PlacementResult["Rejected"] = 1] = "Rejected"; PlacementResult[PlacementResult["Invisible"] = 2] = "Invisible"; })(PlacementResult = exports.PlacementResult || (exports.PlacementResult = {})); /** * Places an icon on screen. * @param iconRenderState - The icon state. * @param poiInfo - Icon information necessary to compute its dimensions. * @param screenPosition - Screen position of the icon. * @param scaleFactor - Scaling factor to apply to the icon dimensions. * @param screenCollisions - Used to check the icon visibility and collisions. * @param env - Current map env. * @returns `PlacementResult.Ok` if icon can be placed, `PlacementResult.Rejected` if there's * a collision, `PlacementResult.Invisible` if it's not visible. */ function placeIcon(iconRenderState, poiInfo, screenPosition, scaleFactor, env, screenCollisions) { PoiRenderer_1.PoiRenderer.computeIconScreenBox(poiInfo, screenPosition, scaleFactor, env, tmp2DBox); if (!screenCollisions.isVisible(tmp2DBox)) { return PlacementResult.Invisible; } const iconSpaceAvailable = poiInfo.mayOverlap === true || !screenCollisions.isAllocated(tmp2DBox); return !iconSpaceAvailable ? PlacementResult.Rejected : PlacementResult.Ok; } exports.placeIcon = placeIcon; /** * Place a point label text using single or multiple alternative placement anchors. * * @param labelState - State of the point label to place. * @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 {@link @here/harp-datasource-protocol#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. * @param multiAnchor - The parameter decides if multi-anchor placement algorithm should be * used, be default [[false]] meaning try to place label using current alignment settings only. * @returns `PlacementResult.Ok` if point __label can be placed__ at the base or any optional * anchor point. `PlacementResult.Rejected` if there's a collision for all placements. Finally * `PlacementResult.Invisible` if it's text is not visible at any placement position. */ function placePointLabel(labelState, screenPosition, scale, textCanvas, env, screenCollisions, outScreenPosition, multiAnchor = false) { harp_utils_1.assert(labelState.element.layoutStyle !== undefined); const layoutStyle = labelState.element.layoutStyle; // Check if alternative placements have been provided. multiAnchor = multiAnchor && layoutStyle.placements !== undefined && layoutStyle.placements.length > 1; // For single placement labels or labels with icon rejected, do only current anchor testing. if (!multiAnchor) { return placePointLabelAtCurrentAnchor(labelState, screenPosition, scale, textCanvas, env, screenCollisions, outScreenPosition); } // Otherwise test also alternative text placements. else { return placePointLabelChoosingAnchor(labelState, screenPosition, scale, textCanvas, env, screenCollisions, outScreenPosition); } } exports.placePointLabel = placePointLabel; /** * Try to place a point label text using multiple optional placements. * * @note Function should be called only for labels with icons not rejected and for text alignments * different then [[HorizontalAlignment.Center]] and [[VerticalAlignment.Center]]. * * @param labelState - State of the point label to place. * @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 {@link @here/harp-datasource-protocol#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, * `PlacementResult.Rejected` if there's a collision for all placements, `PlacementResult.Invisible` * if it's not visible at any placement position. * * @internal * @hidden */ function placePointLabelChoosingAnchor(labelState, screenPosition, scale, textCanvas, env, screenCollisions, outScreenPosition) { harp_utils_1.assert(labelState.element.layoutStyle !== undefined); const label = labelState.element; // Store label state - persistent or new label. const persistent = labelState.visible; // Start with last alignment settings if layout state was stored or // simply begin from layout defined in theme. const lastPlacement = labelState.textPlacement; const placements = label.layoutStyle.placements; const placementsNum = placements.length; // Find current anchor placement on the optional placements list. // Index of exact match. const matchIdx = placements.findIndex(p => p.h === lastPlacement.h && p.v === lastPlacement.v); harp_utils_1.assert(matchIdx >= 0); // Will be true if all text placements are invisible. let allInvisible = true; // Iterate all placements starting from current one. for (let i = matchIdx; i < placementsNum + matchIdx; ++i) { const anchorPlacement = placements[i % placementsNum]; // Bounds may be already calculated for persistent label, force re-calculation only // for alternative (new) placements. const isLastPlacement = i === matchIdx && persistent; // Compute label bounds, visibility or collision according to new layout settings. const placementResult = placePointLabelAtAnchor(labelState, screenPosition, anchorPlacement, scale, textCanvas, env, screenCollisions, !isLastPlacement, tmpPlacementPosition); if (placementResult === PlacementResult.Ok) { outScreenPosition.copy(tmpPlacementPosition); return PlacementResult.Ok; } // Store last successful (previous frame) position even if it's now rejected (to fade out). if (isLastPlacement) { outScreenPosition.copy(tmpPlacementPosition); } // Invisible = Persistent label out of screen or the new label that is colliding. allInvisible = allInvisible && placementResult === PlacementResult.Invisible; } return allInvisible ? // All text's placements out of the screen. PlacementResult.Invisible : // All placements are either colliding or out of screen . PlacementResult.Rejected; } /** * Places a point label on a specified text canvas using the alignment (anchor) currently set. * * @param labelState - State of the point label to place. * @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 {@link @here/harp-datasource-protocol#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 point label can be placed, `PlacementResult.Rejected` if there's * a collision, `PlacementResult.Invisible` if it's not visible. * * @internal * @hidden */ function placePointLabelAtCurrentAnchor(labelState, screenPosition, scale, textCanvas, env, screenCollisions, outScreenPosition) { harp_utils_1.assert(labelState.element.layoutStyle !== undefined); // Use recently rendered (state stored) layout if available, otherwise theme based style. const lastPlacement = labelState.textPlacement; const result = placePointLabelAtAnchor(labelState, screenPosition, lastPlacement, scale, textCanvas, env, screenCollisions, !labelState.visible, outScreenPosition); return result; } /** * Auxiliary function that tries to place a point label on a text canvas using specified alignment. * * @param labelState - State of the point label to place. * @param screenPosition - Position of the label in screen coordinates * @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 {@link @here/harp-datasource-protocol#Env} * used to evaluate technique attributes. * @param screenCollisions - Used to check collisions with other labels. * @param forceInvalidation - Set to true if text layout or other params has changed such as text * re-measurement is required and text buffer need to be invalidated. * @param outScreenPosition - The final label screen position after applying any offsets. * @returns `PlacementResult.Ok` if point label can be placed, `PlacementResult.Rejected` if there's * a collision, `PlacementResult.Invisible` if it's not visible. * * @internal * @hidden */ function placePointLabelAtAnchor(labelState, screenPosition, placement, scale, textCanvas, env, screenCollisions, forceInvalidation, outScreenPosition) { const label = labelState.element; harp_utils_1.assert(label.glyphs !== undefined); harp_utils_1.assert(label.layoutStyle !== undefined); const measureText = !label.bounds || forceInvalidation; const labelBounds = measureText ? tmpBounds : label.bounds; if (measureText) { // Override text canvas layout style for measurement. applyTextPlacement(textCanvas, placement); tmpMeasurementParams.outputCharacterBounds = undefined; tmpMeasurementParams.path = undefined; tmpMeasurementParams.pathOverflow = false; tmpMeasurementParams.letterCaseArray = label.glyphCaseArray; // Compute label bounds according to layout settings. textCanvas.measureText(label.glyphs, labelBounds, tmpMeasurementParams); } // Compute text offset from the anchor point const textOffset = computePointTextOffset(label, labelBounds, placement, scale, env, tmpTextOffset).add(screenPosition); // Update output screen position. outScreenPosition.set(textOffset.x, textOffset.y, labelState.renderDistance); // Apply additional persistent margin, keep in mind that text bounds just calculated // are not (0, 0, w, h) based, so their coords usually are also non-zero. // TODO: Make the margin configurable tmpBox.copy(labelBounds).expandByVector(exports.persistentPointLabelTextMargin).translate(textOffset); tmpBox.getCenter(tmpCenter); tmpBox.getSize(tmpSize); tmpSize.multiplyScalar(scale); tmp2DBox.set(tmpCenter.x - tmpSize.x / 2, tmpCenter.y - tmpSize.y / 2, tmpSize.x, tmpSize.y); // Check the text visibility if invisible finish immediately // regardless of the persistence state - no fading required. if (!screenCollisions.isVisible(tmp2DBox)) { return PlacementResult.Invisible; } if (measureText) { // Up-scaled label bounds are used only for new labels and only for collision check, this // is intentional to avoid processing labels out of the screen due to increased bounds, // such labels would be again invisible in the next frame. tmpBox.getSize(tmpSize); tmpSize.multiplyScalar(scale * (1 + exports.newPointLabelTextMarginPercent)); tmp2DBox.set(tmpCenter.x - tmpSize.x / 2, tmpCenter.y - tmpSize.y / 2, tmpSize.x, tmpSize.y); } // Check label's text collision. Collision is more important than visibility (now), because for // icon/text combinations the icon should be rendered if the text is out of bounds, but it may // _not_ be rendered if the text is colliding with another label. if (!label.textMayOverlap && screenCollisions.isAllocated(tmp2DBox)) { return PlacementResult.Rejected; } // Don't allocate space for rejected text. When zooming, this allows placement of a // lower priority text element that was displaced by a higher priority one (not // present in the new zoom level) before an even lower priority one takes the space. // Otherwise the lowest priority text will fade in and back out. // TODO: Add a unit test for this scenario. if (label.textReservesSpace) { screenCollisions.allocate(tmp2DBox); } // Glyphs arrangement have been changed remove text buffer object which needs to be // re-created. if (measureText) { label.textBufferObject = undefined; label.bounds = label.bounds ? label.bounds.copy(labelBounds) : labelBounds.clone(); } else { // Override text canvas layout style for placement. applyTextPlacement(textCanvas, placement); } // Save current placement in label state. // TextElementState creates layout snapshot solely for alternative placements which saves // memory that could be wasted on unnecessary objects construction. labelState.textPlacement = placement; return PlacementResult.Ok; } /** * Applied modified text layout style to TextCanvas for further use. * @param textCanvas - TextCanvas reference. * @param placement - The text placement to be used. */ function applyTextPlacement(textCanvas, placement) { // Setup TextCanvas layout settings of the new placement as it is required for further // TextBufferObject creation and measurements in addText(). textCanvas.textLayoutStyle.horizontalAlignment = harp_text_canvas_1.hAlignFromPlacement(placement.h); textCanvas.textLayoutStyle.verticalAlignment = harp_text_canvas_1.vAlignFromPlacement(placement.v); } /** * Places a path label along a given path on a specified text canvas. * @param labelState - The state of the path label to place. * @param textPath - The text path along which the label will be placed. * @param screenPosition - Position of the label in screen coordinates. * @param textCanvas - The text canvas where the label will be placed. * @param screenCollisions - Used to check collisions with other labels. * @returns `PlacementResult.Ok` if path label can be placed, `PlacementResult.Rejected` if there's * a collision or text doesn't fit into path, `PlacementResult.Invisible` if it's not visible. */ function placePathLabel(labelState, textPath, screenPosition, textCanvas, screenCollisions) { // Recalculate the text bounds for this path label. If measurement fails, the whole // label doesn't fit the path and should be discarded. tmpMeasurementParams.path = textPath; tmpMeasurementParams.outputCharacterBounds = tmpBoxes; tmpMeasurementParams.letterCaseArray = labelState.element.glyphCaseArray; // TODO: HARP-7648. TextCanvas.measureText does the placement as in TextCanvas.addText but // without storing the result. If the measurement succeeds, the placement work is done // twice. // This could be done in one step (e.g measureAndAddText). Collision test could be injected // in the middle as a function. if (!textCanvas.measureText(labelState.element.glyphs, tmpBox, tmpMeasurementParams)) { return PlacementResult.Rejected; } // Coarse collision check. tmpCollisionBox.copy(tmpBox.translate(screenPosition)); if (!screenCollisions.isVisible(tmpCollisionBox)) { return PlacementResult.Invisible; } let checkGlyphCollision = false; let candidateBoxes; if (!labelState.element.textMayOverlap) { candidateBoxes = screenCollisions.search(tmpCollisionBox); checkGlyphCollision = candidateBoxes.length > 0; } // Perform per-character collision checks. tmpCollisionBoxes.length = tmpBoxes.length; for (let i = 0; i < tmpBoxes.length; ++i) { const glyphBox = tmpBoxes[i].translate(screenPosition); let collisionBox = tmpCollisionBoxes[i]; if (collisionBox === undefined) { collisionBox = new ScreenCollisions_1.CollisionBox(glyphBox); tmpCollisionBoxes[i] = collisionBox; } else { collisionBox.copy(glyphBox); } if (checkGlyphCollision && screenCollisions.intersectsDetails(collisionBox, candidateBoxes)) { return PlacementResult.Rejected; } } // Allocate collision info if needed. if (labelState.element.textReservesSpace) { const collisionBox = new ScreenCollisions_1.DetailedCollisionBox(tmpCollisionBox, tmpCollisionBoxes.slice()); tmpCollisionBoxes.length = 0; screenCollisions.allocate(collisionBox); } return PlacementResult.Ok; } exports.placePathLabel = placePathLabel; /** * Check if a given path label is too small to be rendered. * @param textElement - The text element to check. * @param screenProjector - Used to project coordinates from world to screen space. * @param outScreenPoints - Label path projected to screen space. * @returns `true` if label is too small, `false` otherwise. */ function isPathLabelTooSmall(textElement, screenProjector, outScreenPoints) { harp_utils_1.assert(textElement.type === TextElementType_1.TextElementType.PathLabel); // Get the screen points that define the label's segments and create a path with // them. outScreenPoints.length = 0; let anyPointVisible = false; for (const pt of textElement.points) { // Skip invisible points at the beginning of the path. const screenPoint = anyPointVisible ? screenProjector.project(pt, tmpScreenPosition) : screenProjector.projectToScreen(pt, tmpScreenPosition); if (screenPoint === undefined) { continue; } anyPointVisible = true; outScreenPoints.push(tmpScreenPosition.clone()); } // TODO: (HARP-3515) // The rendering of a path label that contains just a single point that is not // visible is impossible, which is problematic with long paths. // Fix: Skip/clip the invisible points at beginning and end of the path to get // the visible part of the path. // If not a single point is visible, skip the path if (!anyPointVisible) { return true; } // Check/guess if the screen box can hold a string of that length. It is important // to guess that value without measuring the font first to save time. const minScreenSpace = textElement.text.length * MIN_AVERAGE_CHAR_WIDTH; tmpBox.setFromPoints(outScreenPoints); const boxDiagonalSq = tmpBox.max.sub(tmpBox.min).lengthSq(); if (boxDiagonalSq < minScreenSpace * minScreenSpace) { textElement.dbgPathTooSmall = true; return true; } return false; } exports.isPathLabelTooSmall = isPathLabelTooSmall; const tmpOrientedBox = new harp_geoutils_1.OrientedBox3(); /** * Calculates the world position of the supplied label. The label will be shifted if there is a * specified offsetDirection and value to shift it in. * @param poiLabel - The label to shift * @param projection - The projection, required to compute the correct direction offset for * spherical projections. * @param env - The environment to extract the worldOffset needed to shift the icon in world space, * if configured in the style. * @param outWorldPosition - Preallocated vector to store the result in * @returns the [[outWorldPosition]] vector. */ function getWorldPosition(poiLabel, projection, env, outWorldPosition) { var _a, _b; const worldOffsetShiftValue = harp_datasource_protocol_1.getPropertyValue((_b = (_a = poiLabel.poiInfo) === null || _a === void 0 ? void 0 : _a.technique) === null || _b === void 0 ? void 0 : _b.worldOffset, env); outWorldPosition === null || outWorldPosition === void 0 ? void 0 : outWorldPosition.copy(poiLabel.position); if (worldOffsetShiftValue !== null && worldOffsetShiftValue !== undefined && poiLabel.offsetDirection !== undefined) { projection.localTangentSpace(poiLabel.position, tmpOrientedBox); const offsetDirectionVector = tmpOrientedBox.yAxis; const offsetDirectionRad = THREE.MathUtils.degToRad(poiLabel.offsetDirection); // Negate to get the normal, i.e. the vector pointing to the sky. offsetDirectionVector.applyAxisAngle(tmpOrientedBox.zAxis.negate(), offsetDirectionRad); outWorldPosition.addScaledVector(tmpOrientedBox.yAxis, worldOffsetShiftValue); } return outWorldPosition; } exports.getWorldPosition = getWorldPosition; //# sourceMappingURL=Placement.js.map