diff --git a/@here/harp-mapview/lib/MapView.ts b/@here/harp-mapview/lib/MapView.ts index a0778b5dc7..35d2c27e2f 100644 --- a/@here/harp-mapview/lib/MapView.ts +++ b/@here/harp-mapview/lib/MapView.ts @@ -66,19 +66,14 @@ import { MapViewThemeManager } from "./MapViewThemeManager"; import { PickHandler, PickResult } from "./PickHandler"; import { PickingRaycaster } from "./PickingRaycaster"; import { PoiManager } from "./poi/PoiManager"; -import { PoiRenderer } from "./poi/PoiRenderer"; import { PoiTableManager } from "./poi/PoiTableManager"; import { PolarTileDataSource } from "./PolarTileDataSource"; -import { ScreenCollisions, ScreenCollisionsDebug } from "./ScreenCollisions"; import { ScreenProjector } from "./ScreenProjector"; import { FrameStats, PerformanceStatistics } from "./Statistics"; -import { FontCatalogLoader } from "./text/FontCatalogLoader"; import { MapViewState } from "./text/MapViewState"; -import { TextCanvasFactory } from "./text/TextCanvasFactory"; import { TextElement } from "./text/TextElement"; import { TextElementsRenderer, ViewUpdateCallback } from "./text/TextElementsRenderer"; import { TextElementsRendererOptions } from "./text/TextElementsRendererOptions"; -import { TextStyleCache } from "./text/TextStyleCache"; import { Tile } from "./Tile"; import { TileObjectRenderer } from "./TileObjectsRenderer"; import { MapViewUtils } from "./Utils"; @@ -457,11 +452,6 @@ export interface MapViewOptions extends TextElementsRendererOptions, Partial 0) { @@ -3697,29 +3663,6 @@ export class MapView extends EventDispatcher { } } - private prepareRenderTextElements(time: number) { - // Disable rendering of text elements for debug camera. TextElements are rendered using an - // orthographic camera that covers the entire available screen space. Unfortunately, this - // particular camera set up is not compatible with the debug camera. - const debugCameraActive = this.m_pointOfView !== undefined; - - if (debugCameraActive) { - return; - } - - this.m_textElementsRenderer.placeText(this.m_visibleTiles.dataSourceTileList, time); - } - - private finishRenderTextElements() { - const canRenderTextElements = this.m_pointOfView === undefined; - - if (canRenderTextElements) { - // copy far value from scene camera, as the distance to the POIs matter now. - this.m_screenCamera.far = this.m_viewRanges.maximum; - this.m_textElementsRenderer.renderText(this.m_screenCamera); - } - } - private setupCamera() { const { width, height } = this.getCanvasClientSize(); @@ -3744,9 +3687,6 @@ export class MapView extends EventDispatcher { // ### move & customize this.resize(width, height); - - this.m_screenCamera.position.z = 1; - this.m_screenCamera.near = 0; } private createVisibleTileSet(): VisibleTileSet { @@ -3886,26 +3826,18 @@ export class MapView extends EventDispatcher { tileObjectRenderer.setupRenderer(); } - private createTextRenderer( - fontCatalogs?: FontCatalogConfig[], - textStyles?: TextStyleDefinition[], - defaultTextStyle?: TextStyleDefinition - ): TextElementsRenderer { + private createTextRenderer(): TextElementsRenderer { const updateCallback: ViewUpdateCallback = () => { this.update(); }; return new TextElementsRenderer( new MapViewState(this, this.checkIfTilesChanged.bind(this)), - this.m_camera, updateCallback, - this.m_screenCollisions, this.m_screenProjector, - new TextCanvasFactory(this.m_renderer), this.m_poiManager, - this.m_poiRenderer, - new FontCatalogLoader(fontCatalogs), - new TextStyleCache(textStyles, defaultTextStyle), + this.m_renderer, + [this.imageCache, this.userImageCache], this.m_options ); } @@ -3921,18 +3853,9 @@ export class MapView extends EventDispatcher { textStyles?: TextStyleDefinition[], defaultTextStyle?: TextStyleDefinition ): Promise { - const overlayText = this.m_textElementsRenderer.overlayText; - this.m_poiRenderer.reset(); - this.m_textElementsRenderer = this.createTextRenderer( - fontCatalogs, - textStyles, - defaultTextStyle - ); - if (overlayText !== undefined) { - this.m_textElementsRenderer.addOverlayText(overlayText); - } - await this.m_textElementsRenderer.waitInitialized(); - await this.m_textElementsRenderer.waitLoaded(); + await this.m_textElementsRenderer.updateFontCatalogs(fontCatalogs); + await this.m_textElementsRenderer.updateTextStyles(textStyles, defaultTextStyle); + this.update(); } /** @@ -3953,10 +3876,7 @@ export class MapView extends EventDispatcher { private readonly onWebGLContextRestored = (event: Event) => { this.dispatchEvent(this.CONTEXT_RESTORED_EVENT); if (this.m_renderer !== undefined) { - this.m_poiRenderer = new PoiRenderer(this.m_renderer, this.m_poiManager, [ - this.imageCache, - this.userImageCache - ]); + this.textElementsRenderer.restoreRenderers(this.m_renderer); this.getTheme().then(theme => { this.m_sceneEnvironment.updateClearColor(theme.clearColor, theme.clearAlpha); this.update(); diff --git a/@here/harp-mapview/lib/MapViewThemeManager.ts b/@here/harp-mapview/lib/MapViewThemeManager.ts index 1b560d342c..9c3f9014cb 100644 --- a/@here/harp-mapview/lib/MapViewThemeManager.ts +++ b/@here/harp-mapview/lib/MapViewThemeManager.ts @@ -137,10 +137,9 @@ export class MapViewThemeManager { this.m_theme.styles = theme.styles ?? {}; this.m_theme.definitions = theme.definitions; - // TODO: this is asynchronouse too environment.clearBackgroundDataSource(); for (const dataSource of this.m_mapView.dataSources) { - dataSource.setTheme(this.m_theme); + await dataSource.setTheme(this.m_theme); } } diff --git a/@here/harp-mapview/lib/text/FontCatalogLoader.ts b/@here/harp-mapview/lib/text/FontCatalogLoader.ts index 7a657307c7..6d2d317a63 100644 --- a/@here/harp-mapview/lib/text/FontCatalogLoader.ts +++ b/@here/harp-mapview/lib/text/FontCatalogLoader.ts @@ -5,69 +5,23 @@ */ import { FontCatalogConfig } from "@here/harp-datasource-protocol"; import { FontCatalog } from "@here/harp-text-canvas"; -import { assert, LoggerManager } from "@here/harp-utils"; - -export const DEFAULT_FONT_CATALOG_NAME = "default"; +import { LoggerManager } from "@here/harp-utils"; const logger = LoggerManager.instance.create("FontCatalogLoader"); type FontCatalogCallback = (name: string, catalog: FontCatalog) => void; -export class FontCatalogLoader { - private m_catalogsLoading: number = 0; - - constructor(private m_fontCatalogs?: FontCatalogConfig[]) {} - - /** - * Initializes font catalog loader. - * @param defaultFontCatalogUrl - Url of the font catalog that will be used by default if the - * theme doesn't define any font catalog. - * @returns Name of the default font catalog. - */ - initialize(defaultFontCatalogUrl: string): string { - if (this.m_fontCatalogs === undefined || this.m_fontCatalogs.length === 0) { - this.m_fontCatalogs = [ - { - name: DEFAULT_FONT_CATALOG_NAME, - url: defaultFontCatalogUrl - } - ]; - return DEFAULT_FONT_CATALOG_NAME; - } - - const defaultFontCatalogName = this.m_fontCatalogs[0].name; - return defaultFontCatalogName; - } - - async loadCatalogs( - catalogCallback: FontCatalogCallback, - failureCallback?: (name: string, error: Error) => void - ): Promise { - assert(this.m_fontCatalogs !== undefined); - assert(this.m_fontCatalogs!.length > 0); - - const promises: Array> = []; - - this.m_fontCatalogs!.forEach(fontCatalogConfig => { - this.m_catalogsLoading += 1; - const fontCatalogPromise: Promise = FontCatalog.load(fontCatalogConfig.url, 1024) - .then(catalogCallback.bind(undefined, fontCatalogConfig.name)) - .catch((error: Error) => { - logger.error("Failed to load FontCatalog: ", error); - if (failureCallback) { - failureCallback(fontCatalogConfig.name, error); - } - }) - .finally(() => { - this.m_catalogsLoading -= 1; - }); - promises.push(fontCatalogPromise); +export async function loadFontCatalog( + fontCatalogConfig: FontCatalogConfig, + onSuccess: FontCatalogCallback, + onError?: (error: Error) => void +): Promise { + return await FontCatalog.load(fontCatalogConfig.url, 1024) + .then(onSuccess.bind(undefined, fontCatalogConfig.name)) + .catch((error: Error) => { + logger.error("Failed to load FontCatalog: ", fontCatalogConfig.name, error); + if (onError) { + onError(error); + } }); - - return Promise.all(promises); - } - - get loading(): boolean { - return this.m_catalogsLoading > 0; - } } diff --git a/@here/harp-mapview/lib/text/Placement.ts b/@here/harp-mapview/lib/text/Placement.ts index f4d2329cbe..ede111b60f 100644 --- a/@here/harp-mapview/lib/text/Placement.ts +++ b/@here/harp-mapview/lib/text/Placement.ts @@ -184,7 +184,6 @@ const tmpPlacementBounds = new THREE.Box2(); * @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 viewCamera - The view's camera. * @param m_poiManager - To prepare pois for rendering. * @param maxViewDistance - If specified, text elements farther than this max distance will be * rejected. @@ -195,7 +194,6 @@ export function checkReadyForPlacement( textElement: TextElement, poiIndex: number | undefined, viewState: ViewState, - viewCamera: THREE.Camera, poiManager: PoiManager, maxViewDistance?: number ): { result: PrePlacementResult; viewDistance: number | undefined } { diff --git a/@here/harp-mapview/lib/text/TextElementsRenderer.ts b/@here/harp-mapview/lib/text/TextElementsRenderer.ts index 23f12ac43f..aca9bf085c 100644 --- a/@here/harp-mapview/lib/text/TextElementsRenderer.ts +++ b/@here/harp-mapview/lib/text/TextElementsRenderer.ts @@ -3,7 +3,11 @@ * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ -import { LineMarkerTechnique } from "@here/harp-datasource-protocol"; +import { + FontCatalogConfig, + LineMarkerTechnique, + TextStyleDefinition +} from "@here/harp-datasource-protocol"; import { TileKey, Vector3Like } from "@here/harp-geoutils"; import { AdditionParameters, @@ -28,16 +32,17 @@ 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 } from "../ScreenCollisions"; +import { IBox, LineWithBound, ScreenCollisions, ScreenCollisionsDebug } from "../ScreenCollisions"; import { ScreenProjector } from "../ScreenProjector"; import { Tile } from "../Tile"; import { MapViewUtils } from "../Utils"; import { DataSourceTileList } from "../VisibleTileSet"; -import { FontCatalogLoader } from "./FontCatalogLoader"; +import { loadFontCatalog } from "./FontCatalogLoader"; import { checkReadyForPlacement, computeViewDistance, @@ -81,6 +86,10 @@ enum Pass { NewLabels } +export type TextCanvases = Map; + +export const DEFAULT_FONT_CATALOG_NAME = "default"; + /** * Default distance scale. Will be applied if distanceScale is not defined in the technique. * Defines the scale that will be applied to labeled icons (icon and text) in the distance. @@ -134,6 +143,9 @@ const tempPoiScreenPosition = new THREE.Vector2(); const tmpTextBufferCreationParams: TextBufferCreationParameters = {}; const tmpAdditionParams: AdditionParameters = {}; const tmpBufferAdditionParams: TextBufferAdditionParameters = {}; +const cache = { + vector2: [new THREE.Vector2()] +}; class TileTextElements { constructor(readonly tile: Tile, readonly group: TextElementGroup) {} @@ -310,18 +322,23 @@ function isPlacementTimeExceeded(startTime: number | undefined): boolean { return false; } +function createDefaultFontCatalogConfig(defaultFontCatalogUrl: string): FontCatalogConfig { + return { + name: DEFAULT_FONT_CATALOG_NAME, + url: defaultFontCatalogUrl + }; +} + /** * * Internal class to manage all text rendering. */ export class TextElementsRenderer { - private m_initialized: boolean = false; - private m_initPromise: Promise | undefined; - private m_glyphLoadingCount: number = 0; + private m_loadPromisesCount: number = 0; private m_loadPromise: Promise | undefined; private readonly m_options: TextElementsRendererOptions; - private readonly m_textCanvases: TextCanvas[] = []; + private readonly m_textCanvases: TextCanvases = new Map(); private m_overlayTextElements?: TextElement[]; @@ -338,46 +355,70 @@ export class TextElementsRenderer { private m_addNewLabels: boolean = true; 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 m_textStyleCache: TextStyleCache = new TextStyleCache(); + private readonly m_screenCollisions: + | ScreenCollisions + | ScreenCollisionsDebug = new ScreenCollisions(); + + private readonly m_textCanvasFactory: TextCanvasFactory; /** * 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 * {@link TextElement}s every frame. * * @param m_viewState - State of the view for which this renderer will draw text. - * @param m_viewCamera - Camera used by the view for which this renderer will draw text. * @param m_viewUpdateCallback - To be called whenever the view needs to be updated. - * @param m_screenCollisions - General 2D screen occlusion management, may be shared between - * instances. * @param m_screenProjector - Projects 3D coordinates into screen space. - * @param m_textCanvasFactory - To create TextCanvas instances. - * @param m_poiRenderer - To render Icons. * @param m_poiManager - To prepare pois for rendering. - * @param m_fontCatalogLoader - To load font catalogs. - * @param m_textStyleCache - Cache defining text styles. + * @param m_renderer - The renderer to be used. + * @param m_imageCaches - The Image Caches to look for Icons. * @param options - Configuration options for the text renderer. See + * @param textCanvasFactory - Optional A TextCanvasFactory to override the default. + * @param poiRenderer - Optional A PoiRenderer to override the default. + * @param screenCollisions - Optional ScreenCollisions to override the default. * [[TextElementsRendererOptions]]. */ constructor( private readonly m_viewState: ViewState, - private readonly m_viewCamera: THREE.Camera, private readonly m_viewUpdateCallback: ViewUpdateCallback, - private readonly m_screenCollisions: ScreenCollisions, private readonly m_screenProjector: ScreenProjector, - private readonly m_textCanvasFactory: TextCanvasFactory, private readonly m_poiManager: PoiManager, - private readonly m_poiRenderer: PoiRenderer, - private readonly m_fontCatalogLoader: FontCatalogLoader, - private readonly m_textStyleCache: TextStyleCache, - options: TextElementsRendererOptions + private m_renderer: THREE.WebGLRenderer, + private readonly m_imageCaches: MapViewImageCache[], + options: TextElementsRendererOptions, + textCanvasFactory?: TextCanvasFactory, + poiRenderer?: PoiRenderer, + screenCollisions?: 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.initializeTextElementStyles(this.m_textCanvases); } /** @@ -418,33 +459,102 @@ export class TextElementsRenderer { this.m_options.showReplacementGlyphs = value; this.m_textCanvases.forEach(textCanvas => { - textCanvas.fontCatalog.showReplacementGlyphs = value; + if (textCanvas?.fontCatalog) { + textCanvas.fontCatalog.showReplacementGlyphs = value; + } }); } + restoreRenderers(renderer: THREE.WebGLRenderer) { + this.m_renderer = renderer; + this.m_poiRenderer = new PoiRenderer( + this.m_renderer, + this.m_poiManager, + this.m_imageCaches + ); + //TODO: restore TextCanvasRenderers + } + /** - * Render the text using the specified camera into the current canvas. + * Updates the FontCatalogs used by this {@link TextElementsRenderer}. * - * @param camera - Orthographic camera to use. + * @param fontCatalogs - The new list of {@link FontCatalogConfig}s */ - renderText(camera: THREE.OrthographicCamera) { - if (!this.initialized) { - return; + async updateFontCatalogs(fontCatalogs?: FontCatalogConfig[]) { + if (this.m_defaultFontCatalogConfig) { + if ( + !fontCatalogs || + fontCatalogs.findIndex(config => { + return config.name === DEFAULT_FONT_CATALOG_NAME; + }) === -1 + ) { + // not other default catalog available, keep the old one + if (!fontCatalogs) { + fontCatalogs = []; + } + // Never remove the default Canvas if set per configuration + 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) { + // Remove obsolete ones + for (const [name] of this.m_textCanvases) { + if ( + fontCatalogs.findIndex(catalog => { + return catalog.name === name; + }) < 0 + ) { + this.m_textCanvases.delete(name); + } + } + + // Add new catalogs + for (const fontCatalog of fontCatalogs) { + await this.addTextCanvas(fontCatalog); + } + } else { + this.m_textCanvases.clear(); } + this.m_textStyleCache.initializeTextElementStyles(this.m_textCanvases); + } + + async updateTextStyles( + textStyles?: TextStyleDefinition[], + defaultTextStyle?: TextStyleDefinition + ) { + // TODO: this is an intermeditate solution, in the end this + // should not create a new cache, but update the former one + this.m_textStyleCache = new TextStyleCache(textStyles, defaultTextStyle); + this.m_textStyleCache.initializeDefaultTextElementStyle(DEFAULT_FONT_CATALOG_NAME); + await this.waitLoaded(); + this.m_textStyleCache.initializeTextElementStyles(this.m_textCanvases); + } + /** + * Render the text using the specified camera into the current canvas. + * + * @param camera - Orthographic camera to use. + */ + renderText(farPlane: number) { + this.m_camera.far = farPlane; this.updateGlyphDebugMesh(); let previousLayer: PoiLayer | undefined; this.m_poiRenderer.update(); for (const poiLayer of this.m_poiRenderer.layers) { - for (const textCanvas of this.m_textCanvases) { - textCanvas.render(camera, previousLayer?.id, poiLayer.id, undefined, false); + for (const [, textCanvas] of this.m_textCanvases) { + textCanvas?.render(this.m_camera, previousLayer?.id, poiLayer.id, undefined, false); } - this.m_poiRenderer.render(camera, poiLayer); + this.m_poiRenderer.render(this.m_camera, poiLayer); previousLayer = poiLayer; } - for (const textCanvas of this.m_textCanvases) { - textCanvas.render(camera, previousLayer?.id, undefined, undefined, false); + for (const [, textCanvas] of this.m_textCanvases) { + textCanvas?.render(this.m_camera, previousLayer?.id, undefined, undefined, false); } } @@ -492,7 +602,8 @@ export class TextElementsRenderer { const textElementsAvailable = this.hasOverlayText() || tileTextElementsChanged || hasTextElements(dataSourceTileList); - if (!this.initialize(textElementsAvailable)) { + + if (!textElementsAvailable) { return; } @@ -513,6 +624,7 @@ export class TextElementsRenderer { this.m_viewState.zoomLevel ); + // TODO: this seems extremly suboptimal.. review if an update is possible this.reset(); if (this.m_addNewLabels) { this.prepopulateScreenWithBlockingElements(dataSourceTileList); @@ -595,8 +707,8 @@ export class TextElementsRenderer { pickListener.addResult(pickResult); }; - for (const textCanvas of this.m_textCanvases) { - textCanvas.pickText(screenPosition, (pickData: any | undefined) => { + for (const [, textCanvas] of this.m_textCanvases) { + textCanvas?.pickText(screenPosition, (pickData: any | undefined) => { pickHandler(pickData, PickObjectType.Text); }); } @@ -610,22 +722,16 @@ export class TextElementsRenderer { * `true` if any resource used by any `FontCatalog` is still loading. */ get loading(): boolean { - return this.m_fontCatalogLoader.loading || this.m_glyphLoadingCount > 0; + return this.m_loadPromisesCount > 0; } /** * Waits till all pending resources from any `FontCatalog` are loaded. */ - async waitLoaded(): Promise { - const initialized = await this.waitInitialized(); - if (!initialized) { - return false; - } - if (this.m_loadPromise === undefined) { - return false; + async waitLoaded(): Promise { + if (this.m_loadPromise !== undefined) { + return await this.m_loadPromise; } - await this.m_loadPromise; - return true; } /** @@ -650,54 +756,23 @@ export class TextElementsRenderer { gpuSize: 0 }; - for (const textCanvas of this.m_textCanvases) { - textCanvas.getMemoryUsage(memoryUsage); + for (const [, textCanvas] of this.m_textCanvases) { + textCanvas?.getMemoryUsage(memoryUsage); } this.m_poiRenderer.getMemoryUsage(memoryUsage); return memoryUsage; } - get initialized(): boolean { - return this.m_initialized; - } - - get initializing(): boolean { - return this.m_initPromise !== undefined; - } - - /** - * Waits until initialization is done. - * @returns Promise resolved to true if initialization was done, false otherwise. - */ - async waitInitialized(): Promise { - if (this.initialized) { - return true; - } - - if (!this.initializing) { - return false; - } - await this.m_initPromise; - return true; - } - - /** - * Initializes the text renderer once there's any text element available for rendering. - * @param textElementsAvailable - Indicates whether there's any text element to be rendered. - * @returns Whether the text renderer is initialized. - */ - private initialize(textElementsAvailable: boolean): boolean { - if (!this.initialized && !this.initializing && textElementsAvailable) { - this.initializeDefaultAssets(); - this.m_initPromise = this.initializeTextCanvases().then(() => { - this.m_initialized = true; - this.m_initPromise = undefined; - this.invalidateCache(); // Force cache update after initialization. - this.m_viewUpdateCallback(); - }); + private async addDefaultTextCanvas(): Promise { + if ( + this.m_textCanvases.has(DEFAULT_FONT_CATALOG_NAME) || + !this.m_defaultFontCatalogConfig + ) { + return; } - return this.initialized; + await this.addTextCanvas(this.m_defaultFontCatalogConfig); + this.m_textStyleCache.initializeTextElementStyles(this.m_textCanvases); } /** @@ -706,8 +781,8 @@ export class TextElementsRenderer { private reset() { this.m_cameraLookAt.copy(this.m_viewState.lookAtVector); this.m_screenCollisions.reset(); - for (const textCanvas of this.m_textCanvases) { - textCanvas.clear(); + for (const [, textCanvas] of this.m_textCanvases) { + textCanvas?.clear(); } this.m_poiRenderer.reset(); } @@ -951,7 +1026,7 @@ export class TextElementsRenderer { const newLoadPromise = textCanvas.fontCatalog .loadCharset(textElement.text, textElement.renderStyle) .then(() => { - --this.m_glyphLoadingCount; + --this.m_loadPromisesCount; textElement.loadingState = LoadingState.Loaded; // Ensure that text elements still loading glyphs get a chance to // be rendered if there's no text element updates in the next frames. @@ -959,10 +1034,10 @@ export class TextElementsRenderer { this.m_forceNewLabelsPass || forceNewPassOnLoaded; this.m_viewUpdateCallback(); }); - if (this.m_glyphLoadingCount === 0) { + if (this.m_loadPromisesCount === 0) { this.m_loadPromise = undefined; } - ++this.m_glyphLoadingCount; + ++this.m_loadPromisesCount; this.m_loadPromise = this.m_loadPromise === undefined @@ -989,30 +1064,73 @@ export class TextElementsRenderer { return textElement.glyphs !== undefined; } - private initializeDefaultAssets(): void { - const defaultFontCatalogName = this.m_fontCatalogLoader.initialize( - this.m_options.fontCatalog! - ); - this.m_textStyleCache.initializeDefaultTextElementStyle(defaultFontCatalogName); + 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 initializeTextCanvases(): Promise { + private async addTextCanvas(fontCatalogConfig: FontCatalogConfig): Promise { const catalogCallback = (name: string, catalog: FontCatalog) => { - const loadedTextCanvas = this.m_textCanvasFactory.createTextCanvas(catalog, name); + if (this.m_textCanvases.has(name)) { + const loadedTextCanvas = this.m_textCanvasFactory.createTextCanvas(catalog, name); - catalog.showReplacementGlyphs = this.showReplacementGlyphs; + catalog.showReplacementGlyphs = this.showReplacementGlyphs; - this.m_textCanvases.push(loadedTextCanvas); + // Check if the textCanvas has not been removed in the meantime + this.m_textCanvases.set(name, loadedTextCanvas); + } }; - - return this.m_fontCatalogLoader - .loadCatalogs(catalogCallback) - .then(() => { - this.m_textStyleCache.initializeTextElementStyles(this.m_textCanvases); - }) - .catch(error => { - logger.info("rendering without font catalog, only icons possible"); - }); + const errorCallback = () => { + this.m_textCanvases.delete(fontCatalogConfig.name); + }; + if (this.m_textCanvases.has(fontCatalogConfig.name)) { + return Promise.resolve(); + } else { + // Reserve map space, until loaded or error + this.m_textCanvases.set(fontCatalogConfig.name, undefined); + const newLoadPromise = loadFontCatalog( + fontCatalogConfig, + catalogCallback, + errorCallback + ) + .then(() => { + --this.m_loadPromisesCount; + this.m_viewUpdateCallback(); + }) + .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() { @@ -1032,10 +1150,11 @@ export class TextElementsRenderer { } private initializeGlyphDebugMesh() { - if (this.m_textCanvases.length === 0) { + if (this.m_textCanvases.size === 0) { return; } - const defaultFontCatalog = this.m_textCanvases[0].fontCatalog; + const defaultTextCanvas = this.m_textCanvases.values().next().value; + const defaultFontCatalog = defaultTextCanvas.fontCatalog; // Initialize glyph-debugging mesh. const planeGeometry = new THREE.PlaneGeometry( @@ -1072,7 +1191,7 @@ export class TextElementsRenderer { this.m_debugGlyphTextureCacheWireMesh.name = "glyphDebug"; - this.m_textCanvases[0] + defaultTextCanvas .getLayer(DEFAULT_TEXT_CANVAS_LAYER)! .storage.scene.add( this.m_debugGlyphTextureCacheMesh, @@ -1194,7 +1313,6 @@ export class TextElementsRenderer { ? textElementState.lineMarkerIndex : undefined, this.m_viewState, - this.m_viewCamera, this.m_poiManager, maxViewDistance ); diff --git a/@here/harp-mapview/lib/text/TextElementsRendererOptions.ts b/@here/harp-mapview/lib/text/TextElementsRendererOptions.ts index 61c0a01422..e37e375e8f 100644 --- a/@here/harp-mapview/lib/text/TextElementsRendererOptions.ts +++ b/@here/harp-mapview/lib/text/TextElementsRendererOptions.ts @@ -126,6 +126,11 @@ export interface TextElementsRendererOptions { * @default [[DEFAULT_MAX_DISTANCE_TO_BORDER]]. */ maxPoiDistanceToBorder?: number; + + /** + * An optional canvas element that renders 2D collision debug information. + */ + collisionDebugCanvas?: HTMLCanvasElement; } /** diff --git a/@here/harp-mapview/lib/text/TextStyleCache.ts b/@here/harp-mapview/lib/text/TextStyleCache.ts index e3df2e5034..31468a3af1 100644 --- a/@here/harp-mapview/lib/text/TextStyleCache.ts +++ b/@here/harp-mapview/lib/text/TextStyleCache.ts @@ -38,6 +38,7 @@ import { getOptionValue, LoggerManager } from "@here/harp-utils"; import { ColorCache } from "../ColorCache"; import { evaluateColorProperty } from "../DecodedTileHelpers"; import { Tile } from "../Tile"; +import { DEFAULT_FONT_CATALOG_NAME, TextCanvases } from "./TextElementsRenderer"; const logger = LoggerManager.instance.create("TextStyleCache"); @@ -68,7 +69,7 @@ const DEFAULT_STYLE_NAME = "default"; */ export interface TextElementStyle { name: string; - fontCatalog: string; + fontCatalog?: string; renderParams: TextRenderParameters; layoutParams: TextLayoutParameters; textCanvas?: TextCanvas; @@ -78,7 +79,7 @@ export class TextStyleCache { private readonly m_textStyles: Map = new Map(); private m_defaultStyle: TextElementStyle = { name: DEFAULT_STYLE_NAME, - fontCatalog: "", + fontCatalog: undefined, renderParams: defaultTextRenderStyle.params, layoutParams: defaultTextLayoutStyle.params }; @@ -109,14 +110,15 @@ export class TextStyleCache { this.m_defaultStyle = this.createTextElementStyle(styles[0], DEFAULT_STYLE_NAME); } this.m_defaultStyle.fontCatalog = defaultFontCatalogName; + this.m_defaultStyle.textCanvas = undefined; } - initializeTextElementStyles(textCanvases: TextCanvas[]) { + initializeTextElementStyles(textCanvases: TextCanvases) { // Initialize default text style. this.initializeTextCanvas(this.m_defaultStyle, textCanvases); // Initialize theme text styles. - this.m_textStyleDefinitions!.forEach(element => { + this.m_textStyleDefinitions?.forEach(element => { this.m_textStyles.set( element.name!, this.createTextElementStyle(element, element.name!) @@ -351,12 +353,14 @@ export class TextStyleCache { return layoutStyle; } - private initializeTextCanvas(style: TextElementStyle, textCanvases: TextCanvas[]): void { + private initializeTextCanvas(style: TextElementStyle, textCanvases: TextCanvases): void { if (style.fontCatalog !== undefined) { - const styledTextCanvas = textCanvases.find(textCanvas => { - return textCanvas.name === style.fontCatalog; - }); + const styledTextCanvas = textCanvases.get(style.fontCatalog); style.textCanvas = styledTextCanvas; + if (textCanvases.has(style.fontCatalog) && !styledTextCanvas) { + logger.info(`fontCatalog(${style.fontCatalog}), not yet loaded`); + return; + } } if (style.textCanvas === undefined) { if (style.fontCatalog !== undefined) { @@ -365,9 +369,20 @@ export class TextStyleCache { '${style.name}' not found` ); } - if (textCanvases.length > 0) { - style.textCanvas = textCanvases[0]; - logger.info(`using default fontCatalog(${textCanvases[0].fontCatalog.name}).`); + const defaultCanvas = textCanvases.get(DEFAULT_FONT_CATALOG_NAME); + if (defaultCanvas) { + style.textCanvas = defaultCanvas; + logger.info(`using default fontCatalog(${style.textCanvas?.fontCatalog.name}).`); + } else if (textCanvases.size > 0) { + for (const [, canvas] of textCanvases) { + if (canvas) { + style.textCanvas = canvas; + logger.info( + `using default fontCatalog(${style.textCanvas?.fontCatalog.name}).` + ); + break; + } + } } } } diff --git a/@here/harp-mapview/test/MapViewTest.ts b/@here/harp-mapview/test/MapViewTest.ts index e45a1de7e0..5c8600fa40 100644 --- a/@here/harp-mapview/test/MapViewTest.ts +++ b/@here/harp-mapview/test/MapViewTest.ts @@ -22,7 +22,6 @@ import { waitForEvent } from "@here/harp-test-utils"; import * as TestUtils from "@here/harp-test-utils/lib/WebGLStub"; -import { FontCatalog } from "@here/harp-text-canvas"; import { assert, expect } from "chai"; import * as sinon from "sinon"; import * as THREE from "three"; @@ -36,6 +35,7 @@ import { MapObjectAdapter } from "../lib/MapObjectAdapter"; import { MapView, MapViewEventNames, MapViewOptions } from "../lib/MapView"; import { DEFAULT_CLEAR_COLOR } from "../lib/MapViewEnvironment"; import { MapViewFog } from "../lib/MapViewFog"; +import * as FontCatalogLoader from "../lib/text/FontCatalogLoader"; import { MapViewUtils } from "../lib/Utils"; import { VisibleTileSet } from "../lib/VisibleTileSet"; import { FakeOmvDataSource } from "./FakeOmvDataSource"; @@ -69,7 +69,7 @@ describe("MapView", function() { sandbox .stub(THREE, "WebGL1Renderer") .returns(TestUtils.getWebGLRendererStub(sandbox, clearColorStub)); - const _fontStub = sandbox.stub(FontCatalog, "load").returns(new Promise(() => {})); + sandbox.stub(FontCatalogLoader, "loadFontCatalog").resolves(); if (inNodeContext) { const theGlobal: any = global; theGlobal.window = { window: { devicePixelRatio: 10 } }; diff --git a/@here/harp-mapview/test/TextElementsRendererTest.ts b/@here/harp-mapview/test/TextElementsRendererTest.ts index a85177be83..89cec31e47 100644 --- a/@here/harp-mapview/test/TextElementsRendererTest.ts +++ b/@here/harp-mapview/test/TextElementsRendererTest.ts @@ -3,14 +3,12 @@ * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ - -// Mocha discourages using arrow functions, see https://mochajs.org/#arrow-functions - -import { assert, expect } from "chai"; +import { expect } from "chai"; import * as sinon from "sinon"; import * as THREE from "three"; import { TextElement } from "../lib/text/TextElement"; +import { DEFAULT_FONT_CATALOG_NAME } from "../lib/text/TextElementsRenderer"; import { PoiInfoBuilder } from "./PoiInfoBuilder"; import { DEF_PATH, @@ -49,6 +47,8 @@ import { not } from "./TextElementsRendererTestUtils"; +// Mocha discourages using arrow functions, see https://mochajs.org/#arrow-functions + /** * Definition of a test case for TextElementsRenderer, including input data (tiles, text elements, * frame times...) and expected output (text element fade states at each frame). @@ -735,8 +735,7 @@ describe("TextElementsRenderer", function() { } fixture = new TestFixture(sandbox); - const setupDone = await fixture.setUp(); - assert(setupDone, "Setup failed."); + await fixture.setUp(); }); afterEach(function() { @@ -882,4 +881,73 @@ describe("TextElementsRenderer", function() { } }); } + + it("updates FontCatalogs", async function() { + expect(fixture.loadCatalogStub.calledOnce, "default catalog was set").to.be.true; + await fixture.textRenderer.updateFontCatalogs([ + { + name: "catalog1", + url: "some-url-1" + }, + { + name: "catalog2", + url: "some-url-2" + } + ]); + expect(fixture.loadCatalogStub.calledThrice).to.be.true; + + await fixture.textRenderer.updateFontCatalogs([ + { + name: "catalog1", + url: "some-url-1" + }, + { + name: "catalog2", + url: "some-other-url-2" + } + ]); + expect(fixture.loadCatalogStub.calledThrice, "no new catalog added").to.be.true; + + await fixture.textRenderer.updateFontCatalogs([ + { + name: "catalog1", + url: "some-url-1" + }, + { + name: "catalog3", + url: "some-url-3" + } + ]); + expect(fixture.loadCatalogStub.callCount, "adds catalog3").to.equal(4); + + await fixture.textRenderer.updateFontCatalogs([ + { + name: "catalog2", + url: "some-url-2" + } + ]); + expect( + fixture.loadCatalogStub.callCount, + "adds catalog2 back, removed in last step" + ).to.equal(5); + + await fixture.textRenderer.updateFontCatalogs([]); + expect(fixture.loadCatalogStub.callCount).to.equal(5); + + await fixture.textRenderer.updateFontCatalogs([ + { + name: DEFAULT_FONT_CATALOG_NAME, + url: "some-url" + } + ]); + expect(fixture.loadCatalogStub.callCount).to.equal(6); + + await fixture.textRenderer.updateFontCatalogs([ + { + name: DEFAULT_FONT_CATALOG_NAME, + url: "some-other-url" + } + ]); + expect(fixture.loadCatalogStub.callCount).to.equal(7); + }); }); diff --git a/@here/harp-mapview/test/TextElementsRendererTestFixture.ts b/@here/harp-mapview/test/TextElementsRendererTestFixture.ts index 2f30984838..290f72fbd4 100644 --- a/@here/harp-mapview/test/TextElementsRendererTestFixture.ts +++ b/@here/harp-mapview/test/TextElementsRendererTestFixture.ts @@ -6,7 +6,7 @@ import { MapEnv, Theme } from "@here/harp-datasource-protocol"; import { identityProjection, TileKey } from "@here/harp-geoutils"; import { silenceLoggingAroundFunction } from "@here/harp-test-utils"; -import { TextCanvas } from "@here/harp-text-canvas"; +import { FontCatalog, TextCanvas } from "@here/harp-text-canvas"; import { assert, expect } from "chai"; import * as sinon from "sinon"; import * as THREE from "three"; @@ -14,11 +14,12 @@ import * as THREE from "three"; import { PoiRenderer } from "../lib/poi/PoiRenderer"; import { ScreenCollisions } from "../lib/ScreenCollisions"; import { ScreenProjector } from "../lib/ScreenProjector"; +import * as FontCatalogLoader from "../lib/text/FontCatalogLoader"; +import { TextCanvasFactory } from "../lib/text/TextCanvasFactory"; import { TextElement } from "../lib/text/TextElement"; import { TextElementsRenderer } from "../lib/text/TextElementsRenderer"; import { TextElementsRendererOptions } from "../lib/text/TextElementsRendererOptions"; import { TextElementType } from "../lib/text/TextElementType"; -import { TextStyleCache } from "../lib/text/TextStyleCache"; import { ViewState } from "../lib/text/ViewState"; import { Tile } from "../lib/Tile"; import { TileOffsetUtils } from "../lib/Utils"; @@ -30,7 +31,6 @@ import { stubElevationProvider } from "./stubElevationProvider"; import { stubFontCatalog } from "./stubFontCatalog"; -import { stubFontCatalogLoader } from "./stubFontCatalogLoader"; import { stubPoiManager } from "./stubPoiManager"; import { stubPoiRenderer } from "./stubPoiRenderer"; import { stubTextCanvas, stubTextCanvasFactory } from "./stubTextCanvas"; @@ -106,13 +106,15 @@ export class TestFixture { private readonly m_addTextBufferObjSpy: sinon.SinonSpy; private readonly m_dataSource: FakeOmvDataSource = new FakeOmvDataSource({ name: "omv" }); private readonly m_screenProjector: ScreenProjector; - private readonly m_camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(); private readonly m_theme: Theme = {}; private m_viewState: ViewState; private m_options: TextElementsRendererOptions = {}; private m_screenCollisionTestStub: sinon.SinonStub | undefined; private m_textCanvasStub: TextCanvas | undefined; private m_textRenderer: TextElementsRenderer | undefined; + private m_loadCatalogStub: sinon.SinonStub | undefined; + private m_textCanvasFactoryStub: sinon.SinonStubbedInstance | undefined; + private m_fontCatalog: FontCatalog | undefined; private m_defaultTile: Tile | undefined; private m_allTiles: Tile[] = []; private readonly m_allTextElements: TextElement[][] = []; @@ -127,15 +129,12 @@ export class TestFixture { this.m_poiRendererStub = stubPoiRenderer(this.sandbox, this.m_renderPoiSpy); this.m_elevationProviderStub = stubElevationProvider(this.sandbox); this.m_screenProjector = createScreenProjector(); - this.syncCamera(); } /** * Sets up required before every test case. - * @returns A promise that resolves to true once the setup is finished, to false if there was an - * error. */ - setUp(): Promise { + async setUp(): Promise { this.m_defaultTile = this.m_dataSource.getTile(new TileKey(0, 0, TILE_LEVEL)); this.m_defaultTile.textElementsChanged = true; this.m_allTiles = []; @@ -152,34 +151,34 @@ export class TestFixture { labelDistanceScaleMin: 1, labelDistanceScaleMax: 1 }; - const fontCatalog = stubFontCatalog(this.sandbox); + this.m_fontCatalog = stubFontCatalog(this.sandbox); this.m_textCanvasStub = stubTextCanvas( this.sandbox, this.m_addTextSpy, this.m_addTextBufferObjSpy, - fontCatalog, + this.m_fontCatalog, DEF_TEXT_WIDTH, DEF_TEXT_HEIGHT ); const dummyUpdateCall = () => {}; + this.m_loadCatalogStub = this.sandbox.stub(FontCatalogLoader, "loadFontCatalog").resolves(); + + this.m_textCanvasFactoryStub = stubTextCanvasFactory(this.sandbox, this.m_textCanvasStub); this.m_textRenderer = new TextElementsRenderer( this.m_viewState, - this.m_camera, dummyUpdateCall, - this.m_screenCollisions, this.m_screenProjector, - stubTextCanvasFactory(this.sandbox, this.m_textCanvasStub), stubPoiManager(this.sandbox), + sinon.createStubInstance(THREE.WebGLRenderer), + [], + this.m_options, + (this.m_textCanvasFactoryStub as unknown) as TextCanvasFactory, (this.m_poiRendererStub as unknown) as PoiRenderer, - stubFontCatalogLoader(this.sandbox, fontCatalog), - new TextStyleCache(this.m_theme.textStyles), - this.m_options + this.m_screenCollisions ); - // Force renderer initialization by calling render with changed text elements. - const time = 0; - this.m_textRenderer.placeText(this.tileLists, time); - this.clearVisibleTiles(); - return this.m_textRenderer.waitInitialized(); + this.m_textRenderer.updateTextStyles(this.m_theme.textStyles); + this.m_loadCatalogStub.yieldOn("onSuccess", "default", this.m_fontCatalog); + return this.m_textRenderer.waitLoaded(); } /** @@ -348,11 +347,26 @@ export class TestFixture { }); } - private get textRenderer(): TextElementsRenderer { + get textRenderer(): TextElementsRenderer { assert(this.m_textRenderer !== undefined); return this.m_textRenderer!; } + get loadCatalogStub(): sinon.SinonStub { + assert(this.m_loadCatalogStub !== undefined); + return this.m_loadCatalogStub!; + } + + get fontCatalog(): FontCatalog { + assert(this.m_fontCatalog !== undefined); + return this.m_fontCatalog!; + } + + get textCanvasFactoryStub(): sinon.SinonStubbedInstance { + assert(this.m_textCanvasFactoryStub !== undefined); + return this.m_textCanvasFactoryStub!; + } + setElevationProvider(enabled: boolean) { this.m_viewState.elevationProvider = enabled ? this.m_elevationProviderStub : undefined; } @@ -635,12 +649,4 @@ export class TestFixture { const currentFrame = this.m_viewState.frameNumber - 2; return "Frame " + currentFrame + ", label '" + textElement.text + "': "; } - - private syncCamera() { - this.m_camera.lookAt( - this.m_camera.position.add( - this.m_viewState.lookAtVector.multiplyScalar(this.m_viewState.lookAtDistance) - ) - ); - } } diff --git a/@here/harp-mapview/test/stubFontCatalogLoader.ts b/@here/harp-mapview/test/stubFontCatalogLoader.ts deleted file mode 100644 index bae49857d8..0000000000 --- a/@here/harp-mapview/test/stubFontCatalogLoader.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2017-2020 HERE Europe B.V. - * Licensed under Apache 2.0, see full license in LICENSE - * SPDX-License-Identifier: Apache-2.0 - */ - -import { FontCatalog } from "@here/harp-text-canvas"; -import * as sinon from "sinon"; - -import { DEFAULT_FONT_CATALOG_NAME, FontCatalogLoader } from "../lib/text/FontCatalogLoader"; - -/** - * Stubs font catalog loader. - * @param sandbox - Sinon sandbox to keep track of created stubs. - * @param fontCatalog - Font catalog the loader will always return. - * @returns FontCatalogLoader stub. - */ -export function stubFontCatalogLoader( - sandbox: sinon.SinonSandbox, - fontCatalog: FontCatalog -): FontCatalogLoader { - const fontCatalogLoaderStub = sinon.createStubInstance(FontCatalogLoader); - - sandbox.stub(fontCatalogLoaderStub, "loading").get(() => { - return false; - }); - - fontCatalogLoaderStub.loadCatalogs.yields(DEFAULT_FONT_CATALOG_NAME, fontCatalog).resolves([]); - - return (fontCatalogLoaderStub as unknown) as FontCatalogLoader; -} diff --git a/@here/harp-mapview/test/stubTextCanvas.ts b/@here/harp-mapview/test/stubTextCanvas.ts index b1dfd730c2..4c05ccf5de 100644 --- a/@here/harp-mapview/test/stubTextCanvas.ts +++ b/@here/harp-mapview/test/stubTextCanvas.ts @@ -110,9 +110,9 @@ export function stubTextCanvas( export function stubTextCanvasFactory( sandbox: sinon.SinonSandbox, textCanvas: TextCanvas -): TextCanvasFactory { +): sinon.SinonStubbedInstance { const textCanvasFactoryStub = sandbox.createStubInstance(TextCanvasFactory); textCanvasFactoryStub.createTextCanvas.returns((textCanvas as unknown) as TextCanvas); - return (textCanvasFactoryStub as unknown) as TextCanvasFactory; + return textCanvasFactoryStub; } diff --git a/@here/harp-text-canvas/test/rendering/TextCanvasRenderingTest.ts b/@here/harp-text-canvas/test/rendering/TextCanvasRenderingTest.ts index 31bad76746..bcf07bea75 100644 --- a/@here/harp-text-canvas/test/rendering/TextCanvasRenderingTest.ts +++ b/@here/harp-text-canvas/test/rendering/TextCanvasRenderingTest.ts @@ -55,7 +55,7 @@ describe("TextCanvas", function() { camera.updateProjectionMatrix(); fontCatalog = await FontCatalog.load( - "../@here/harp-fontcatalog/resources/Default_FontCatalog.json", + "../dist/resources/fonts/Default_FontCatalog.json", 16 ); diff --git a/@here/harp-vectortile-datasource/test/MapViewPickingTest.ts b/@here/harp-vectortile-datasource/test/MapViewPickingTest.ts index 8fcafa3712..8f70aa4c08 100644 --- a/@here/harp-vectortile-datasource/test/MapViewPickingTest.ts +++ b/@here/harp-vectortile-datasource/test/MapViewPickingTest.ts @@ -190,9 +190,10 @@ describe("MapView Picking", async function() { }); await waitForEvent(mapView, MapViewEventNames.ThemeLoaded); - sinon - .stub(mapView.textElementsRenderer, "renderText") - .callsFake((_camera: THREE.OrthographicCamera) => {}); + if (mapView.textElementsRenderer.loading) { + await mapView.textElementsRenderer.waitLoaded(); + } + sinon.stub(mapView.textElementsRenderer, "renderText").callsFake((_farPlane: number) => {}); const geoJsonDataProvider = new GeoJsonDataProvider("italy_test", GEOJSON_DATA, { tiler: new GeoJsonTiler()