diff --git a/example/three/annotationsExample.js b/example/three/annotationsExample.js index c4d1af7ac..2aadddde2 100644 --- a/example/three/annotationsExample.js +++ b/example/three/annotationsExample.js @@ -102,8 +102,15 @@ const KIND_TO_ICON = { }; +// localized name variants exposed by the protomaps v4 basemap +const LANGUAGES = [ 'default', 'en', 'ja', 'ko' ]; + const params = { + language: 'default', + displayIcons: true, + displayPaths: true, + occupancyGrid: false, annotationLines: false, tileHierarchy: false, @@ -128,6 +135,10 @@ class ExampleAnnotationsDriver extends MVTAnnotationsDriver { super(); + this.language = 'default'; + this.displayIcons = true; + this.displayPaths = true; + const dpr = renderer.getPixelRatio(); // icons for point annotations @@ -190,6 +201,27 @@ class ExampleAnnotationsDriver extends MVTAnnotationsDriver { } + isAnnotationEnabled( layer, properties, type ) { + + if ( type === 1 && this.displayIcons ) return true; + if ( type === 2 && this.displayPaths ) return true; + return false; + + } + + getText( properties ) { + + const { language } = this; + if ( language !== 'default' ) { + + return properties[ `name:${ language }` ] ?? properties.name ?? ''; + + } + + return properties.name ?? ''; + + } + measureChar( char ) { return this.characterPoints.measureChar( char ); @@ -264,6 +296,7 @@ function reinstantiateTiles() { tiles.registerPlugin( new TilesFadePlugin() ); tiles.registerPlugin( new MeshBVHPlugin() ); driver = new ExampleAnnotationsDriver(); + driver.language = params.language; tiles.registerPlugin( new MVTAnnotationsPlugin( { overlay, camera, @@ -331,17 +364,37 @@ function init() { // GUI const gui = new GUI(); - gui.add( params, 'occupancyGrid' ).onChange( v => { + gui.add( params, 'language', LANGUAGES ).onChange( v => { + + driver.language = v; + tiles.getPluginByName( 'UPDATE_ON_CHANGE_PLUGIN' ).needsUpdate = true; + + } ); + gui.add( params, 'displayIcons' ).onChange( v => { + + driver.displayIcons = v; + tiles.getPluginByName( 'UPDATE_ON_CHANGE_PLUGIN' ).needsUpdate = true; + + } ); + gui.add( params, 'displayPaths' ).onChange( v => { + + driver.displayPaths = v; + tiles.getPluginByName( 'UPDATE_ON_CHANGE_PLUGIN' ).needsUpdate = true; + + } ); + + const debugFolder = gui.addFolder( 'Debug' ); + debugFolder.add( params, 'occupancyGrid' ).onChange( v => { tiles.getPluginByName( 'MVT_ANNOTATIONS_PLUGIN' ).debug.occupancy.enabled = v; } ); - gui.add( params, 'annotationLines' ).onChange( v => { + debugFolder.add( params, 'annotationLines' ).onChange( v => { tiles.getPluginByName( 'MVT_ANNOTATIONS_PLUGIN' ).debug.paths.enabled = v; } ); - gui.add( params, 'tileHierarchy' ).onChange( v => { + debugFolder.add( params, 'tileHierarchy' ).onChange( v => { tiles.getPluginByName( 'MVT_ANNOTATIONS_PLUGIN' ).debug.hierarchy.enabled = v; diff --git a/example/three/src/plugins/mvt/CharacterPoints.js b/example/three/src/plugins/mvt/CharacterPoints.js index 864c69b24..2cdf334aa 100644 --- a/example/three/src/plugins/mvt/CharacterPoints.js +++ b/example/three/src/plugins/mvt/CharacterPoints.js @@ -23,16 +23,18 @@ export class CharacterPoints extends GlyphPoints { const fontSize = Math.round( glyphSize * 0.7 ); this._font = font ?? `400 ${ fontSize }px sans-serif`; - // canvas context for measuring advance widths, normalized to em units ( width / fontSize ) + // advance-width cache, keyed per character this._advanceCache = new Map(); // styling this._strokeStyle = strokeStyle; this._strokeWidth = strokeWidth; - // the refs and unused list of the character glyphs - this._refs = {}; - this._unused = new Set(); + // characters currently rasterized in the atlas, plus a reused scratch set of the characters + // needed this frame. the atlas is reconciled against what's actually rendered ( not against + // add / remove events ), so it stays correct even when an annotation's text changes. + this._drawn = new Set(); + this._needed = new Set(); this.glyphAtlas.resize( slotCount, glyphSize ); @@ -56,89 +58,76 @@ export class CharacterPoints extends GlyphPoints { } - update( added, removed ) { + // rasterize a character into the atlas, evicting a glyph that isn't needed this frame ( or + // growing the atlas if every rasterized glyph is still in use ) + _drawChar( char, needed ) { - // TODO: if the "text" suddenly changes (eg from a language change) this will break - // since we will never get a "removed" event associated with the original text string. - // We may have to track the characters that have been used in a geometry update, instead. - super.update( added, removed ); - const { _refs, _unused, _added, _removed, glyphAtlas } = this; + const { glyphAtlas, _drawn } = this; + if ( glyphAtlas.capacity === glyphAtlas.count ) { - // iterate over all the added annotations and increment the ref, drawing the - // glyph to the atlas if needed. - _added.forEach( ( { text } ) => { + let toRemove = null; + for ( const c of _drawn ) { - for ( let i = 0, l = text.length; i < l; i ++ ) { + if ( ! needed.has( c ) ) { - const char = text[ i ]; - if ( ! ( char in _refs ) ) { + toRemove = c; + break; - // if we've reached our capacity - if ( glyphAtlas.capacity === glyphAtlas.count ) { - - if ( _unused.size > 0 ) { - - // if there are unused slots then free up one of those - const unusedChar = _unused.values().next().value; - glyphAtlas.release( unusedChar ); - delete _refs[ unusedChar ]; - _unused.delete( unusedChar ); - - } else { - - // otherwise resize the atlas to fit more glyphs - glyphAtlas.resize( glyphAtlas.capacity * 2 ); + } - } + } - } + if ( toRemove !== null ) { - // draw the glyph - _refs[ char ] = 0; - glyphAtlas.drawChar( char, char, { - font: this._font, - color: 'white', - strokeStyle: this._strokeStyle, - strokeWidth: this._strokeWidth, - } ); + glyphAtlas.release( toRemove ); + _drawn.delete( toRemove ); - } + } else { - _unused.delete( char ); - _refs[ char ] ++; + glyphAtlas.resize( glyphAtlas.capacity * 2 ); } + } + + glyphAtlas.drawChar( char, char, { + font: this._font, + color: 'white', + strokeStyle: this._strokeStyle, + strokeWidth: this._strokeWidth, } ); + _drawn.add( char ); - // decrement the refs and queue up any characters as unused if they reach 0 - _removed.forEach( ( { text } ) => { + } - for ( let i = 0, l = text.length; i < l; i ++ ) { + _updateGeometry() { - const char = text[ i ]; - _refs[ char ] --; - if ( _refs[ char ] === 0 ) { + const { _orderedEntries, _needed, geometry, position, glyphAtlas } = this; - _unused.add( char ); + // collect the characters needed this frame from each items text, and the total glyph + // count + _needed.clear(); + let count = 0; + for ( const entry of _orderedEntries ) { - } + const { text, characterPositions } = entry.item; + count += characterPositions.length; + for ( let c = 0, l = text.length; c < l; c ++ ) { - } + _needed.add( text[ c ] ); - } ); + } - } + } - _updateGeometry() { + // make sure every needed character is rasterized before we look up its uv + for ( const char of _needed ) { - const { _orderedEntries, geometry, position, glyphAtlas } = this; + if ( ! glyphAtlas.has( char ) ) { - // total character count across all entries ( including fading ones ) - let count = 0; - for ( const entry of _orderedEntries ) { + this._drawChar( char, _needed ); - count += entry.item.characterPositions.length; + } } diff --git a/example/three/src/plugins/mvt/GlyphPoints.js b/example/three/src/plugins/mvt/GlyphPoints.js index 5b20587ab..aada1f3bc 100644 --- a/example/three/src/plugins/mvt/GlyphPoints.js +++ b/example/three/src/plugins/mvt/GlyphPoints.js @@ -38,8 +38,6 @@ export class GlyphPoints extends Points { this._entryMap = new Map(); this._orderedEntries = []; this._lastUpdateTime = - 1; - this._added = []; - this._removed = []; } @@ -58,9 +56,7 @@ export class GlyphPoints extends Points { const dt = this._lastUpdateTime < 0 ? 0 : Math.min( now - this._lastUpdateTime, 0.1 ); this._lastUpdateTime = now; - const { _entryMap, _orderedEntries, _added, _removed, fadeInDuration, fadeOutDuration } = this; - _added.length = 0; - _removed.length = 0; + const { _entryMap, _orderedEntries, fadeInDuration, fadeOutDuration } = this; for ( const item of added ) { @@ -70,7 +66,6 @@ export class GlyphPoints extends Points { const entry = { item, fade: 0, state: 'in' }; _entryMap.set( item.id, entry ); _orderedEntries.push( entry ); - _added.push( item ); } else { @@ -93,6 +88,7 @@ export class GlyphPoints extends Points { } + let didRemove = false; for ( const [ id, entry ] of _entryMap ) { if ( entry.state === 'in' ) { @@ -110,7 +106,7 @@ export class GlyphPoints extends Points { if ( entry.fade <= 0 ) { _entryMap.delete( id ); - _removed.push( entry.item ); + didRemove = true; } @@ -118,7 +114,7 @@ export class GlyphPoints extends Points { } - if ( _removed.length > 0 ) { + if ( didRemove ) { this._orderedEntries = _orderedEntries.filter( e => _entryMap.has( e.item.id ) ); @@ -126,8 +122,6 @@ export class GlyphPoints extends Points { this._updateGeometry(); - return _removed; - } onAfterRender( renderer, scene, camera ) { diff --git a/src/three/plugins/images/sources/PMTilesImageSource.js b/src/three/plugins/images/sources/PMTilesImageSource.js index adade0ebe..637710db2 100644 --- a/src/three/plugins/images/sources/PMTilesImageSource.js +++ b/src/three/plugins/images/sources/PMTilesImageSource.js @@ -62,7 +62,11 @@ class PMTilesContentCache extends MVTContentCache { getKey: () => this.url, getBytes: async ( offset, length, signal ) => { - signal.aborted.throwIfAborted(); + if ( signal ) { + + signal.throwIfAborted(); + + } const { fetchOptions, url } = this; const res = await this.fetchData( url, { diff --git a/src/three/plugins/mvt/MVTAnnotationsPlugin.js b/src/three/plugins/mvt/MVTAnnotationsPlugin.js index 92f6c83a8..691911cff 100644 --- a/src/three/plugins/mvt/MVTAnnotationsPlugin.js +++ b/src/three/plugins/mvt/MVTAnnotationsPlugin.js @@ -113,6 +113,19 @@ export class MVTAnnotationsDriver { } + /** + * Whether a parsed annotation should currently be displayed. Unlike `filterAnnotation` which + * decides what is parsed once. + * @param {Object} properties - The feature's property map. + * @param {number} type - The MVT geometry type: `1` = point, `2` = line. + * @returns {boolean} True to display the annotation. + */ + isAnnotationEnabled( properties, type ) { + + return true; + + } + /** * Called each frame with the annotations whose visibility changed, for the caller to render. * @param {Set} added - Annotations that became visible this frame. @@ -166,10 +179,9 @@ export class MVTAnnotationsPlugin { this.camera = camera; this.driver = driver; - // stable bound callbacks handed to sub-objects that invoke them ( preserves driver `this` ) + // annotations call these live each frame so driver changes take effect immediately this._measureChar = char => this.driver.measureChar( char ); this._filterAnnotation = ( layer, properties, type ) => this.driver.filterAnnotation( layer, properties, type ); - this._getText = properties => this.driver.getText( properties ); // hierarchy for managing tile loading and visibility this.hierarchy = new MVTHierarchy(); @@ -291,8 +303,23 @@ export class MVTAnnotationsPlugin { this._onUpdateAfter = () => { + const { driver, camera } = this; + + // Recalculate the field state per annotation + for ( const annotation of pointManager.points ) { + + annotation.enabled = driver.isAnnotationEnabled( annotation.layer, annotation.properties, 1 ); + + } + + for ( const line of anchorManager.lines ) { + + line.enabled = driver.isAnnotationEnabled( line.layer, line.properties, 2 ); + line.text = driver.getText( line.properties ); + + } + // sync camera and localToWorld matrix into occupancy grid - const { camera } = this; if ( camera !== null ) { tiles.getResolution( camera, occupancy.resolution ); @@ -324,7 +351,6 @@ export class MVTAnnotationsPlugin { anchorManager.added.forEach( item => { item.measureChar = this._measureChar; - item.getText = this._getText; occupancy.register( item ); } ); diff --git a/src/three/plugins/mvt/ScreenOccupationManager.js b/src/three/plugins/mvt/ScreenOccupationManager.js index 4a809e27b..840622338 100644 --- a/src/three/plugins/mvt/ScreenOccupationManager.js +++ b/src/three/plugins/mvt/ScreenOccupationManager.js @@ -8,6 +8,7 @@ export class OccupancyAnnotation { constructor() { + this.enabled = true; this.id = ''; this.layer = ''; this.properties = null; @@ -240,7 +241,10 @@ export class ScreenOccupationManager extends EventDispatcher { const item = items[ i ]; this._id = i + 1; - if ( ndcMatrix !== null && item.evaluate( handle ) ) { + + // disabled items ( filtered out by the driver ) are skipped so they fall out of the + // visible set and fade out via the delayed manager, without being unregistered + if ( ndcMatrix !== null && item.enabled && item.evaluate( handle ) ) { visible.add( item ); if ( ! prevVisible.has( item ) ) { diff --git a/src/three/plugins/mvt/SettlingManager.js b/src/three/plugins/mvt/SettlingManager.js index 2fbccf412..074170956 100644 --- a/src/three/plugins/mvt/SettlingManager.js +++ b/src/three/plugins/mvt/SettlingManager.js @@ -316,6 +316,14 @@ export class SettlingManager { } + // skip disabled items and mark them unsettled so they re-settle if re-enabled + if ( ! item.enabled ) { + + item.ready = false; + continue; + + } + yield* this._settleItem( item ); // yield between items once the budget is spent diff --git a/src/three/plugins/mvt/TextAnchorManager.js b/src/three/plugins/mvt/TextAnchorManager.js index c05bd55f1..b88ac33ce 100644 --- a/src/three/plugins/mvt/TextAnchorManager.js +++ b/src/three/plugins/mvt/TextAnchorManager.js @@ -11,6 +11,11 @@ export class TextAnchorManager { this._anchorsById = new Map(); this._linesById = new Map(); + // flat sets of every tracked line / anchor, maintained on add / delete for allocation-free + // iteration + this.lines = new Set(); + this.anchors = new Set(); + } reset() { @@ -31,6 +36,7 @@ export class TextAnchorManager { if ( anchor.isEmpty() ) { anchors.delete( anchor ); + this.anchors.delete( anchor ); removed.add( anchor ); } @@ -48,42 +54,6 @@ export class TextAnchorManager { } - // retrieve all lines - getLines() { - - const res = []; - this._linesById.forEach( value => { - - value.forEach( line => { - - res.push( line ); - - } ); - - } ); - - return res; - - } - - // retrieve all anchors - getAnchors() { - - const target = []; - this._anchorsById.forEach( anchors => { - - anchors.forEach( anchor => { - - target.push( anchor ); - - } ); - - } ); - - return target; - - } - // add a set of lines // NOTE: This is is designed to be called with all lines from a single tile at once addLines( lines ) { @@ -188,6 +158,7 @@ export class TextAnchorManager { anchorPosition.ref = anchor; anchorSet.add( anchor ); + this.anchors.add( anchor ); added.add( anchor ); } @@ -207,6 +178,7 @@ export class TextAnchorManager { lines.forEach( line => { lineSet.add( line ); + this.lines.add( line ); } ); @@ -227,6 +199,7 @@ export class TextAnchorManager { const id = line.id; _linesById.get( id ).delete( line ); + this.lines.delete( line ); if ( _linesById.get( id ).size === 0 ) { _linesById.delete( id ); diff --git a/src/three/plugins/mvt/annotations/LineAnnotation.js b/src/three/plugins/mvt/annotations/LineAnnotation.js index d968a0c12..538241a8f 100644 --- a/src/three/plugins/mvt/annotations/LineAnnotation.js +++ b/src/three/plugins/mvt/annotations/LineAnnotation.js @@ -22,6 +22,9 @@ export class LineAnnotation extends OccupancyAnnotation { super(); + // display text for this path + this.text = ''; + // the range of the tile this line is associated with this.range = null; diff --git a/src/three/plugins/mvt/annotations/PointAnnotationManager.js b/src/three/plugins/mvt/annotations/PointAnnotationManager.js index 852f46ef4..504bee5b3 100644 --- a/src/three/plugins/mvt/annotations/PointAnnotationManager.js +++ b/src/three/plugins/mvt/annotations/PointAnnotationManager.js @@ -6,24 +6,27 @@ export class PointAnnotationManager { this.added = new Set(); this.removed = new Set(); - this.annotations = new Map(); + this.points = new Set(); + this._annotationsById = new Map(); + } add( annotation ) { - const { annotations, added } = this; + const { _annotationsById, points, added } = this; const { id } = annotation; - if ( ! annotations.has( id ) ) { + if ( ! _annotationsById.has( id ) ) { - annotations.set( id, { annotation, ref: 0 } ); + _annotationsById.set( id, { annotation, ref: 0 } ); + points.add( annotation ); added.add( annotation ); } else { // refine the position of the canonical annotation if the new one is from a // higher LoD. - const canonicalAnnotation = annotations.get( id ).annotation; + const canonicalAnnotation = _annotationsById.get( id ).annotation; if ( annotation.lodLevel > canonicalAnnotation.lodLevel ) { canonicalAnnotation.lodLevel = annotation.lodLevel; @@ -34,15 +37,15 @@ export class PointAnnotationManager { } - annotations.get( id ).ref ++; + _annotationsById.get( id ).ref ++; } delete( annotation ) { - const { annotations } = this; + const { _annotationsById } = this; const { id } = annotation; - const info = annotations.get( id ); + const info = _annotationsById.get( id ); info.ref --; @@ -50,13 +53,14 @@ export class PointAnnotationManager { update() { - const { removed, annotations } = this; - annotations.forEach( ( info, id ) => { + const { removed, points, _annotationsById } = this; + _annotationsById.forEach( ( info, id ) => { if ( info.ref === 0 ) { removed.add( info.annotation ); - annotations.delete( id ); + points.delete( info.annotation ); + _annotationsById.delete( id ); } diff --git a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js index 25c2017c5..b8838f312 100644 --- a/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js +++ b/src/three/plugins/mvt/annotations/TextAnchorAnnotation.js @@ -27,12 +27,6 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { } - get text() { - - return this.getText( this.properties ); - - } - get ready() { return this.getActiveReference().line.ready; @@ -49,6 +43,20 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { set properties( value ) {} + get enabled() { + + return this.getActiveReference().line.enabled; + + } + + set enabled( value ) {} + + get text() { + + return this.getActiveReference().line.text; + + } + constructor( id ) { super(); @@ -69,9 +77,6 @@ export class TextAnchorAnnotation extends OccupancyAnnotation { // per-character advance width provider (pixels) this.measureChar = () => 1; - // text provider — returns the string to display for the line's properties - this.getText = properties => properties.name ?? ''; - // total advance width of the label ( screen px ) and per-character footprint radius, // recomputed each layout ( individual advances come from the cached measureChar ) this._totalWidth = 0; diff --git a/src/three/plugins/mvt/debug/LineAnnotationOverlay.js b/src/three/plugins/mvt/debug/LineAnnotationOverlay.js index 7d3ecbac4..a406f9879 100644 --- a/src/three/plugins/mvt/debug/LineAnnotationOverlay.js +++ b/src/three/plugins/mvt/debug/LineAnnotationOverlay.js @@ -122,7 +122,7 @@ export class LineAnnotationOverlay { } // settled line paths → segment buffer - const lineItems = anchorManager.getLines().filter( item => item instanceof LineAnnotation && item.ready ); + const lineItems = Array.from( anchorManager.lines ).filter( item => item instanceof LineAnnotation && item.ready ); let segmentCount = 0; for ( const line of lineItems ) { @@ -153,7 +153,7 @@ export class LineAnnotationOverlay { } // anchors at their active ( highest-LoD settled ) path → point buffer - const anchorItems = anchorManager.getAnchors().filter( anchor => anchor.ready ); + const anchorItems = Array.from( anchorManager.anchors ).filter( anchor => anchor.ready ); const pointsPosAttr = new BufferAttribute( new Float32Array( anchorItems.length * 3 ), 3 ); const pointsColAttr = new BufferAttribute( new Float32Array( anchorItems.length * 2 * 3 ), 3 );