import {extend} from '../util/util.ts'; import type {Tile} from '../tile/tile.ts'; import type {FeatureState} from '@maplibre/maplibre-gl-style-spec'; import type {InViewTiles} from '../tile/tile_manager_in_view_tiles.ts'; import type {Painter} from '../render/painter.ts'; export type FeatureStateEntry = {id: string; state: FeatureState}; export type FeatureStates = FeatureStateEntry[]; export type LayerFeatureStates = Record; type FeatureStatesMap = Record; type LayerFeatureStatesMap = Record; function featureStatesMapToArray(map: FeatureStatesMap): FeatureStates { const result: FeatureStates = []; for (const id in map) { result.push({id, state: map[id]}); } return result; } /** * @internal * SourceFeatureState manages the state and pending changes * to features in a source, separated by source layer. * stateChanges and deletedStates batch all changes to the tile (updates and removes, respectively) * between coalesce() events. addFeatureState() and removeFeatureState() also update their counterpart's * list of changes, such that coalesce() can apply the proper state changes while agnostic to the order of operations. * In deletedStates, all null's denote complete removal of state at that scope */ export class SourceFeatureState { state: LayerFeatureStatesMap; stateChanges: LayerFeatureStatesMap; deletedStates: {}; revision: number; constructor() { this.state = {}; this.stateChanges = {}; this.deletedStates = {}; this.revision = 0; } updateState(sourceLayer: string, featureId: number | string, newState: any): void { const feature = String(featureId); this.stateChanges[sourceLayer] ||= {}; this.stateChanges[sourceLayer][feature] ||= {}; extend(this.stateChanges[sourceLayer][feature], newState); if (this.deletedStates[sourceLayer] === null) { this.deletedStates[sourceLayer] = {}; for (const ft in this.state[sourceLayer]) { if (ft === feature){ this.deletedStates[sourceLayer][feature] = {}; for (const prop in this.state[sourceLayer][ft]){ if (newState[prop] === undefined) this.deletedStates[sourceLayer][feature][prop] = null; } } else { this.deletedStates[sourceLayer][ft] = null; } } } else { const featureDeletionQueued = this.deletedStates[sourceLayer]?.[feature] === null; if (featureDeletionQueued) { this.deletedStates[sourceLayer][feature] = {}; for (const prop in this.state[sourceLayer][feature]) { if (newState[prop] === undefined) this.deletedStates[sourceLayer][feature][prop] = null; } } else { for (const key in newState) { const deletionInQueue = this.deletedStates[sourceLayer]?.[feature]?.[key] === null; if (deletionInQueue) delete this.deletedStates[sourceLayer][feature][key]; } } } } removeFeatureState(sourceLayer: string, featureId?: number | string, key?: string): void { const sourceLayerDeleted = this.deletedStates[sourceLayer] === null; if (sourceLayerDeleted) return; const feature = String(featureId); this.deletedStates[sourceLayer] ||= {}; if (key && featureId !== undefined) { if (this.deletedStates[sourceLayer][feature] !== null) { this.deletedStates[sourceLayer][feature] ||= {}; this.deletedStates[sourceLayer][feature][key] = null; } } else if (featureId !== undefined) { const updateInQueue = this.stateChanges[sourceLayer]?.[feature]; if (updateInQueue) { this.deletedStates[sourceLayer][feature] = {}; for (key in this.stateChanges[sourceLayer][feature]) this.deletedStates[sourceLayer][feature][key] = null; } else { this.deletedStates[sourceLayer][feature] = null; } } else { this.deletedStates[sourceLayer] = null; } } getState(sourceLayer: string, featureId: number | string): FeatureState { const feature = String(featureId); const base = this.state[sourceLayer] || {}; const changes = this.stateChanges[sourceLayer] || {}; const reconciledState = extend({}, base[feature], changes[feature]); //return empty object if the whole source layer is awaiting deletion if (this.deletedStates[sourceLayer] === null) return {}; else if (this.deletedStates[sourceLayer]) { const featureDeletions = this.deletedStates[sourceLayer][featureId]; if (featureDeletions === null) return {}; for (const prop in featureDeletions) delete reconciledState[prop]; } return reconciledState; } initializeTileState(tile: Tile, painter: Painter): void { const layerStates: LayerFeatureStates = {}; for (const sourceLayer in this.state) { layerStates[sourceLayer] = featureStatesMapToArray(this.state[sourceLayer]); } tile.setFeatureState(layerStates, painter, this.revision); } coalesceChanges(inViewTiles: InViewTiles, painter: Painter): void { //track changes with full state objects, but only for features that got modified //use an intermediate object keyed by feature id to naturally deduplicate entries const featuresChangedMap: LayerFeatureStatesMap = {}; for (const sourceLayer in this.stateChanges) { this.state[sourceLayer] ||= {}; featuresChangedMap[sourceLayer] ||= {}; for (const feature in this.stateChanges[sourceLayer]) { this.state[sourceLayer][feature] ||= {}; extend(this.state[sourceLayer][feature], this.stateChanges[sourceLayer][feature]); featuresChangedMap[sourceLayer][feature] = this.state[sourceLayer][feature]; } } for (const sourceLayer in this.deletedStates) { this.state[sourceLayer] ||= {}; featuresChangedMap[sourceLayer] ||= {}; if (this.deletedStates[sourceLayer] === null) { for (const ft in this.state[sourceLayer]) { this.state[sourceLayer][ft] = {}; featuresChangedMap[sourceLayer][ft] = {}; } } else { for (const feature in this.deletedStates[sourceLayer]) { const deleteWholeFeatureState = this.deletedStates[sourceLayer][feature] === null; if (deleteWholeFeatureState) this.state[sourceLayer][feature] = {}; else { for (const key of Object.keys(this.deletedStates[sourceLayer][feature])) { delete this.state[sourceLayer][feature][key]; } } featuresChangedMap[sourceLayer][feature] = this.state[sourceLayer][feature]; } } } this.stateChanges = {}; this.deletedStates = {}; if (Object.keys(featuresChangedMap).length === 0) return; this.revision++; const featuresChanged: LayerFeatureStates = {}; for (const sourceLayer in featuresChangedMap) { featuresChanged[sourceLayer] = featureStatesMapToArray(featuresChangedMap[sourceLayer]); } inViewTiles.setFeatureState(featuresChanged, painter, this.revision); } }