import { FontCatalogConfig , LineMarkerTechnique , TextStyleDefinition } from "@here/harp-datasource-protocol" ; import { TileKey , Vector3Like } from "@here/harp-geoutils" ; import { AdditionParameters , DEFAULT_TEXT_CANVAS_LAYER , FontCatalog , TextBufferAdditionParameters , TextBufferCreationParameters , TextCanvas , TextLayoutStyle , TextRenderStyle } from "@here/harp-text-canvas" ; import { assert , LoggerManager , LogLevel , Math2D , MathUtils , PerformanceTimer } from "@here/harp-utils" ; import * as THREE from "three" ; import { DataSource } from "../DataSource" ; import { debugContext } from "../DebugContext" ; import { overlayTextElement } from "../geometry/overlayOnElevation" ; import { MapViewImageCache } from "../image/MapViewImageCache" ; import { PickObjectType } from "../PickHandler" ; import { PickListener } from "../PickListener" ; import { PoiManager } from "../poi/PoiManager" ; import { PoiLayer , PoiRenderer } from "../poi/PoiRenderer" ; import { IBox , LineWithBound , ScreenCollisions , ScreenCollisionsDebug } from "../ScreenCollisions" ; import { ScreenProjector } from "../ScreenProjector" ; import { Tile } from "../Tile" ; import { MapViewUtils } from "../Utils" ; import { DataSourceTileList } from "../VisibleTileSet" ; import { loadFontCatalog } from "./FontCatalogLoader" ; import { checkReadyForPlacement , computeViewDistance , getMaxViewDistance , getWorldPosition , isPathLabelTooSmall , placeIcon , PlacementResult , placePathLabel , placePointLabel , pointToPlaneDistance , PrePlacementResult } from "./Placement" ; import { PlacementStats } from "./PlacementStats" ; import { RenderState } from "./RenderState" ; import { SimpleLineCurve , SimplePath } from "./SimplePath" ; import { TextCanvasFactory } from "./TextCanvasFactory" ; import { LoadingState , TextElement , TextPickResult } from "./TextElement" ; import { TextElementGroup } from "./TextElementGroup" ; import { TextElementFilter , TextElementGroupState } from "./TextElementGroupState" ; import { initializeDefaultOptions , TextElementsRendererOptions } from "./TextElementsRendererOptions" ; import { TextElementState } from "./TextElementState" ; import { TextElementStateCache } from "./TextElementStateCache" ; import { TextElementType } from "./TextElementType" ; import { TextElementStyle , TextStyleCache } from "./TextStyleCache" ; import { UpdateStats } from "./UpdateStats" ; import { ViewState } from "./ViewState" ; interface RenderParams { numRenderedTextElements : number ; fadeAnimationRunning : boolean ; time : number ; } enum Pass { PersistentLabels , NewLabels } export type TextCanvases = Map < string , TextCanvas | undefined > ; export const DEFAULT_FONT_CATALOG_NAME = "default" ; internal export const DEFAULT_TEXT_DISTANCE_SCALE = 0.5 ; /**
 * Maximum number of recommended labels. If more labels are encountered, the "overloaded" mode is
 * set, which modifies the behavior of label placement and rendering, trying to keep delivering an
 * interactive performance. The overloaded mode should not be activated if the  MapView is
 * rendering a static image (camera not moving and no animation running).
  const OVERLOAD_LABEL_LIMIT = 20000 ; /**
 * If "overloaded" is `true`:
 *
 * Default number of labels/POIs updated in a frame. They are rendered only if they fit. If the
 * camera is not moving, it is ignored. See [[TextElementsRenderer.isUpdatePending]].
 */ const OVERLOAD_UPDATED_LABEL_LIMIT = 100 ; /**
 * If "overloaded" is `true`:
 *
 * Maximum time in milliseconds available for placement. If value is <= 0, or if the camera is not
 * moving, it is ignored. See [[TextElementsRenderer.isUpdatePending]].
 */ const OVERLOAD_UPDATE_TIME_LIMIT = 5 ; /**
 * If "overloaded" is `true`:
 *
 * Maximum time in milliseconds available for rendering. If value is <= 0, or if the camera is not
 * moving, it is ignored. See [[TextElementsRenderer.isUpdatePending]].
 */ const OVERLOAD_PLACE_TIME_LIMIT = 10 ; const logger = LoggerManager . instance . create ( "TextElementsRenderer" , { level : LogLevel . Log } ) ; const PRINT_LABEL_DEBUG_INFO = false ; const updateStats = PRINT_LABEL_DEBUG_INFO ? new UpdateStats ( logger ) : undefined ; const placementStats = PRINT_LABEL_DEBUG_INFO ? new PlacementStats ( logger ) : undefined ; const tempPosition = new THREE . Vector3 ( ) ; const tempScreenPosition = new THREE . Vector2 ( ) ; const tempScreenPoints = [ ] ; const tempPoiScreenPosition = new THREE . Vector2 ( ) ; const tmpTextBufferCreationParams = { } ; const tmpAdditionParams = { } ; const tmpBufferAdditionParams = { } ; const cache = { vector2 : [ new THREE . Vector2 ( ) ] } ; class TileTextElements { constructor ( readonly tile , readonly group ) { } } class TextElementLists { constructor ( readonly lists ) { } get priority ( ) { assert ( this . lists . length > 0 ) ; return this . lists [ 0 ] . group . priority ; } /**
     * Sum up the number of elements in all lists.
     */ count ( ) { let n = 0 ; for ( const list of this . lists ) { n += list . group . elements . length ; } return n ; } } function checkIfTextElementsChanged ( dataSourceTileList ) { let textElementsChanged = false ; dataSourceTileList . forEach ( ( { renderedTiles } ) => { renderedTiles . forEach ( tile => { if ( tile . textElementsChanged ) { tile . textElementsChanged = false ; textElementsChanged = true ; } } ) ; } ) ; return textElementsChanged ; } function hasTextElements ( dataSourceTileList ) { for ( let i = 0 ; i < dataSourceTileList . length ; i ++ ) { for ( const [ _key , value ] of dataSourceTileList [ i ] . renderedTiles ) { if ( value . hasTextElements ( ) ) { return true ; } } } return false ; } function addTextToCanvas ( textElement , canvas , screenPosition , path ? , pathOverflow ? ) { tmpAdditionParams . path = path ; tmpAdditionParams . pathOverflow = pathOverflow ; tmpAdditionParams . layer = textElement . renderOrder ; tmpAdditionParams . letterCaseArray = textElement . glyphCaseArray ; tmpAdditionParams . pickingData = textElement . userData ? textElement : undefined ; canvas . addText ( textElement . glyphs ! , screenPosition , tmpAdditionParams ) ; } function addTextBufferToCanvas ( textElementState , canvas , screenPosition , fadeFactor , scaleFactor ) { const textElement = textElementState . element ; const textRenderState = textElementState . textRenderState ; const opacity = textRenderState ! . opacity * fadeFactor * textElement . renderStyle ! . opacity ; if ( opacity === 0 ) { return false ; } tmpTextBufferCreationParams . letterCaseArray = textElement . glyphCaseArray ; if ( textElement . textBufferObject === undefined ) { textElement . textBufferObject = canvas . createTextBufferObject ( textElement . glyphs ! , tmpTextBufferCreationParams ) ; } const backgroundIsVisible = textElement . renderStyle ! . backgroundOpacity > 0 && canvas . textRenderStyle . fontSize . backgroundSize > 0 ; tmpBufferAdditionParams . layer = textElement . renderOrder ; tmpBufferAdditionParams . position = screenPosition ; tmpBufferAdditionParams . scale = scaleFactor ; tmpBufferAdditionParams . opacity = opacity ; tmpBufferAdditionParams . backgroundOpacity = backgroundIsVisible ? tmpBufferAdditionParams . opacity * textElement . renderStyle ! . backgroundOpacity : 0.0 ; tmpBufferAdditionParams . pickingData = textElement . userData ? textElement : undefined ; canvas . addTextBufferObject ( textElement . textBufferObject ! , tmpBufferAdditionParams ) ; return true ; } function shouldRenderPointText ( labelState , viewState , options ) { const textRenderState = labelState . textRenderState ; const label = labelState . element ; const poiInfo = label . poiInfo ; assert ( label . type !== TextElementType . PathLabel ) ; const hasText = textRenderState !== undefined && label . text !== "" ; if ( ! hasText ) { return false ; } const visibleInZoomLevel = poiInfo === undefined || MathUtils . isClamped ( viewState . zoomLevel , poiInfo . textMinZoomLevel , poiInfo . textMaxZoomLevel ) ; if ( ! visibleInZoomLevel ) { return false ; } const poiTextMaxDistance = getMaxViewDistance ( viewState , options . maxDistanceRatioForPoiLabels ! ) ; const visibleAtDistance = label . ignoreDistance === true || labelState . viewDistance === undefined || ( labelState . viewDistance < poiTextMaxDistance && labelState . viewDistance > 0 ) ; if ( ! visibleAtDistance ) { return false ; } return ! poiInfo || poiInfo . isValid === true || poiInfo . iconIsOptional === true ; } function shouldRenderPoiText ( labelState , viewState ) { const poiInfo = labelState . element . poiInfo ; return ( ! viewState . cameraIsMoving || poiInfo === undefined || poiInfo . renderTextDuringMovements === true ) ; } function isPlacementTimeExceeded ( startTime ) { if ( startTime === undefined || OVERLOAD_PLACE_TIME_LIMIT <= 0 ) { return false ; } const endTime = PerformanceTimer . now ( ) ; const elapsedTime = endTime - startTime ; if ( elapsedTime > OVERLOAD_PLACE_TIME_LIMIT ) { logger . debug ( "Placement time limit exceeded." ) ; return true ; } return false ; } function createDefaultFontCatalogConfig ( defaultFontCatalogUrl ) { return { name : DEFAULT_FONT_CATALOG_NAME , url : defaultFontCatalogUrl } ; } /**
 *
 * Internal class to manage all text rendering.
 */ export class TextElementsRenderer { private m_loadPromisesCount : number = 0 ; private m_loadPromise : Promise < any > | undefined ; private readonly m_options : TextElementsRendererOptions ; private readonly m_textCanvases : TextCanvases = new Map ( ) ; private m_overlayTextElements ? : TextElement [ ] ; private m_debugGlyphTextureCacheMesh ? : THREE . Mesh ; private m_debugGlyphTextureCacheWireMesh ? : THREE . LineSegments ; private readonly m_tmpVector = new THREE . Vector2 ( ) ; private readonly m_tmpVector3 = new THREE . Vector3 ( ) ; private readonly m_cameraLookAt = new THREE . Vector3 ( ) ; private m_overloaded : boolean = false ; private m_cacheInvalidated : boolean = false ; private m_addNewLabels : boolean = true ; private m_forceNewLabelsPass : boolean = false ; private readonly m_textElementStateCache : TextElementStateCache = new TextElementStateCache ( ) ; private readonly m_camera = new THREE . OrthographicCamera ( - 1 , 1 , 1 , - 1 ) ; private m_defaultFontCatalogConfig : FontCatalogConfig | undefined ; private m_poiRenderer : PoiRenderer ; private readonly m_textStyleCache : TextStyleCache = new TextStyleCache ( ) ; private readonly m_screenCollisions : ScreenCollisions | ScreenCollisionsDebug = new ScreenCollisions ( ) ; private readonly m_textCanvasFactory : TextCanvasFactory ; /**
     * indicates if the TextElementsRenderer is still updating, includes fading, elevations etc
     */ private m_isUpdatePending : boolean = false ; /**
     * Create the `TextElementsRenderer` which selects which labels should be placed on screen as
     * a preprocessing step, which is not done every frame, and also renders the placed
     *  TextElement s every frame.
     *
     *  param m_viewState param m_screenProjector param m_poiManager param m_renderer param m_imageCaches param options param textCanvasFactory param poiRenderer param screenCollisions constructor ( private readonly m_viewState , private readonly m_screenProjector , private readonly m_poiManager , private m_renderer , private readonly m_imageCaches , options , textCanvasFactory ? , poiRenderer ? , screenCollisions ? ) { this . m_options = { ... options } ; initializeDefaultOptions ( this . m_options ) ; if ( screenCollisions ) { this . m_screenCollisions = screenCollisions ; } else if ( this . m_options . collisionDebugCanvas !== undefined && this . m_options . collisionDebugCanvas !== null ) { this . m_screenCollisions = new ScreenCollisionsDebug ( this . m_options . collisionDebugCanvas ) ; } this . m_textCanvasFactory = textCanvasFactory ?? new TextCanvasFactory ( this . m_renderer ) ; this . m_textCanvasFactory . setGlyphCountLimits ( this . m_options . minNumGlyphs ! , this . m_options . maxNumGlyphs ! ) ; this . m_poiRenderer = poiRenderer ?? new PoiRenderer ( this . m_renderer , this . m_poiManager , this . m_imageCaches ) ; this . initializeCamera ( ) ; this . initializeDefaultFontCatalog ( ) ; this . m_textStyleCache . updateTextCanvases ( this . m_textCanvases ) ; } /**
     * Disable all fading animations (for debugging and performance measurement). Defaults to
     * `false`.
     */ set disableFading ( disable ) { this . m_options . disableFading = disable ; } get disableFading ( ) : boolean { return this . m_options . disableFading === true ; } get styleCache ( ) { return this . m_textStyleCache ; } get delayLabelsUntilMovementFinished ( ) : boolean { return this . m_options . delayLabelsUntilMovementFinished === true ; } set delayLabelsUntilMovementFinished ( delay ) { this . m_options . delayLabelsUntilMovementFinished = delay ; } /**
     * If `true`, a replacement glyph ("?") is rendered for every missing glyph.
     */ get showReplacementGlyphs ( ) { return this . m_options . showReplacementGlyphs === true ; } /**
     * If `true`, a replacement glyph ("?") is rendered for every missing glyph.
     */ set showReplacementGlyphs ( value ) { this . m_options . showReplacementGlyphs = value ; this . m_textCanvases . forEach ( textCanvas => { if ( textCanvas ?. fontCatalog ) { textCanvas . fontCatalog . showReplacementGlyphs = value ; } } ) ; } restoreRenderers ( renderer ) { this . m_renderer = renderer ; this . m_poiRenderer = new PoiRenderer ( this . m_renderer , this . m_poiManager , this . m_imageCaches ) ; } /**
     * Updates the FontCatalogs used by this  TextElementsRenderer .
     *
     *  param fontCatalogs - The new list of  FontCatalogConfig s
      async updateFontCatalogs ( fontCatalogs ? ) { if ( this . m_defaultFontCatalogConfig ) { if ( ! fontCatalogs || fontCatalogs . findIndex ( config => { return config . name === DEFAULT_FONT_CATALOG_NAME ; } ) === - 1 ) { if ( ! fontCatalogs ) { fontCatalogs = [ ] ; } fontCatalogs . unshift ( this . m_defaultFontCatalogConfig ) ; } else { if ( this . m_textCanvases . has ( DEFAULT_FONT_CATALOG_NAME ) ) { this . m_textCanvases . delete ( DEFAULT_FONT_CATALOG_NAME ) ; } } } if ( fontCatalogs && fontCatalogs . length > 0 ) { for ( const [ name ] of this . m_textCanvases ) { if ( fontCatalogs . findIndex ( catalog => { return catalog . name === name ; } ) < 0 ) { this . m_textCanvases . delete ( name ) ; } } for ( const fontCatalog of fontCatalogs ) { await this . addTextCanvas ( fontCatalog ) ; } } else { this . m_textCanvases . clear ( ) ; } this . m_textStyleCache . updateTextCanvases ( this . m_textCanvases ) ; } async updateTextStyles ( textStyles ? , defaultTextStyle ? ) { this . m_textStyleCache . updateTextStyles ( textStyles , defaultTextStyle ) ; await this . waitLoaded ( ) ; this . m_textStyleCache . updateTextCanvases ( this . m_textCanvases ) ; this . invalidateCache ( ) ; } param camera renderText ( farPlane ) { this . m_camera . far = farPlane ; this . updateGlyphDebugMesh ( ) ; let previousLayer ; this . m_poiRenderer . update ( ) ; for ( const poiLayer of this . m_poiRenderer . layers ) { for ( const [ , textCanvas ] of this . m_textCanvases ) { textCanvas ?. render ( this . m_camera , previousLayer ?. id , poiLayer . id , undefined , false ) ; } this . m_poiRenderer . render ( this . m_camera , poiLayer ) ; previousLayer = poiLayer ; } for ( const [ , textCanvas ] of this . m_textCanvases ) { textCanvas ?. render ( this . m_camera , previousLayer ?. id , undefined , undefined , false ) ; } } /**
     * Forces update of text elements in the next call to [[placeText]].
     */ invalidateCache ( ) { this . m_cacheInvalidated = true ; } /**
     * Notify `TextElementsRenderer` that the camera has started a movement.
     */ movementStarted ( ) { if ( this . delayLabelsUntilMovementFinished ) { this . m_addNewLabels = false ; } } /**
     * Notify `TextElementsRenderer` that the camera has finished its movement.
     */ movementFinished ( ) { this . invalidateCache ( ) ; if ( this . delayLabelsUntilMovementFinished ) { this . m_addNewLabels = true ; } } /**
     * Is `true` if number of  TextElement s in visible tiles is larger than the recommended
     * number `OVERLOAD_LABEL_LIMIT`.
      get overloaded ( ) : boolean { return this . m_overloaded ; } param dataSourceTileList param time placeText ( dataSourceTileList , time ) { const tileTextElementsChanged = checkIfTextElementsChanged ( dataSourceTileList ) ; const textElementsAvailable = this . hasOverlayText ( ) || tileTextElementsChanged || hasTextElements ( dataSourceTileList ) ; this . m_isUpdatePending = false ; if ( ! textElementsAvailable && ! this . m_cacheInvalidated && ! this . m_viewState . renderedTilesChanged ) { return ; } const updateTextElements = this . m_cacheInvalidated || tileTextElementsChanged || this . m_viewState . renderedTilesChanged ; const findReplacements = updateTextElements && this . m_addNewLabels ; if ( findReplacements ) { this . m_textElementStateCache . clearVisited ( ) ; this . updateTextElements ( dataSourceTileList ) ; } const anyTextGroupEvicted = this . m_textElementStateCache . update ( time , this . m_options . disableFading ! , findReplacements , this . m_viewState . zoomLevel ) ; this . reset ( ) ; if ( this . m_addNewLabels ) { this . prepopulateScreenWithBlockingElements ( dataSourceTileList ) ; } const placeNewTextElements = ( updateTextElements || anyTextGroupEvicted ) && this . m_addNewLabels ; this . placeTextElements ( time , placeNewTextElements ) ; this . placeOverlayTextElements ( ) ; } param textElements - Array of  TextElement to be added.
      addOverlayText ( textElements ) { if ( textElements . length === 0 ) { return ; } this . m_overlayTextElements = this . m_overlayTextElements === undefined ? textElements . slice ( ) : this . m_overlayTextElements . concat ( textElements ) ; } param textElements - Array of  TextElement to be added.
      clearOverlayText ( ) { this . m_overlayTextElements = [ ] ; } returns hasOverlayText ( ) { return this . m_overlayTextElements !== undefined && this . m_overlayTextElements . length > 0 ; } get overlayText ( ) : TextElement [ ] | undefined { return this . m_overlayTextElements ; } /**
     * Fill the picking results for the pixel with the given screen coordinate. If multiple
     *  TextElement s are found, the order of the results is unspecified.
     *
     * Note:  TextElement s with identical `featureId` or
     * identical `userData` will only appear
     * once in the list `pickResults`.
     *
     *  param screenPosition param pickResults pickTextElements ( screenPosition , pickListener ) { const pickHandler = ( pickData , pickObjectType ) => { if ( pickData === undefined ) { return ; } const textElement = pickData as TextElement ; const pickResult = { type : pickObjectType , point : screenPosition , distance : 0 , renderOrder : textElement . renderOrder , featureId : textElement . featureId , userData : textElement . userData , dataSourceName : textElement . dataSourceName , dataSourceOrder : textElement . dataSourceOrder , text : textElement . text } ; pickListener . addResult ( pickResult ) ; } ; for ( const [ , textCanvas ] of this . m_textCanvases ) { textCanvas ?. pickText ( screenPosition , ( pickData ) => { pickHandler ( pickData , PickObjectType . Text ) ; } ) ; } this . m_poiRenderer . pickTextElements ( screenPosition , ( pickData ) => { pickHandler ( pickData , PickObjectType . Icon ) ; } ) ; } /**
     * `true` if any resource used by any `FontCatalog` is still loading.
     */ get loading ( ) : boolean { return this . m_loadPromisesCount > 0 ; } /**
     * `true` if TextElements are not placed finally but are still updating, including fading or
     * waiting for elevation.
     */ get isUpdatePending ( ) : boolean { return this . m_isUpdatePending ; } /**
     * Waits till all pending resources from any `FontCatalog` are loaded.
     */ async waitLoaded ( ) { if ( this . m_loadPromise !== undefined ) { return await this . m_loadPromise ; } } remarks All  TextElement s will fade in
     * after that as if they have just been added.
      clearRenderStates ( ) { this . m_textElementStateCache . clear ( ) ; } returns getMemoryUsage ( ) { const memoryUsage = { heapSize : 0 , gpuSize : 0 } ; for ( const [ , textCanvas ] of this . m_textCanvases ) { textCanvas ?. getMemoryUsage ( memoryUsage ) ; } this . m_poiRenderer . getMemoryUsage ( memoryUsage ) ; return memoryUsage ; } private async addDefaultTextCanvas ( ) { if ( this . m_textCanvases . has ( DEFAULT_FONT_CATALOG_NAME ) || ! this . m_defaultFontCatalogConfig ) { return ; } await this . addTextCanvas ( this . m_defaultFontCatalogConfig ) ; this . m_textStyleCache . updateTextCanvases ( this . m_textCanvases ) ; } /**
     * Reset internal state at the beginning of a frame.
     */ private reset ( ) { this . m_cameraLookAt . copy ( this . m_viewState . lookAtVector ) ; this . m_screenCollisions . reset ( ) ; for ( const [ , textCanvas ] of this . m_textCanvases ) { textCanvas ?. clear ( ) ; } this . m_poiRenderer . reset ( ) ; } note param dataSourceTileList private prepopulateScreenWithBlockingElements ( dataSourceTileList ) { const boxes = [ ] ; dataSourceTileList . forEach ( renderListEntry => { const startLinePointProj = new THREE . Vector3 ( ) ; const endLinePointProj = new THREE . Vector3 ( ) ; for ( const tile of renderListEntry . renderedTiles . values ( ) ) { for ( const pathBlockingElement of tile . blockingElements ) { if ( pathBlockingElement . points . length < 2 ) { continue ; } this . m_screenProjector . project3 ( pathBlockingElement . points [ 0 ] , startLinePointProj ) ; for ( let i = 1 ; i < pathBlockingElement . points . length ; i ++ ) { this . m_screenProjector . project3 ( pathBlockingElement . points [ i ] , endLinePointProj ) ; const line = pathBlockingElement . screenSpaceLines [ i - 1 ] ; line . start . copy ( startLinePointProj ) ; line . end . copy ( endLinePointProj ) ; const lineWithBound = { minX : Math . min ( startLinePointProj . x , endLinePointProj . x ) , maxX : Math . max ( startLinePointProj . x , endLinePointProj . x ) , minY : Math . min ( startLinePointProj . y , endLinePointProj . y ) , maxY : Math . max ( startLinePointProj . y , endLinePointProj . y ) , line } ; boxes . push ( lineWithBound ) ; startLinePointProj . copy ( endLinePointProj ) ; } } } } ) ; this . m_screenCollisions . allocateIBoxes ( boxes ) ; } returns private placeTextElementGroup ( groupState , renderParams , maxNumPlacedLabels , pass ) { assert ( groupState . visited ) ; const shieldGroups = [ ] ; const hiddenKinds = this . m_viewState . hiddenGeometryKinds ; const projection = this . m_viewState . projection ; const elevationProvider = this . m_viewState . elevationProvider ; const elevationMap = elevationProvider ?. getDisplacementMap ( groupState . tileKey ) ; for ( const textElementState of groupState . textElementStates ) { if ( pass === Pass . PersistentLabels ) { if ( placementStats ) { ++ placementStats . total ; } } else if ( maxNumPlacedLabels !== undefined && renderParams . numRenderedTextElements >= maxNumPlacedLabels ) { logger . debug ( "Placement label limit exceeded." ) ; return false ; } if ( ! textElementState . initialized ) { if ( placementStats ) { ++ placementStats . uninitialized ; } continue ; } if ( textElementState . viewDistance === undefined || textElementState . viewDistance < 0 ) { if ( placementStats ) { ++ placementStats . tooFar ; } continue ; } const elementVisible = textElementState . visible ; if ( ( pass === Pass . PersistentLabels && ! elementVisible ) || ( pass === Pass . NewLabels && elementVisible ) ) { continue ; } const textElement = textElementState . element ; const textElementStyle = this . m_textStyleCache . getTextElementStyle ( textElement . style ) ; const textCanvas = textElementStyle . textCanvas ; if ( hiddenKinds !== undefined && textElement . kind !== undefined && hiddenKinds . hasOrIntersects ( textElement . kind ) ) { continue ; } if ( elevationProvider !== undefined && ! textElement . elevated ) { if ( ! elevationMap ) { this . m_isUpdatePending = true ; this . m_forceNewLabelsPass = true ; continue ; } overlayTextElement ( textElement , elevationProvider , elevationMap , projection ) ; } const elementType = textElement . type ; const isPathLabel = elementType === TextElementType . PathLabel ; if ( isPathLabel ) { if ( isPathLabelTooSmall ( textElement , this . m_screenProjector , tempScreenPoints ) ) { if ( placementStats ) { placementStats . numNotVisible ++ ; } if ( textElement . dbgPathTooSmall === true ) { if ( placementStats ) { placementStats . numPathTooSmall ++ ; } } textElementState . reset ( ) ; continue ; } } const forceNewPassOnLoaded = true ; if ( textCanvas ) { if ( ! this . initializeGlyphs ( textElement , textElementStyle , forceNewPassOnLoaded ) ) { continue ; } const layer = textCanvas . getLayer ( textElement . renderOrder ?? DEFAULT_TEXT_CANVAS_LAYER ) ; if ( layer !== undefined ) { if ( layer . storage . drawCount + textElement . glyphs ! . length > layer . storage . capacity ) { if ( placementStats ) { ++ placementStats . numCannotAdd ; } logger . warn ( "layer glyph storage capacity exceeded." ) ; continue ; } } textCanvas . textRenderStyle = textElement . renderStyle ! ; textCanvas . textLayoutStyle = textElement . layoutStyle ! ; } switch ( elementType ) { case TextElementType . PoiLabel : this . addPoiLabel ( textElementState , textCanvas , renderParams ) ; break ; case TextElementType . LineMarker : this . addLineMarkerLabel ( textElementState , shieldGroups , textCanvas , renderParams ) ; break ; case TextElementType . PathLabel : if ( textCanvas ) { this . addPathLabel ( textElementState , tempScreenPoints , textCanvas , renderParams ) ; } } } return true ; } private initializeGlyphs ( textElement , textElementStyle , forceNewPassOnLoaded ) { if ( textElement . loadingState === LoadingState . Initialized ) { return textElement . glyphs !== undefined ; } assert ( textElementStyle . textCanvas !== undefined ) ; const textCanvas = textElementStyle . textCanvas ! ; if ( textElement . loadingState === undefined ) { textElement . loadingState = LoadingState . Requested ; if ( textElement . renderStyle === undefined ) { textElement . renderStyle = new TextRenderStyle ( { ... textElementStyle . renderParams , ... textElement . renderParams } ) ; } if ( textElement . layoutStyle === undefined ) { textElement . layoutStyle = new TextLayoutStyle ( { ... textElementStyle . layoutParams , ... textElement . layoutParams } ) ; } if ( textElement . text === "" ) { textElement . loadingState = LoadingState . Loaded ; } else { const newLoadPromise = textCanvas . fontCatalog . loadCharset ( textElement . text , textElement . renderStyle ) . then ( ( ) => { -- this . m_loadPromisesCount ; textElement . loadingState = LoadingState . Loaded ; this . m_isUpdatePending = true ; this . m_forceNewLabelsPass = this . m_forceNewLabelsPass || forceNewPassOnLoaded ; } ) ; if ( this . m_loadPromisesCount === 0 ) { this . m_loadPromise = undefined ; } ++ this . m_loadPromisesCount ; this . m_loadPromise = this . m_loadPromise === undefined ? newLoadPromise : Promise . all ( [ this . m_loadPromise , newLoadPromise ] ) ; } } if ( textElement . loadingState === LoadingState . Loaded ) { textCanvas . textRenderStyle = textElement . renderStyle ! ; textCanvas . textLayoutStyle = textElement . layoutStyle ! ; textElement . glyphCaseArray = [ ] ; textElement . bounds = undefined ; textElement . glyphs = textCanvas . fontCatalog . getGlyphs ( textElement . text , textCanvas . textRenderStyle , textElement . glyphCaseArray ) ; textElement . loadingState = LoadingState . Initialized ; } return textElement . glyphs !== undefined ; } private initializeCamera ( ) { this . m_camera . position . z = 1 ; this . m_camera . near = 0 ; } updateCamera ( ) { const { width , height } = this . m_renderer . getSize ( cache . vector2 [ 0 ] ) ; this . m_camera . left = width / - 2 ; this . m_camera . right = width / 2 ; this . m_camera . bottom = height / - 2 ; this . m_camera . top = height / 2 ; this . m_camera . updateProjectionMatrix ( ) ; this . m_camera . updateMatrixWorld ( false ) ; this . m_screenCollisions . update ( width , height ) ; } private initializeDefaultFontCatalog ( ) { if ( this . m_options . fontCatalog ) { this . m_defaultFontCatalogConfig = createDefaultFontCatalogConfig ( this . m_options . fontCatalog ) ; this . addDefaultTextCanvas ( ) ; } } private async addTextCanvas ( fontCatalogConfig ) { const catalogCallback = ( name , catalog ) => { if ( this . m_textCanvases . has ( name ) ) { const loadedTextCanvas = this . m_textCanvasFactory . createTextCanvas ( catalog , name ) ; catalog . showReplacementGlyphs = this . showReplacementGlyphs ; this . m_textCanvases . set ( name , loadedTextCanvas ) ; } } ; const errorCallback = ( ) => { this . m_textCanvases . delete ( fontCatalogConfig . name ) ; } ; if ( this . m_textCanvases . has ( fontCatalogConfig . name ) ) { return Promise . resolve ( ) ; } else { this . m_textCanvases . set ( fontCatalogConfig . name , undefined ) ; const newLoadPromise = loadFontCatalog ( fontCatalogConfig , catalogCallback , errorCallback ) . then ( ( ) => { -- this . m_loadPromisesCount ; } ) . catch ( error => { logger . info ( "rendering without font catalog, only icons possible" , error ) ; -- this . m_loadPromisesCount ; } ) ; if ( this . m_loadPromisesCount === 0 ) { this . m_loadPromise = undefined ; } ++ this . m_loadPromisesCount ; this . m_loadPromise = this . m_loadPromise === undefined ? newLoadPromise : Promise . all ( [ this . m_loadPromise , newLoadPromise ] ) ; return newLoadPromise ; } } private updateGlyphDebugMesh ( ) { const debugGlyphs = debugContext . getValue ( "DEBUG_GLYPHS" ) ; if ( debugGlyphs === undefined ) { return ; } if ( debugGlyphs && this . m_debugGlyphTextureCacheMesh === undefined ) { this . initializeGlyphDebugMesh ( ) ; } assert ( this . m_debugGlyphTextureCacheMesh !== undefined ) ; assert ( this . m_debugGlyphTextureCacheWireMesh !== undefined ) ; this . m_debugGlyphTextureCacheMesh ! . visible = debugGlyphs ; this . m_debugGlyphTextureCacheWireMesh ! . visible = debugGlyphs ; } private initializeGlyphDebugMesh ( ) { if ( this . m_textCanvases . size === 0 ) { return ; } const defaultTextCanvas = this . m_textCanvases . values ( ) . next ( ) . value ; const defaultFontCatalog = defaultTextCanvas . fontCatalog ; const planeGeometry = new THREE . PlaneGeometry ( defaultFontCatalog . textureSize . width / 2.5 , defaultFontCatalog . textureSize . height / 2.5 , defaultFontCatalog . textureSize . width / defaultFontCatalog . maxWidth , defaultFontCatalog . textureSize . height / defaultFontCatalog . maxHeight ) ; const material = new THREE . MeshBasicMaterial ( { transparent : true , depthWrite : false , depthTest : false , map : defaultFontCatalog . texture } ) ; this . m_debugGlyphTextureCacheMesh = new THREE . Mesh ( planeGeometry , material ) ; this . m_debugGlyphTextureCacheMesh . renderOrder = 10000 ; this . m_debugGlyphTextureCacheMesh . visible = false ; this . m_debugGlyphTextureCacheMesh . name = "glyphDebug" ; const wireframe = new THREE . WireframeGeometry ( planeGeometry ) ; const wireframeMaterial = new THREE . LineBasicMaterial ( { transparent : true , color : 0x999999 , depthWrite : false , depthTest : false } ) ; this . m_debugGlyphTextureCacheWireMesh = new THREE . LineSegments ( wireframe , wireframeMaterial ) ; this . m_debugGlyphTextureCacheWireMesh . renderOrder = 9999 ; this . m_debugGlyphTextureCacheWireMesh . visible = false ; this . m_debugGlyphTextureCacheWireMesh . name = "glyphDebug" ; defaultTextCanvas . getLayer ( DEFAULT_TEXT_CANVAS_LAYER ) ! . storage . scene . add ( this . m_debugGlyphTextureCacheMesh , this . m_debugGlyphTextureCacheWireMesh ) ; } remarks The update of  TextElement s is a time consuming process,
     * and cannot be done every frame, but should only
     * be done when the camera moved (a lot) of whenever the set of visible tiles change.
     *
     * The actually rendered  TextElement s are stored internally
     * until the next update is done
     * to speed up rendering when no camera movement was detected.
     *  param dataSourceTileList private updateTextElements ( dataSourceTileList ) { if ( updateStats ) { updateStats . clear ( ) ; } this . m_textElementStateCache . clearTextCache ( ) ; this . m_cacheInvalidated = false ; this . checkIfOverloaded ( dataSourceTileList ) ; const updateStartTime = this . overloaded && this . m_viewState . isDynamic ? PerformanceTimer . now ( ) : undefined ; dataSourceTileList . forEach ( tileList => { this . updateTextElementsFromSource ( tileList . dataSource , tileList . storageLevel , Array . from ( tileList . renderedTiles . values ( ) ) , updateStartTime ) ; } ) ; if ( updateStats ) { updateStats . log ( ) ; } } private updateTextElementsFromSource ( tileDataSource , storageLevel , visibleTiles , updateStartTime ) { if ( updateStats ) { updateStats . tiles += visibleTiles . length ; } const sortedTiles = visibleTiles ; sortedTiles . sort ( ( a , b ) => { return a . tileKey . mortonCode ( ) - b . tileKey . mortonCode ( ) ; } ) ; const sortedGroups = [ ] ; this . createSortedGroupsForSorting ( tileDataSource , storageLevel , sortedTiles , sortedGroups ) ; let numTextElementsUpdated = 0 ; for ( const textElementLists of sortedGroups ) { this . selectTextElementsToUpdateByDistance ( textElementLists ) ; if ( updateStartTime !== undefined ) { if ( OVERLOAD_UPDATE_TIME_LIMIT > 0 ) { const endTime = PerformanceTimer . now ( ) ; const elapsedTime = endTime - updateStartTime ; if ( elapsedTime > OVERLOAD_UPDATE_TIME_LIMIT ) { logger . debug ( "Update time limit exceeded." ) ; break ; } } numTextElementsUpdated += textElementLists . count ( ) ; if ( numTextElementsUpdated >= OVERLOAD_UPDATED_LABEL_LIMIT ) { logger . debug ( "Update label limit exceeded." ) ; break ; } } } } private prepareTextElementGroup ( textElementGroup , tileKey , maxViewDistance ? ) { if ( textElementGroup . elements . length === 0 ) { return ; } const textElementSelection = ( textElementState ) : number | undefined => { let { result , viewDistance } = checkReadyForPlacement ( textElementState . element , textElementState . element . type === TextElementType . LineMarker ? textElementState . lineMarkerIndex : undefined , this . m_viewState , this . m_poiManager , maxViewDistance ) ; if ( result === PrePlacementResult . Ok && ! this . m_textElementStateCache . deduplicateElement ( this . m_viewState . zoomLevel , textElementState ) ) { result = PrePlacementResult . Duplicate ; viewDistance = undefined ; } if ( updateStats ) { updateStats . totalLabels ++ ; updateStats . results [ result ] ++ ; } return viewDistance ; } ; const [ , found ] = this . m_textElementStateCache . getOrSet ( textElementGroup , tileKey , textElementSelection ) ; if ( updateStats ) { ++ updateStats . totalGroups ; if ( ! found ) { ++ updateStats . newGroups ; } } } private createSortedGroupsForSorting ( tileDataSource , storageLevel , sortedTiles , sortedGroups ) { if ( sortedTiles . length === 0 ) { return ; } const tilesToRender = [ ] ; for ( const tile of sortedTiles ) { if ( tileDataSource . shouldRenderText ( storageLevel , tile . tileKey ) ) { tilesToRender . push ( tile ) ; } } const groupedPriorityLists = new Map ( ) ; for ( const tile of tilesToRender ) { for ( const group of tile . textElementGroups . groups . values ( ) ) { if ( group . elements . length === 0 ) { continue ; } const foundGroup = groupedPriorityLists . get ( group . priority ) ; if ( foundGroup === undefined ) { groupedPriorityLists . set ( group . priority , new TextElementLists ( [ new TileTextElements ( tile , group ) ] ) ) ; } else { foundGroup . lists . push ( new TileTextElements ( tile , group ) ) ; } } } if ( groupedPriorityLists . size === 0 ) { return ; } for ( const g of groupedPriorityLists ) { const lists = g [ 1 ] ; sortedGroups . push ( lists ) ; } sortedGroups . sort ( ( a , b ) => { return b . priority - a . priority ; } ) ; const printTextInfo = false ; if ( PRINT_LABEL_DEBUG_INFO && printTextInfo ) { let outString = "" ; for ( const textElementLists of sortedGroups ) { let size = 0 ; for ( const tileTextElements of textElementLists . lists ) { size += tileTextElements . group . elements . length ; } outString += `priority ${ textElementLists . priority } size: ${ size }\n` ; } logger . log ( outString ) ; } } private selectTextElementsToUpdateByDistance ( textElementLists ) { const farDistanceLimitRatio = Math . max ( this . m_options . maxDistanceRatioForTextLabels ! , this . m_options . maxDistanceRatioForPoiLabels ! ) ; const maxViewDistance = getMaxViewDistance ( this . m_viewState , farDistanceLimitRatio ) ; for ( const tileTextElements of textElementLists . lists ) { this . prepareTextElementGroup ( tileTextElements . group , tileTextElements . tile . tileKey , maxViewDistance ) ; } } private placeTextElements ( time , placeNewTextElements ) { const renderParams = { numRenderedTextElements : 0 , fadeAnimationRunning : false , time } ; const placeStartTime = this . overloaded && this . m_viewState . isDynamic ? PerformanceTimer . now ( ) : undefined ; if ( placementStats ) { placementStats . clear ( ) ; } if ( this . m_textElementStateCache . size === 0 ) { logger . debug ( "Text element cache empty." ) ; return ; } const placeNew = this . m_forceNewLabelsPass || placeNewTextElements ; if ( this . m_forceNewLabelsPass ) { this . m_forceNewLabelsPass = false ; } const maxNumPlacedTextElements = this . m_options . maxNumVisibleLabels ; const groupStates = this . m_textElementStateCache . sortedGroupStates ; let currentPriority = groupStates [ 0 ] . priority ; let currentPriorityBegin = 0 ; for ( let i = 0 ; i < groupStates . length ; ++ i ) { const textElementGroupState = groupStates [ i ] ; if ( placementStats ) { ++ placementStats . totalGroups ; } const newPriority = textElementGroupState . priority ; if ( placeNew && currentPriority !== newPriority ) { this . placeNewTextElements ( currentPriorityBegin , i , renderParams ) ; if ( isPlacementTimeExceeded ( placeStartTime ) ) { break ; } currentPriority = newPriority ; currentPriorityBegin = i ; } if ( ! this . placeTextElementGroup ( textElementGroupState , renderParams , maxNumPlacedTextElements , Pass . PersistentLabels ) ) { break ; } if ( isPlacementTimeExceeded ( placeStartTime ) ) { break ; } } if ( placeNew ) { this . placeNewTextElements ( currentPriorityBegin , groupStates . length , renderParams ) ; } if ( placementStats ) { placementStats . numRenderedTextElements = renderParams . numRenderedTextElements ; placementStats . log ( ) ; } if ( renderParams . fadeAnimationRunning ) { this . m_isUpdatePending = true ; } } private placeNewTextElements ( beginGroupIndex , endGroupIndex , renderParams ) { const groupStates = this . m_textElementStateCache . sortedGroupStates ; for ( let i = beginGroupIndex ; i < endGroupIndex ; ++ i ) { if ( ! this . placeTextElementGroup ( groupStates [ i ] , renderParams , this . m_options . maxNumVisibleLabels , Pass . NewLabels ) ) { break ; } } } private placeOverlayTextElements ( ) { if ( this . m_overlayTextElements === undefined || this . m_overlayTextElements . length === 0 ) { return ; } const screenSize = this . m_tmpVector . set ( this . m_screenProjector . width , this . m_screenProjector . height ) ; const screenXOrigin = - screenSize . width / 2.0 ; const screenYOrigin = screenSize . height / 2.0 ; for ( const textElement of this . m_overlayTextElements ! ) { const textElementStyle = this . m_textStyleCache . getTextElementStyle ( textElement . style ) ; const textCanvas = textElementStyle . textCanvas ; if ( textCanvas === undefined ) { continue ; } const forceNewPassOnLoaded = false ; this . initializeGlyphs ( textElement , textElementStyle , forceNewPassOnLoaded ) ; if ( textElement . loadingState !== LoadingState . Initialized ) { continue ; } const layer = textCanvas . getLayer ( textElement . renderOrder ?? DEFAULT_TEXT_CANVAS_LAYER ) ; if ( layer !== undefined ) { if ( layer . storage . drawCount + textElement . glyphs ! . length > layer . storage . capacity ) { continue ; } } textCanvas . textRenderStyle = textElement . renderStyle ! ; textCanvas . textLayoutStyle = textElement . layoutStyle ! ; let textPath ; if ( ! ( textElement . type === TextElementType . PathLabel ) ) { tempScreenPosition . x = screenXOrigin + textElement . position . x * screenSize . width ; tempScreenPosition . y = screenYOrigin - textElement . position . y * screenSize . height ; if ( textElement . xOffset !== undefined ) { tempScreenPosition . x += textElement . xOffset ; } if ( textElement . yOffset !== undefined ) { tempScreenPosition . y -= textElement . yOffset ; } tempPosition . x = tempScreenPosition . x ; tempPosition . y = tempScreenPosition . y ; tempPosition . z = 0.0 ; addTextToCanvas ( textElement , textCanvas , tempPosition ) ; } else { tempScreenPosition . x = screenXOrigin ; tempScreenPosition . y = screenYOrigin ; if ( textElement . xOffset !== undefined ) { tempScreenPosition . x += textElement . xOffset ; } if ( textElement . yOffset !== undefined ) { tempScreenPosition . y -= textElement . yOffset ; } const screenPoints = [ ] ; for ( const pt of textElement . path ! ) { const pX = tempScreenPosition . x + pt . x * screenSize . width ; const pY = tempScreenPosition . y - pt . y * screenSize . height ; screenPoints . push ( new THREE . Vector2 ( pX , pY ) ) ; } textPath = new SimplePath ( ) ; for ( let i = 0 ; i < screenPoints . length - 1 ; ++ i ) { textPath . add ( new THREE . LineCurve ( screenPoints [ i ] , screenPoints [ i + 1 ] ) ) ; } addTextToCanvas ( textElement , textCanvas , tempPosition , textPath , true ) ; } } } private getDistanceScalingFactor ( label , distance , lookAtDistance ) { let factor = lookAtDistance / distance ; factor = 1.0 + ( factor - 1.0 ) * label . distanceScale ; factor = Math . max ( factor , this . m_options . labelDistanceScaleMin ! ) ; factor = Math . min ( factor , this . m_options . labelDistanceScaleMax ! ) ; return factor ; } private getDistanceFadingFactor ( label , state , maxVisibilityDist ) { let distanceFadeValue = 1.0 ; const textDistance = state . viewDistance ; if ( textDistance !== undefined && label . fadeFar !== undefined && label . fadeFar > 0.0 ) { const fadeNear = label . fadeNear === undefined ? 0.0 : label . fadeNear ; const fadeFar = label . fadeFar ; if ( fadeFar > fadeNear ) { distanceFadeValue = 1.0 - THREE . MathUtils . clamp ( ( textDistance / maxVisibilityDist - fadeNear ) / ( fadeFar - fadeNear ) , 0.0 , 1.0 ) ; } } return distanceFadeValue ; } private addPointLabel ( labelState , position , screenPosition , textCanvas , renderParams ) { const pointLabel = labelState . element ; const textRenderState = labelState . textRenderState ; const isLineMarker = pointLabel . type === TextElementType . LineMarker ; const iconRenderState = labelState . iconRenderState ! ; assert ( iconRenderState !== undefined ) ; tempScreenPosition . x = tempPoiScreenPosition . x = screenPosition . x ; tempScreenPosition . y = tempPoiScreenPosition . y = screenPosition . y ; const textDistance = pointToPlaneDistance ( position , this . m_viewState . worldCenter , this . m_cameraLookAt ) ; if ( pointLabel . fadeFar !== undefined && ( pointLabel . fadeFar <= 0.0 || pointLabel . fadeFar * this . m_viewState . maxVisibilityDist < textDistance ) ) { if ( placementStats ) { ++ placementStats . tooFar ; } return false ; } labelState . setViewDistance ( textDistance ) ; const poiInfo = pointLabel . poiInfo ; let iconRejected = false ; const renderIcon = poiInfo !== undefined && MathUtils . isClamped ( this . m_viewState . zoomLevel , poiInfo . iconMinZoomLevel , poiInfo . iconMaxZoomLevel ) && poiInfo ! . isValid !== false ; const distanceScaleFactor = this . getDistanceScalingFactor ( pointLabel , textDistance , this . m_viewState . lookAtDistance ) ; const iconReady = renderIcon && this . m_poiRenderer . prepareRender ( pointLabel , this . m_viewState . env ) ; let iconInvisible = false ; if ( iconReady ) { const result = placeIcon ( iconRenderState , poiInfo ! , tempPoiScreenPosition , distanceScaleFactor , this . m_viewState . env , this . m_screenCollisions ) ; iconInvisible = result === PlacementResult . Invisible ; iconRejected = result === PlacementResult . Rejected ; if ( iconInvisible ) { iconRenderState . reset ( ) ; } } else if ( renderIcon && poiInfo ?. imageItem !== null ) { this . m_forceNewLabelsPass = true ; this . m_isUpdatePending = true ; } const distanceFadeFactor = this . getDistanceFadingFactor ( pointLabel , labelState , this . m_viewState . maxVisibilityDist ) ; if ( textCanvas && shouldRenderPointText ( labelState , this . m_viewState , this . m_options ) ) { const newLabel = ! labelState . visible ; const placeResult = iconRejected && newLabel ? PlacementResult . Rejected : placePointLabel ( labelState , tempScreenPosition , distanceScaleFactor , textCanvas , this . m_viewState . env , this . m_screenCollisions , tempPosition , ! isLineMarker ) ; const textInvisible = placeResult === PlacementResult . Invisible ; if ( textInvisible ) { if ( placementStats ) { placementStats . numPoiTextsInvisible ++ ; } if ( ! renderIcon || iconInvisible ) { labelState . reset ( ) ; return false ; } textRenderState ! . reset ( ) ; } const iconIsOptional = poiInfo ?. iconIsOptional === true ; const requiredIconRejected = iconRejected && iconReady && ! iconIsOptional ; const textRejected = requiredIconRejected || placeResult === PlacementResult . Rejected ; if ( ! iconRejected && ! iconInvisible ) { const textIsOptional = pointLabel . poiInfo ?. textIsOptional === true ; iconRejected = textRejected && ! textIsOptional ; } if ( textRejected ) { textRenderState ! . startFadeOut ( renderParams . time ) ; } const textNeedsDraw = ! textInvisible && ( ( ! textRejected && shouldRenderPoiText ( labelState , this . m_viewState ) ) || textRenderState ! . isFading ( ) ) ; if ( textNeedsDraw ) { if ( ! textRejected ) { textRenderState ! . startFadeIn ( renderParams . time , this . m_options . disableFading ) ; } renderParams . fadeAnimationRunning = renderParams . fadeAnimationRunning || textRenderState ! . isFading ( ) ; if ( addTextBufferToCanvas ( labelState , textCanvas , tempPosition , distanceFadeFactor , distanceScaleFactor ) && placementStats ) { placementStats . numRenderedPoiTexts ++ ; } } } if ( iconReady && ! iconInvisible ) { if ( iconRejected ) { iconRenderState . startFadeOut ( renderParams . time ) ; } else { iconRenderState ! . startFadeIn ( renderParams . time , this . m_options . disableFading ) ; } renderParams . fadeAnimationRunning = renderParams . fadeAnimationRunning || iconRenderState ! . isFading ( ) ; const opacity = iconRenderState . opacity * distanceFadeFactor ; if ( opacity > 0 ) { const allocateSpace = poiInfo ! . reserveSpace !== false && ! iconRejected ; this . m_poiRenderer . addPoi ( poiInfo ! , tempPoiScreenPosition , this . m_screenCollisions , labelState . renderDistance , distanceScaleFactor , allocateSpace , opacity , this . m_viewState . env ) ; if ( placementStats ) { placementStats . numRenderedPoiIcons ++ ; } } } renderParams . numRenderedTextElements ++ ; return true ; } private addPoiLabel ( labelState , textCanvas , renderParams ) { const worldPosition = getWorldPosition ( labelState . element , this . m_viewState . projection , this . m_viewState . env , this . m_tmpVector3 ) ; if ( ! this . labelPotentiallyVisible ( worldPosition , tempScreenPosition ) ) { return false ; } return this . addPointLabel ( labelState , worldPosition , tempScreenPosition , textCanvas , renderParams ) ; } private addLineMarkerLabel ( labelState , shieldGroups , textCanvas , renderParams ) { const lineMarkerLabel = labelState . element ; const poiInfo = lineMarkerLabel . poiInfo ! ; if ( ! this . m_poiRenderer ?. prepareRender ( lineMarkerLabel , this . m_viewState . env ) ) { return ; } let shieldGroup ; if ( poiInfo . shieldGroupIndex !== undefined ) { shieldGroup = shieldGroups [ poiInfo . shieldGroupIndex ] ; if ( shieldGroup === undefined ) { shieldGroup = [ ] ; shieldGroups [ poiInfo . shieldGroupIndex ] = shieldGroup ; } } const lineTechnique = poiInfo . technique as LineMarkerTechnique ; const minDistanceSqr = lineTechnique . minDistance !== undefined ? lineTechnique . minDistance * lineTechnique . minDistance : 0 ; if ( minDistanceSqr > 0 && shieldGroup !== undefined ) { let numShieldsVisible = 0 ; const point = labelState . position ! ; if ( this . labelPotentiallyVisible ( point , tempScreenPosition ) ) { let tooClose = false ; for ( let j = 0 ; j < shieldGroup . length ; j += 2 ) { const distanceSqr = Math2D . distSquared ( shieldGroup [ j ] , shieldGroup [ j + 1 ] , tempScreenPosition . x , tempScreenPosition . y ) ; tooClose = distanceSqr < minDistanceSqr ; if ( tooClose ) { break ; } } if ( ! tooClose ) { if ( this . addPointLabel ( labelState , point , tempScreenPosition , textCanvas , renderParams ) ) { shieldGroup . push ( tempScreenPosition . x , tempScreenPosition . y ) ; numShieldsVisible ++ ; } } } if ( numShieldsVisible === 0 ) { labelState . reset ( ) ; } } else { const point = labelState . position ! ; if ( this . labelPotentiallyVisible ( point , tempScreenPosition ) ) { this . addPointLabel ( labelState , point , tempScreenPosition , textCanvas , renderParams ) ; } } } private addPathLabel ( labelState , screenPoints , textCanvas , renderParams ) { const textMaxDistance = getMaxViewDistance ( this . m_viewState , this . m_options . maxDistanceRatioForTextLabels ! ) ; const pathLabel = labelState . element ; if ( ! ( pathLabel . ignoreDistance === true || labelState . viewDistance === undefined || labelState . viewDistance < textMaxDistance ) ) { if ( placementStats ) { ++ placementStats . tooFar ; } labelState . textRenderState ! . reset ( ) ; return false ; } if ( pathLabel . fadeFar !== undefined && ( pathLabel . fadeFar <= 0.0 || pathLabel . fadeFar * this . m_viewState . maxVisibilityDist < labelState . renderDistance ) ) { if ( placementStats ) { ++ placementStats . tooFar ; } labelState . textRenderState ! . reset ( ) ; return false ; } let textPath = new THREE . Path ( ) ; tempScreenPosition . copy ( screenPoints [ 0 ] ) ; for ( let i = 0 ; i < screenPoints . length - 1 ; ++ i ) { textPath . add ( new SimpleLineCurve ( screenPoints [ i ] , screenPoints [ i + 1 ] ) ) ; } if ( textPath . getPoint ( 0.5 ) . x - textPath . getPoint ( 0.51 ) . x > 0 ) { tempScreenPosition . copy ( screenPoints [ screenPoints . length - 1 ] ) ; textPath = new THREE . Path ( ) ; for ( let i = screenPoints . length - 1 ; i > 0 ; -- i ) { textPath . add ( new SimpleLineCurve ( screenPoints [ i ] , screenPoints [ i - 1 ] ) ) ; } } labelState . setViewDistance ( computeViewDistance ( pathLabel , undefined , this . m_viewState . worldCenter , this . m_cameraLookAt ) ) ; const textRenderDistance = - labelState . renderDistance ; const distanceScaleFactor = this . getDistanceScalingFactor ( pathLabel , textRenderDistance , this . m_viewState . lookAtDistance ) ; const prevSize = textCanvas . textRenderStyle . fontSize . size ; textCanvas . textRenderStyle . fontSize . size *= distanceScaleFactor ; if ( placePathLabel ( labelState , textPath , tempScreenPosition , textCanvas , this . m_screenCollisions ) !== PlacementResult . Ok ) { textCanvas . textRenderStyle . fontSize . size = prevSize ; if ( placementStats ) { ++ placementStats . numNotVisible ; } labelState . textRenderState ! . reset ( ) ; return false ; } labelState . textRenderState ! . startFadeIn ( renderParams . time , this . m_options . disableFading ) ; let opacity = pathLabel . renderStyle ! . opacity ; if ( labelState . textRenderState ! . isFading ( ) ) { opacity *= labelState . textRenderState ! . opacity ; renderParams . fadeAnimationRunning = true ; } if ( labelState . textRenderState ! . opacity === 0 ) { textCanvas . textRenderStyle . fontSize . size = prevSize ; return false ; } const prevOpacity = textCanvas . textRenderStyle . opacity ; const prevBgOpacity = textCanvas . textRenderStyle . backgroundOpacity ; const distanceFadeFactor = this . getDistanceFadingFactor ( pathLabel , labelState , this . m_viewState . maxVisibilityDist ) ; textCanvas . textRenderStyle . opacity = opacity * distanceFadeFactor ; textCanvas . textRenderStyle . backgroundOpacity = textCanvas . textRenderStyle . opacity * pathLabel . renderStyle ! . backgroundOpacity ; tempPosition . z = labelState . renderDistance ; addTextToCanvas ( pathLabel , textCanvas , tempPosition , textPath ) ; renderParams . numRenderedTextElements ++ ; textCanvas . textRenderStyle . fontSize . size = prevSize ; textCanvas . textRenderStyle . opacity = prevOpacity ; textCanvas . textRenderStyle . backgroundOpacity = prevBgOpacity ; return true ; } private checkIfOverloaded ( dataSourceTileList ) { let numTextElementsInScene = 0 ; dataSourceTileList . forEach ( renderListEntry => { for ( const tile of renderListEntry . renderedTiles . values ( ) ) { numTextElementsInScene += tile . textElementGroups . count ( ) ; } } ) ; const newOverloaded = numTextElementsInScene > OVERLOAD_LABEL_LIMIT ; if ( newOverloaded && ! this . m_overloaded ) { logger . debug ( "Overloaded Mode enabled." ) ; } this . m_overloaded = newOverloaded ; return this . m_overloaded ; } param point param outPoint private labelPotentiallyVisible ( point , outPoint ) { 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 ; } }