import { Env , getPropertyValue , PoiTechnique } from "@here/harp-datasource-protocol" ; import { OrientedBox3 , Projection , ProjectionType } from "@here/harp-geoutils" ; import { hAlignFromPlacement , HorizontalPlacement , hPlacementFromAlignment , MeasurementParameters , TextCanvas , TextPlacement , vAlignFromPlacement , VerticalPlacement , vPlacementFromAlignment } from "@here/harp-text-canvas" ; import { assert , Math2D , MathUtils } from "@here/harp-utils" ; import * as THREE from "three" ; import { PoiManager } from "../poi/PoiManager" ; import { PoiRenderer } from "../poi/PoiRenderer" ; import { CollisionBox , DetailedCollisionBox , IBox , ScreenCollisions } from "../ScreenCollisions" ; import { ScreenProjector } from "../ScreenProjector" ; import { RenderState } from "./RenderState" ; import { PoiInfo , poiIsRenderable , TextElement } from "./TextElement" ; import { TextElementState } from "./TextElementState" ; import { TextElementType } from "./TextElementType" ; import { ViewState } from "./ViewState" ; /**
 * 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 ; param textElement param poiIndex param eyePos param eyeLookAt param maxViewDistance returns function checkViewDistance ( textElement , poiIndex , eyePos , eyeLookAt , projectionType , maxViewDistance ) { const textDistance = computeViewDistance ( textElement , poiIndex , eyePos , eyeLookAt ) ; if ( projectionType !== ProjectionType . Spherical ) { return textDistance <= maxViewDistance ? textDistance : undefined ; } 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 ; } note param textElement param poiIndex param eyePosition param eyeLookAt returns export function computeViewDistance ( textElement , poiIndex , eyePosition , eyeLookAt ) { let viewDistance ; 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 ; } param pointPos param planePos param planeNorm export function pointToPlaneDistance ( pointPos , planePos , planeNorm ) { const labelCamVec = tmpPointDir . copy ( pointPos ) . sub ( planePos ) ; return labelCamVec . dot ( planeNorm ) ; } param viewState param farDistanceLimitRatio returns export function getMaxViewDistance ( viewState , farDistanceLimitRatio ) { return viewState . maxVisibilityDist * farDistanceLimitRatio ; } /**
 * State of fading.
 */ export enum PrePlacementResult { Ok = 0 , NotReady , Invisible , TooFar , Duplicate , Count } const tmpPlacementPosition = new THREE . Vector3 ( ) ; param textElement param poiIndex param viewState param m_poiManager param maxViewDistance returns export function checkReadyForPlacement ( textElement , poiIndex , viewState , poiManager , maxViewDistance ? ) { let viewDistance ; if ( ! textElement . visible ) { return { result : PrePlacementResult . Invisible , viewDistance } ; } if ( ! poiManager . updatePoiFromPoiTable ( textElement ) ) { return { result : PrePlacementResult . NotReady , viewDistance } ; } if ( ! textElement . visible || viewState . zoomLevel === textElement . maxZoomLevel || ! 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 } ; } param textElement param textBounds param placement param scale param env - The  {@link @here/harp-datasource-protocol#Env} used
 *                  to evaluate technique attributes.
 *  param offset function computePointTextOffset ( textElement , textBounds , placement , scale , env , offset = new THREE . Vector2 ( ) ) { assert ( textElement . type === TextElementType . PoiLabel || textElement . type === TextElementType . LineMarker ) ; assert ( textElement . layoutStyle !== undefined ) ; offset . x = textElement . xOffset ; offset . y = textElement . yOffset ; switch ( placement . h ) { case HorizontalPlacement . Left : offset . x -= textBounds . max . x ; break ; case HorizontalPlacement . Right : offset . x -= textBounds . min . x ; break ; } switch ( placement . v ) { case VerticalPlacement . Top : offset . y -= textBounds . min . y ; break ; case VerticalPlacement . Center : offset . y -= 0.5 * ( textBounds . max . y + textBounds . min . y ) ; break ; case VerticalPlacement . Bottom : offset . y -= textBounds . max . y ; break ; } if ( textElement . poiInfo !== undefined && poiIsRenderable ( textElement . poiInfo ) ) { assert ( textElement . poiInfo . computedWidth !== undefined ) ; assert ( textElement . poiInfo . computedHeight !== undefined ) ; offset . x += textElement . poiInfo . computedWidth ! * ( 0.5 + placement . h ) ; offset . y += textElement . poiInfo . computedHeight ! * ( 0.5 + placement . v ) ; const hAlign = hPlacementFromAlignment ( textElement . layoutStyle ! . horizontalAlignment ) ; const vAlign = vPlacementFromAlignment ( textElement . layoutStyle ! . verticalAlignment ) ; if ( hAlign !== placement . h || vAlign !== placement . v ) { 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 ; const hAlignDiff = hAlign - placement . h ; const vAlignDiff = vAlign - placement . v ; const relOffsetX = iconXOffset - textElement . xOffset ; const relOffsetY = iconYOffset - textElement . yOffset ; const centerBased = hAlign === HorizontalPlacement . Center || vAlign === VerticalPlacement . Center ; if ( centerBased ) { 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 { 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 CollisionBox ( ) ; const tmpScreenPosition = new THREE . Vector2 ( ) ; const tmpTextOffset = new THREE . Vector2 ( ) ; const tmp2DBox = new Math2D . Box ( ) ; const tmpCenter = new THREE . Vector2 ( ) ; const tmpSize = new THREE . Vector2 ( ) ; /**
 * The margin applied to the text bounds of every point label.
 */ export const 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.
 */ export const newPointLabelTextMarginPercent = 0.1 ; export enum PlacementResult { Ok , Rejected , Invisible } param iconRenderState param poiInfo param screenPosition param scaleFactor param screenCollisions param env returns export function placeIcon ( iconRenderState , poiInfo , screenPosition , scaleFactor , env , screenCollisions ) { 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 ; } param labelState param screenPosition param scale param textCanvas param env - The  {@link @here/harp-datasource-protocol#Env} used
 *              to evaluate technique attributes.
 *  param screenCollisions param outScreenPosition param multiAnchor returns export function placePointLabel ( labelState , screenPosition , scale , textCanvas , env , screenCollisions , outScreenPosition , multiAnchor = false ) { assert ( labelState . element . layoutStyle !== undefined ) ; const layoutStyle = labelState . element . layoutStyle ! ; multiAnchor = multiAnchor && layoutStyle . placements !== undefined && layoutStyle . placements . length > 1 ; if ( ! multiAnchor ) { return placePointLabelAtCurrentAnchor ( labelState , screenPosition , scale , textCanvas , env , screenCollisions , outScreenPosition ) ; } else { return placePointLabelChoosingAnchor ( labelState , screenPosition , scale , textCanvas , env , screenCollisions , outScreenPosition ) ; } } note param labelState param screenPosition param scale param textCanvas param env - The  {@link @here/harp-datasource-protocol#Env} *              used to evaluate technique attributes.
 *  param screenCollisions param outScreenPosition returns internal hidden function placePointLabelChoosingAnchor ( labelState , screenPosition , scale , textCanvas , env , screenCollisions , outScreenPosition ) { assert ( labelState . element . layoutStyle !== undefined ) ; const label = labelState . element ; const persistent = labelState . visible ; const lastPlacement = labelState . textPlacement ; const placements = label . layoutStyle ! . placements ; const placementsNum = placements . length ; const matchIdx = placements . findIndex ( p => p . h === lastPlacement . h && p . v === lastPlacement . v ) ; assert ( matchIdx >= 0 ) ; let allInvisible = true ; for ( let i = matchIdx ; i < placementsNum + matchIdx ; ++ i ) { const anchorPlacement = placements [ i % placementsNum ] ; const isLastPlacement = i === matchIdx && persistent ; const placementResult = placePointLabelAtAnchor ( labelState , screenPosition , anchorPlacement , scale , textCanvas , env , screenCollisions , ! isLastPlacement , tmpPlacementPosition ) ; if ( placementResult === PlacementResult . Ok ) { outScreenPosition . copy ( tmpPlacementPosition ) ; return PlacementResult . Ok ; } if ( isLastPlacement ) { outScreenPosition . copy ( tmpPlacementPosition ) ; } allInvisible = allInvisible && placementResult === PlacementResult . Invisible ; } return allInvisible ? PlacementResult . Invisible : PlacementResult . Rejected ; } param labelState param screenPosition param scale param textCanvas param env - The  {@link @here/harp-datasource-protocol#Env} *              used to evaluate technique attributes.
 *  param screenCollisions param outScreenPosition returns internal hidden function placePointLabelAtCurrentAnchor ( labelState , screenPosition , scale , textCanvas , env , screenCollisions , outScreenPosition ) { assert ( labelState . element . layoutStyle !== undefined ) ; const lastPlacement = labelState . textPlacement ; const result = placePointLabelAtAnchor ( labelState , screenPosition , lastPlacement , scale , textCanvas , env , screenCollisions , ! labelState . visible , outScreenPosition ) ; return result ; } param labelState param screenPosition param placement param scale param textCanvas param env - The  {@link @here/harp-datasource-protocol#Env} *              used to evaluate technique attributes.
 *  param screenCollisions param forceInvalidation param outScreenPosition returns internal hidden function placePointLabelAtAnchor ( labelState , screenPosition , placement , scale , textCanvas , env , screenCollisions , forceInvalidation , outScreenPosition ) { const label = labelState . element ; assert ( label . glyphs !== undefined ) ; assert ( label . layoutStyle !== undefined ) ; const measureText = ! label . bounds || forceInvalidation ; const labelBounds = measureText ? tmpBounds : label . bounds ! ; if ( measureText ) { applyTextPlacement ( textCanvas , placement ) ; tmpMeasurementParams . outputCharacterBounds = undefined ; tmpMeasurementParams . path = undefined ; tmpMeasurementParams . pathOverflow = false ; tmpMeasurementParams . letterCaseArray = label . glyphCaseArray ! ; textCanvas . measureText ( label . glyphs ! , labelBounds , tmpMeasurementParams ) ; } const textOffset = computePointTextOffset ( label , labelBounds , placement , scale , env , tmpTextOffset ) . add ( screenPosition ) ; outScreenPosition . set ( textOffset . x , textOffset . y , labelState . renderDistance ) ; tmpBox . copy ( labelBounds ) . expandByVector ( 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 ) ; if ( ! screenCollisions . isVisible ( tmp2DBox ) ) { return PlacementResult . Invisible ; } if ( measureText ) { tmpBox . getSize ( tmpSize ) ; tmpSize . multiplyScalar ( scale * ( 1 + newPointLabelTextMarginPercent ) ) ; tmp2DBox . set ( tmpCenter . x - tmpSize . x / 2 , tmpCenter . y - tmpSize . y / 2 , tmpSize . x , tmpSize . y ) ; } if ( ! label . textMayOverlap && screenCollisions . isAllocated ( tmp2DBox ) ) { return PlacementResult . Rejected ; } if ( label . textReservesSpace ) { screenCollisions . allocate ( tmp2DBox ) ; } if ( measureText ) { label . textBufferObject = undefined ; label . bounds = label . bounds ? label . bounds . copy ( labelBounds ) : labelBounds . clone ( ) ; } else { applyTextPlacement ( textCanvas , placement ) ; } labelState . textPlacement = placement ; return PlacementResult . Ok ; } param textCanvas param placement function applyTextPlacement ( textCanvas , placement ) { textCanvas . textLayoutStyle . horizontalAlignment = hAlignFromPlacement ( placement . h ) ; textCanvas . textLayoutStyle . verticalAlignment = vAlignFromPlacement ( placement . v ) ; } param labelState param textPath param screenPosition param textCanvas param screenCollisions returns export function placePathLabel ( labelState , textPath , screenPosition , textCanvas , screenCollisions ) { tmpMeasurementParams . path = textPath ; tmpMeasurementParams . outputCharacterBounds = tmpBoxes ; tmpMeasurementParams . letterCaseArray = labelState . element . glyphCaseArray ! ; if ( ! textCanvas . measureText ( labelState . element . glyphs ! , tmpBox , tmpMeasurementParams ) ) { return PlacementResult . Rejected ; } 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 ; } 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 CollisionBox ( glyphBox ) ; tmpCollisionBoxes [ i ] = collisionBox ; } else { collisionBox . copy ( glyphBox ) ; } if ( checkGlyphCollision && screenCollisions . intersectsDetails ( collisionBox , candidateBoxes ! ) ) { return PlacementResult . Rejected ; } } if ( labelState . element . textReservesSpace ) { const collisionBox = new DetailedCollisionBox ( tmpCollisionBox , tmpCollisionBoxes . slice ( ) ) ; tmpCollisionBoxes . length = 0 ; screenCollisions . allocate ( collisionBox ) ; } return PlacementResult . Ok ; } param textElement param screenProjector param outScreenPoints returns export function isPathLabelTooSmall ( textElement , screenProjector , outScreenPoints ) { assert ( textElement . type === TextElementType . PathLabel ) ; outScreenPoints . length = 0 ; let anyPointVisible = false ; for ( const pt of textElement . points as THREE . Vector3 [ ] ) { const screenPoint = anyPointVisible ? screenProjector . project ( pt , tmpScreenPosition ) : screenProjector . projectToScreen ( pt , tmpScreenPosition ) ; if ( screenPoint === undefined ) { continue ; } anyPointVisible = true ; outScreenPoints . push ( tmpScreenPosition . clone ( ) ) ; } if ( ! anyPointVisible ) { return true ; } 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 ; } const tmpOrientedBox = new OrientedBox3 ( ) ; param poiLabel param projection param env param outWorldPosition returns export function getWorldPosition ( poiLabel , projection , env , outWorldPosition ) { const worldOffsetShiftValue = getPropertyValue ( ( poiLabel . poiInfo ?. technique as PoiTechnique ) ?. worldOffset , env ) ; 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 ) ; offsetDirectionVector . applyAxisAngle ( tmpOrientedBox . zAxis . negate ( ) , offsetDirectionRad ) ; outWorldPosition . addScaledVector ( tmpOrientedBox . yAxis , worldOffsetShiftValue ) ; } return outWorldPosition ; }