diff --git a/modules/behaviors/DragBehavior.js b/modules/behaviors/DragBehavior.js index 250495667..1383495b0 100644 --- a/modules/behaviors/DragBehavior.js +++ b/modules/behaviors/DragBehavior.js @@ -80,8 +80,7 @@ export class DragBehavior extends AbstractBehavior { const target = this.dragTarget; if (eventData && target) { - target.layer.unclassData(target.dataID, 'active'); - target.feature.active = false; + target.feature.allowInteraction = true; this.dragTarget = null; this.emit('cancel', eventData); } @@ -174,8 +173,7 @@ export class DragBehavior extends AbstractBehavior { // This lets us catch events for what other objects it passes over as the user drags it. const target = Object.assign({}, down.target); // shallow copy this.dragTarget = target; - target.layer.classData(target.dataID, 'active'); - target.feature.active = true; + target.feature.allowInteraction = false; // What are we dragging? const data = target.data; @@ -244,14 +242,12 @@ export class DragBehavior extends AbstractBehavior { } } - this.lastDown = null; this.lastMove = null; const target = this.dragTarget; if (target) { - target.layer.unclassData(target.dataID, 'active'); - target.feature.active = false; + target.feature.allowInteraction = true; this.dragTarget = null; this.emit('end', up); } @@ -273,8 +269,7 @@ export class DragBehavior extends AbstractBehavior { const target = this.dragTarget; if (target) { - target.layer.unclassData(target.dataID, 'active'); - target.feature.active = false; + target.feature.allowInteraction = true; this.dragTarget = null; this.emit('cancel', cancel); } diff --git a/modules/core/PhotoSystem.js b/modules/core/PhotoSystem.js index 5fc7ab0c4..a46599d43 100644 --- a/modules/core/PhotoSystem.js +++ b/modules/core/PhotoSystem.js @@ -313,7 +313,7 @@ export class PhotoSystem extends AbstractSystem { // Clear out any existing display classes for (const oldLayerID of this._LAYERIDS) { const oldLayer = scene.layers.get(oldLayerID); - oldLayer?.clearClass('selected'); + oldLayer?.clearClass('select'); oldLayer?.clearClass('selectphoto'); } @@ -324,8 +324,8 @@ export class PhotoSystem extends AbstractSystem { // If we're selecting a photo then make sure its layer is enabled too. scene.enableLayers(layerID); - scene.classData(layerID, photoID, 'selected'); - scene.classData(layerID, photoID, 'selectphoto'); + scene.setClass('select', layerID, photoID); + scene.setClass('selectphoto', layerID, photoID); // Try to show the viewer with the image selected.. service.startAsync() diff --git a/modules/modes/DragNodeMode.js b/modules/modes/DragNodeMode.js index b8ea71298..57adbb4e7 100644 --- a/modules/modes/DragNodeMode.js +++ b/modules/modes/DragNodeMode.js @@ -54,6 +54,7 @@ export class DragNodeMode extends AbstractMode { const editor = context.systems.editor; const filters = context.systems.filters; const l10n = context.systems.l10n; + const map = context.systems.map; const ui = context.systems.ui; this._reselectIDs = options.reselectIDs ?? []; @@ -99,12 +100,12 @@ export class DragNodeMode extends AbstractMode { this.dragNode = entity; this._startLoc = entity.loc; + this._selectedData.set(entity.id, entity); - // Set the 'drawing' class so that the dragNode and any parent ways won't emit events - const scene = context.scene(); - scene.classData('osm', this.dragNode.id, 'drawing'); + const layer = map.scene.layers.get('osm'); + layer.setClass('drawing', this.dragNode.id); for (const parent of graph.parentWays(this.dragNode)) { - scene.classData('osm', parent.id, 'drawing'); + layer.setClass('drawing', parent.id); } // `_clickLoc` is used later to calculate a drag offset, @@ -143,8 +144,9 @@ export class DragNodeMode extends AbstractMode { this._selectedData.clear(); const context = this.context; - - context.scene().clearClass('drawing'); + const map = context.systems.map; + const layer = map.scene.layers.get('osm'); + layer.clearClass('drawing'); context.behaviors.drag .off('move', this._move) diff --git a/modules/modes/DragNoteMode.js b/modules/modes/DragNoteMode.js index e5f4215b7..d7140d565 100644 --- a/modules/modes/DragNoteMode.js +++ b/modules/modes/DragNoteMode.js @@ -51,10 +51,6 @@ export class DragNoteMode extends AbstractMode { this._startLoc = note.loc; this._selectedData.set(this.dragNote.id, this.dragNote); - // Set the 'drawing' class so that the dragNote won't emit events - const scene = context.scene(); - scene.classData('notes', this.dragNote.id, 'drawing'); - // `_clickLoc` is used later to calculate a drag offset, // to correct for where "on the pin" the user grabbed the target. const point = context.behaviors.drag.lastDown.coord.map; @@ -88,9 +84,6 @@ export class DragNoteMode extends AbstractMode { this._selectedData.clear(); const context = this.context; - - context.scene().clearClass('drawing'); - context.behaviors.drag .off('move', this._move) .off('end', this._end) diff --git a/modules/modes/DrawAreaMode.js b/modules/modes/DrawAreaMode.js index 4bd6cf5e7..0e374db80 100644 --- a/modules/modes/DrawAreaMode.js +++ b/modules/modes/DrawAreaMode.js @@ -129,9 +129,10 @@ export class DrawAreaMode extends AbstractMode { const context = this.context; const editor = context.systems.editor; - const scene = context.systems.map.scene; + const map = context.systems.map; + const layer = map.scene.layers.get('osm'); + const eventManager = map.renderer.events; - const eventManager = context.systems.map.renderer.events; eventManager.setCursor('grab'); context.behaviors.hover @@ -177,7 +178,8 @@ export class DrawAreaMode extends AbstractMode { this._lastPoint = null; this._selectedData.clear(); - scene.clearClass('drawing'); + + layer.clearClass('drawing'); window.setTimeout(() => { context.behaviors.mapInteraction.doubleClickEnabled = true; @@ -196,9 +198,10 @@ export class DrawAreaMode extends AbstractMode { _refreshEntities() { const context = this.context; const editor = context.systems.editor; - const scene = context.systems.map.scene; + const map = context.systems.map; + const layer = map.scene.layers.get('osm'); - scene.clearClass('drawing'); + layer.clearClass('drawing'); this._selectedData.clear(); const graph = editor.staging.graph; @@ -209,20 +212,19 @@ export class DrawAreaMode extends AbstractMode { // Sanity check - Bail out if any of these are missing. if (!drawWay || !lastNode || !firstNode) { - // debugger; this._cancel(); return; } // `drawNode` may or may not exist, it will be recreated after the user moves the pointer. if (drawNode) { - scene.classData('osm', drawNode.id, 'drawing'); + layer.setClass('drawing', drawNode.id); // Nudging at the edge of the map is allowed after the drawNode exists. context.behaviors.mapNudge.allow(); } - scene.classData('osm', drawWay.id, 'drawing'); + layer.setClass('drawing', drawWay.id); this._selectedData.set(drawWay.id, drawWay); } diff --git a/modules/modes/DrawLineMode.js b/modules/modes/DrawLineMode.js index 4efca275c..73c210dd3 100644 --- a/modules/modes/DrawLineMode.js +++ b/modules/modes/DrawLineMode.js @@ -165,9 +165,10 @@ export class DrawLineMode extends AbstractMode { const context = this.context; const editor = context.systems.editor; - const scene = context.systems.map.scene; + const map = context.systems.map; + const layer = map.scene.layers.get('osm'); + const eventManager = map.renderer.events; - const eventManager = context.systems.map.renderer.events; eventManager.setCursor('grab'); context.behaviors.hover @@ -214,7 +215,8 @@ export class DrawLineMode extends AbstractMode { this._lastPoint = null; this._selectedData.clear(); - scene.clearClass('drawing'); + + layer.clearClass('drawing'); window.setTimeout(() => { context.behaviors.mapInteraction.doubleClickEnabled = true; @@ -233,9 +235,10 @@ export class DrawLineMode extends AbstractMode { _refreshEntities() { const context = this.context; const editor = context.systems.editor; - const scene = context.scene(); + const map = context.systems.map; + const layer = map.scene.layers.get('osm'); - scene.clearClass('drawing'); + layer.clearClass('drawing'); this._selectedData.clear(); const graph = editor.staging.graph; @@ -246,21 +249,20 @@ export class DrawLineMode extends AbstractMode { // Sanity check - Bail out if any of these are missing. if (!drawWay || !lastNode || !firstNode) { - // debugger; this._cancel(); return; } // `drawNode` may or may not exist, it will be recreated after the user moves the pointer. if (drawNode) { - scene.classData('osm', drawNode.id, 'drawing'); + layer.setClass('drawing', drawNode.id); // Nudging at the edge of the map is allowed after the drawNode exists. context.behaviors.mapNudge.allow(); } // todo - we do want to allow connecting a line to itself in some situations - scene.classData('osm', drawWay.id, 'drawing'); + layer.setClass('drawing', drawWay.id); this._selectedData.set(drawWay.id, drawWay); } diff --git a/modules/pixi/AbstractFeature.js b/modules/pixi/AbstractFeature.js index 0cea7226d..8544c3cf5 100644 --- a/modules/pixi/AbstractFeature.js +++ b/modules/pixi/AbstractFeature.js @@ -23,21 +23,13 @@ import { PixiGeometry } from './PixiGeometry.js'; * `lod` Level of detail for the Feature last time it was styled (0 = off, 1 = simplified, 2 = full) * `halo` A PIXI.DisplayObject() that contains the graphics for the Feature's halo (if it has one) * `sceneBounds` PIXI.Rectangle() where 0,0 is the origin of the scene - * - * PseudoClasses: - * `active` - * `drawing` - * `highlighted` - * `hovered` - * `selected` - * `selectphoto` */ export class AbstractFeature { /** * @constructor - * @param layer The Layer that owns this Feature - * @param featureID Unique string to use for the name of this Feature + * @param {Layer} layer - The Layer that owns this Feature + * @param {string} featureID - Unique string to use for the name of this Feature */ constructor(layer, featureID) { this.type = 'unknown'; @@ -73,12 +65,7 @@ export class AbstractFeature { this._data = null; // pseudoclasses, @see `AbstractLayer.syncFeatureClasses()` - this._active = false; - this._drawing = false; - this._highlighted = false; - this._hovered = false; - this._selected = false; - this._selectphoto = false; + this._classes = new Set(); // We will manage our own bounds for now because we can probably do this // faster than Pixi's built in bounds calculations. @@ -132,8 +119,8 @@ export class AbstractFeature { * Every Feature should have an `update()` function that redraws the Feature at the given viewport and zoom. * When the Feature is updated, its `dirty` flags should be set to `false`. * Override in a subclass with needed logic. It will be passed: - * @param viewport Pixi viewport to use for rendering - * @param zoom Effective zoom to use for rendering + * @param {Viewport} viewport - Pixi viewport to use for rendering + * @param {number} zoom - Effective zoom to use for rendering * @abstract */ update(viewport, zoom) { @@ -222,113 +209,17 @@ export class AbstractFeature { this._allowInteraction = val; if (this.container) { - this.container.eventMode = (this._allowInteraction && !this._active) ? 'static' : 'none'; + this.container.eventMode = this._allowInteraction ? 'static' : 'none'; } } - /** - * active - * @see `AbstractLayer.syncFeatureClasses()` - * @param `true` to apply the 'active' pseudoclass - */ - get active() { - return this._active; - } - set active(val) { - if (val === this._active) return; // no change - this._active = val; - - if (this.container) { - this.container.eventMode = (this._allowInteraction && !this._active) ? 'static' : 'none'; - } - } - - - /** - * drawing - * @see `AbstractLayer.syncFeatureClasses()` - * @param val `true` to apply the 'drawing' pseudoclass - */ - get drawing() { - return this._drawing; - } - set drawing(val) { - if (val === this._drawing) return; // no change - this._drawing = val; - this._styleDirty = true; - } - - - /** - * highlighted - * @see `AbstractLayer.syncFeatureClasses()` - * @param val `true` to apply the 'highlighted' pseudoclass - */ - get highlighted() { - return this._highlighted; - } - set highlighted(val) { - if (val === this._highlighted) return; // no change - this._highlighted = val; - this._styleDirty = true; - this._labelDirty = true; - } - - - /** - * hovered - * @see `AbstractLayer.syncFeatureClasses()` - * @param val `true` to apply the 'hovered' pseudoclass - */ - get hovered() { - return this._hovered; - } - set hovered(val) { - if (val === this._hovered) return; // no change - this._hovered = val; - this._styleDirty = true; - } - - - /** - * selected - * @see `AbstractLayer.syncFeatureClasses()` - * @param val `true` to apply the 'selected' pseudoclass - */ - get selected() { - return this._selected; - } - set selected(val) { - if (val === this._selected) return; // no change - this._selected = val; - this._styleDirty = true; - this._labelDirty = true; - } - - - /** - * selectphoto - * @see `AbstractLayer.syncFeatureClasses()` - * @param val `true` to apply the 'selectphoto' pseudoclass - */ - get selectphoto() { - return this._selectphoto; - } - set selectphoto(val) { - if (val === this._selectphoto) return; // no change - this._selectphoto = val; - this._styleDirty = true; - this._labelDirty = true; - } - - /** * style - * @param obj Style `Object` (contents depends on the Feature type) + * @param {Object} obj - Style `Object` (contents depends on the Feature type) * - * 'point' - see AbstractFeaturePoint.js - * 'line'/'polygon' - see styles.js + * 'point' - @see AbstractFeaturePoint.js + * 'line'/'polygon' - @see styles.js */ get style() { return this._style; @@ -341,7 +232,7 @@ export class AbstractFeature { /** * label - * @param str String containing the label to use + * @param {string} str - the label to use */ get label() { return this._label; @@ -374,11 +265,56 @@ export class AbstractFeature { } + /** + * setClass + * Sets a pseudoclass. + * Pseudoclasses are special values that can affecct the styling of a feature. + * (They do the same thing that CSS classes do). + * When changing the value of the class we'll also dirty the feature so that it gets redrawn on the next pass. + * @param {string} classID - the pseudoclass to set + */ + setClass(classID) { + const hasClass = this._classes.has(classID); + if (hasClass) return; // nothing to do + + this._classes.add(classID); + this._styleDirty = true; + this._labelDirty = true; + } + + + /** + * unsetClass + * Unsets a pseudoclass. + * Pseudoclasses are special values that can affecct the styling of a feature. + * (They do the same thing that CSS classes do). + * When changing the value of the class we'll also dirty the feature so that it gets redrawn on the next pass. + * @param {string} classID - the pseudoclass to remove + */ + unsetClass(classID) { + const hasClass = this._classes.has(classID); + if (!hasClass) return; // nothing to do + + this._classes.delete(classID); + this._styleDirty = true; + this._labelDirty = true; + } + + + /** + * hasClass + * @param {string} classID - the class to check + * @return {boolean} `true` if the feature has this class, `false` if not + */ + hasClass(classID) { + return this._classes.has(classID); + } + /** * setData * This binds the data element to the feature, also lets the layer know about it. - * @param dataID `String` identifer for this data element (e.g. 'n123') - * @param data `Object` data to bind to the feature (e.g. an OSM Node) + * @param {string} dataID - Identifer for this data element (e.g. 'n123') + * @param {*} data - data to bind to the feature (e.g. an OSM Node) */ setData(dataID, data) { this._dataID = dataID; @@ -390,8 +326,8 @@ export class AbstractFeature { /** * addChildData * Adds a mapping from parent data to child data. - * @param parentID `String` dataID of the parent (e.g. 'r123') - * @param childID `String` dataID of the child (e.g. 'w123') + * @param {string} parentID - dataID of the parent (e.g. 'r123') + * @param {string} childID - dataID of the child (e.g. 'w123') */ addChildData(parentID, childID) { this.layer.addChildData(parentID, childID); @@ -401,7 +337,7 @@ export class AbstractFeature { /** * clearChildData * Removes all child dataIDs for the given parent dataID - * @param parentID `String` dataID of the parent (e.g. 'r123') + * @param {string} parentID - dataID of the parent (e.g. 'r123') */ clearChildData(parentID) { this.layer.clearChildData(parentID); diff --git a/modules/pixi/AbstractLayer.js b/modules/pixi/AbstractLayer.js index 3d0cf61a4..38cd129c1 100644 --- a/modules/pixi/AbstractLayer.js +++ b/modules/pixi/AbstractLayer.js @@ -11,25 +11,26 @@ function asSet(vals) { * It creates a container to hold the Layer data. * * Notes on identifiers: - * - `layerID` - A unique identifier for the layer, for example 'osm' - * - `featureID` - A unique identifier for the feature, for example 'osm-w-123-fill' - * - `dataID` - A feature may have data bound to it, for example OSM identifier like 'w-123' - * - `classID` - A class identifier like 'hovered' or 'selected' + * All identifiers should be strings, to avoid JavaScript comparison surprises (e.g. `'0' !== 0`) + * `layerID` A unique identifier for the layer, for example 'osm' + * `featureID` A unique identifier for the feature, for example 'osm-w-123-fill' + * `dataID` A feature may have data bound to it, for example OSM identifier like 'w-123' + * `classID` A pseudoclass identifier like 'hover' or 'select' * * Properties you can access: - * `id` Unique string to use for the name of this Layer - * `supported` Is this Layer supported? (i.e. do we even show it in lists?) - * `zIndex` Where this Layer sits compared to other Layers - * `enabled` Whether the the user has chosen to see the Layer - * `features` `Map(featureID -> Feature)` of all features on this Layer - * `retained` `Map(featureID -> Integer frame)` last seen + * `id` Unique string to use for the name of this Layer + * `supported` Is this Layer supported? (i.e. do we even show it in lists?) + * `zIndex` Where this Layer sits compared to other Layers + * `enabled` Whether the the user has chosen to see the Layer + * `features` `Map` of all features on this Layer + * `retained` `Map` last seen */ export class AbstractLayer { /** * @constructor - * @param scene The Scene that owns this Layer - * @param layerID Unique string to use for the name of this Layer + * @param {PixiScene} scene - The Scene that owns this Layer + * @param {string} layerID - Unique string to use for the name of this Layer */ constructor(scene, layerID) { this.scene = scene; @@ -40,28 +41,26 @@ export class AbstractLayer { this._enabled = false; // Whether the user has chosen to see the layer // Collection of Features on this Layer - this.features = new Map(); // Map (featureID -> Feature) - this.retained = new Map(); // Map (featureID -> frame last seen) + this.features = new Map(); // Map + this.retained = new Map(); // Map // Feature <-> Data // These lookups capture which features are bound to which data. - this._featureHasData = new Map(); // Map (featureID -> dataID) - this._dataHasFeature = new Map(); // Map (dataID -> Set(featureID)) + this._featureHasData = new Map(); // Map + this._dataHasFeature = new Map(); // Map> // Parent Data <-> Child Data // We establish a parent-child data hierarchy (like what the DOM used to do for us) // For example, we need this to know which ways make up a multipolygon relation. - this._parentHasChildren = new Map(); // Map (parent dataID -> Set(child dataID)) - this._childHasParents = new Map(); // Map (child dataID -> Set(parent dataID)) + this._parentHasChildren = new Map(); // Map> + this._childHasParents = new Map(); // Map> // Data <-> Class // Data classes are strings (like what CSS classes used to do for us) - // Currently supported "selected", "hovered", "drawing" - // Can set other ones but it won't do anything (but won't break anything either) // Counterintuitively, the Layer needs to be the source of truth for these data classes, - // because a Feature can be "selected" or "drawing" even before it has been created, or after destroyed - this._dataHasClass = new Map(); // Map (dataID -> Set(classID)) - this._classHasData = new Map(); // Map (classID -> Set(dataID)) + // because a Feature can be 'selected' or 'drawing' even before it has been created, or after destroyed + this._dataHasClass = new Map(); // Map> + this._classHasData = new Map(); // Map> } @@ -92,9 +91,9 @@ export class AbstractLayer { * render * Every Layer should have a render function that manages the Features in view. * Override in a subclass with needed logic. It will be passed: - * @param frame Integer frame being rendered - * @param viewport Pixi viewport to use for rendering - * @param zoom Effective zoom to use for rendering + * @param {number} frame - Integer frame being rendered + * @param {Viewport} viewport - Pixi viewport to use for rendering + * @param {number} zoom - Effective zoom to use for rendering * @abstract */ render() { @@ -104,7 +103,7 @@ export class AbstractLayer { /** * cull * Make invisible any Features that were not seen during the current frame - * @param frame Integer frame being rendered + * @param {number} frame - Integer frame being rendered */ cull(frame) { for (const [featureID, feature] of this.features) { @@ -125,7 +124,7 @@ export class AbstractLayer { /** * addFeature * Add a feature to the layer cache. - * @param feature A Feature derived from `AbstractFeature` (point, line, multipolygon) + * @param {Feature} feature - A Feature derived from `AbstractFeature` (point, line, multipolygon) */ addFeature(feature) { this.features.set(feature.id, feature); @@ -135,7 +134,7 @@ export class AbstractLayer { /** * removeFeature * Remove a Feature from the layer cache. - * @param feature A Feature derived from `AbstractFeature` (point, line, multipolygon) + * @param {Feature} feature - A Feature derived from `AbstractFeature` (point, line, multipolygon) */ removeFeature(feature) { this.unbindData(feature.id); @@ -148,8 +147,8 @@ export class AbstractLayer { * retainFeature * Retain the feature for the given frame. * Features that are not retained may be automatically culled (made invisible) or removed. - * @param feature A Feature derived from `AbstractFeature` (point, line, multipolygon) - * @param frame Integer frame being rendered + * @param {Feature} feature - A Feature derived from `AbstractFeature` (point, line, multipolygon) + * @param {number} frame - Integer frame being rendered */ retainFeature(feature, frame) { if (feature.lod > 0) { @@ -162,48 +161,11 @@ export class AbstractLayer { } - /** - * syncFeatureClasses - * Set the feature's various state properties (e.g. selected, hovered, etc.) - * Counterintuitively, the Layer needs to be the source of truth for these properties, - * because a Feature can be "selected" or "drawing" even before it has been created. - * - * Setting these state properties may dirty the feature if the it causes the state to change. - * Therefore this should be called after the Feature has been created but before any updates happen. - * - * @param feature A Feature derived from `AbstractFeature` (point, line, multipolygon) - */ - syncFeatureClasses(feature) { - const featureID = feature.id; - const dataID = this._featureHasData.get(featureID); - if (!dataID) return; - - const classList = this._dataHasClass.get(dataID) ?? new Set(); - - // - // Trying to document all the supported pseudo-classes here: - // - // 'active': prevents events from firing, e.g. when dragging - // 'drawing': removes the hitarea, and avoids hover, e.g. it prevents snapping - // 'highlighted': adds a blue glowfilter - // 'hovered': adds a yellow glowfilter - // 'selected': adds a dashed line halo - // 'selectphoto': styling for the currently selected photo - - feature.active = classList.has('active'); - feature.drawing = classList.has('drawing'); - feature.highlighted = classList.has('highlighted'); - feature.hovered = classList.has('hovered'); - feature.selected = classList.has('selected'); - feature.selectphoto = classList.has('selectphoto'); - } - - /** * bindData * Adds (or replaces) a data binding from featureID to a dataID - * @param featureID `String` featureID (e.g. 'osm-w-123-fill') - * @param dataID `String` dataID (e.g. 'w-123') + * @param {string} featureID - featureID (e.g. 'osm-w-123-fill') + * @param {string} dataID - dataID (e.g. 'w-123') */ bindData(featureID, dataID) { this.unbindData(featureID); @@ -222,7 +184,7 @@ export class AbstractLayer { /** * unbindData * Removes the data binding for a given featureID - * @param featureID `String` featureID (e.g. 'osm-w-123-fill') + * @param {string} featureID - featureID (e.g. 'osm-w-123-fill') */ unbindData(featureID) { const dataID = this._featureHasData.get(featureID); @@ -249,8 +211,8 @@ export class AbstractLayer { /** * addChildData * Adds a mapping from parent data to child data. - * @param parentID `String` dataID of the parent (e.g. 'r123') - * @param childID `String` dataID of the child (e.g. 'w123') + * @param {string} parentID - dataID of the parent (e.g. 'r123') + * @param {string} childID - dataID of the child (e.g. 'w123') */ addChildData(parentID, childID) { let childIDs = this._parentHasChildren.get(parentID); @@ -272,8 +234,8 @@ export class AbstractLayer { /** * removeChildData * Removes mapping from parent data to child data. - * @param parentID `String` dataID (e.g. 'r123') - * @param childID `String` dataID to remove as a child (e.g. 'w1') + * @param {string} parentID - dataID (e.g. 'r123') + * @param {string} childID - dataID to remove as a child (e.g. 'w1') */ removeChildData(parentID, childID) { let childIDs = this._parentHasChildren.get(parentID); @@ -300,7 +262,7 @@ export class AbstractLayer { /** * clearChildData * Removes all child dataIDs for the given parent dataID - * @param parentID `String` dataID (e.g. 'r123') + * @param {string} parentID - dataID (e.g. 'r123') */ clearChildData(parentID) { const childIDs = this._parentHasChildren.get(parentID) ?? new Set(); @@ -313,9 +275,9 @@ export class AbstractLayer { /** * getSelfAndDescendants * Recursively get a result `Set` including the given dataID and all dataIDs in the child hierarchy. - * @param dataID `String` dataID (e.g. 'r123') - * @param result? `Set` containing the results (e.g. ['r123','w123','n123']) - * @return `Set` including the dataID and all dataIDs in the child hierarchy + * @param {string} dataID - dataID (e.g. 'r123') + * @param {Set} result? - `Set` containing the results (e.g. ['r123','w123','n123']) + * @return {Set} `Set` including the dataID and all dataIDs in the child hierarchy */ getSelfAndDescendants(dataID, result) { if (result instanceof Set) { @@ -338,9 +300,9 @@ export class AbstractLayer { /** * getSelfAndAncestors * Recursively get a result `Set` including the given dataID and all dataIDs in the parent hierarchy - * @param dataID `String` dataID (e.g. 'n123') - * @param result? `Set` containing the results (e.g. ['n123','w123','r123']) - * @return `Set` including the dataID and all dataIDs in the parent hierarchy + * @param {string} dataID - dataID (e.g. 'n123') + * @param {Set} result? - `Set` containing the results (e.g. ['n123','w123','r123']) + * @return {Set} `Set` including the dataID and all dataIDs in the parent hierarchy */ getSelfAndAncestors(dataID, result) { if (result instanceof Set) { @@ -363,9 +325,9 @@ export class AbstractLayer { /** * getSelfAndSiblings * Get a result `Set` including the dataID and all sibling dataIDs in the parent-child hierarchy - * @param dataID `String` dataID (e.g. 'n123') - * @param result? `Set` containing the results (e.g. ['n121','n122','n123','n124']) - * @return `Set` including the dataID and all dataIDs adjacent in the parent-child hierarchy + * @param {string} dataID - `String` dataID (e.g. 'n123') + * @param {Set} result? - `Set` containing the results (e.g. ['n121','n122','n123','n124']) + * @return {Set} `Set` including the dataID and all dataIDs adjacent in the parent-child hierarchy */ getSelfAndSiblings(dataID, result) { if (result instanceof Set) { @@ -387,12 +349,12 @@ export class AbstractLayer { /** - * classData - * Sets a dataID as being classed a certain way (e.g. 'hovered') - * @param dataID `String` dataID (e.g. 'r123') - * @param classID `String` classID (e.g. 'hovered') + * setClass + * Sets a dataID as being classed a certain way (e.g. 'hover') + * @param {string} classID - classID to set (e.g. 'hover') + * @param {string} dataID - dataID (e.g. 'r123') */ - classData(dataID, classID) { + setClass(classID, dataID) { let classIDs = this._dataHasClass.get(dataID); if (!classIDs) { classIDs = new Set(); @@ -410,12 +372,12 @@ export class AbstractLayer { /** - * unclassData - * Unsets a dataID from being classed a certain way (e.g. 'hovered') - * @param dataID `String` dataID (e.g. 'r123') - * @param classID `String` classID (e.g. 'hovered') + * unsetClass + * Unsets a dataID from being classed a certain way (e.g. 'hover') + * @param {string} classID - classID to unset (e.g. 'hover') + * @param {string} dataID - dataID (e.g. 'r123') */ - unclassData(dataID, classID) { + unsetClass(classID, dataID) { let classIDs = this._dataHasClass.get(dataID); if (classIDs) { classIDs.delete(classID); @@ -437,13 +399,64 @@ export class AbstractLayer { /** * clearClass * Clear out all uses of the given classID. - * @param classID `String` classID (e.g. 'hovered') + * @param {string} classID - classID to clear (e.g. 'hover') */ clearClass(classID) { const dataIDs = this._classHasData.get(classID) ?? new Set(); for (const dataID of dataIDs) { - this.unclassData(dataID, classID); + this.unsetClass(classID, dataID); + } + } + + + /** + * getDataWithClass + * Returns the dataIDs that are currently classed with the given classID + * @param {string} classID - classID to check (e.g. 'hover') + * @return {Set} dataIDs the dataIDs that currently have this classID set + */ + getDataWithClass(classID) { + const dataIDs = this._classHasData.get(classID) ?? new Set(); + return new Set(dataIDs); // copy + } + + + /** + * syncFeatureClasses + * This updates the feature's classes (e.g. 'select', 'hover', etc.) to match the Layer classes. + * + * Counterintuitively, the Layer needs to be the source of truth for these classes, + * because a Feature can be 'selected' or 'drawing' even before it has been created, or after destroyed. + * + * Syncing these classes will dirty the feature if the it causes a change. + * Therefore this should be called after the Feature has been created, but before any updates happen. + * + * @param {Feature} feature - A Feature derived from `AbstractFeature` (point, line, multipolygon) + */ + syncFeatureClasses(feature) { + const featureID = feature.id; + const dataID = this._featureHasData.get(featureID); + if (!dataID) return; + + const layerClasses = this._dataHasClass.get(dataID) ?? new Set(); + const featureClasses = feature._classes; + + for (const classID of featureClasses) { + if (layerClasses.has(classID)) continue; // no change + feature.unsetClass(classID); // remove extra class from feature } + for (const classID of layerClasses) { + if (featureClasses.has(classID)) continue; // no change + feature.setClass(classID); // add missing class to feature + } + + // Trying to document all the supported pseudoclasses here: + // + // 'drawing': removes the hitarea, and avoids hover, e.g. it prevents snapping + // 'highlight': adds a blue glowfilter + // 'hover': adds a yellow glowfilter + // 'select': adds a dashed line halo + // 'selectphoto': styling for the currently selected photo } @@ -462,7 +475,7 @@ export class AbstractLayer { * dirtyFeatures * Mark specific features features as `dirty` * During the next "app" pass, dirty features will be rebuilt. - * @param featureIDs A `Set` or `Array` of featureIDs, or single `String` featureID + * @param {Set|Array|string} featureIDs - A `Set` or `Array` of featureIDs, or single `String` featureID */ dirtyFeatures(featureIDs) { for (const featureID of asSet(featureIDs)) { // coax ids into a Set @@ -477,7 +490,7 @@ export class AbstractLayer { * dirtyData * Mark any features bound to a given dataID as `dirty` * During the next "app" pass, dirty features will be rebuilt. - * @param dataIDs A `Set` or `Array` of dataIDs, or single `String` dataID + * @param {Set|Array|string} dataIDs - A `Set` or `Array` of dataIDs, or single `String` dataID */ dirtyData(dataIDs) { for (const dataID of asSet(dataIDs)) { // coax ids into a Set diff --git a/modules/pixi/PixiFeatureLine.js b/modules/pixi/PixiFeatureLine.js index 629ec3880..5b9007139 100644 --- a/modules/pixi/PixiFeatureLine.js +++ b/modules/pixi/PixiFeatureLine.js @@ -26,8 +26,8 @@ export class PixiFeatureLine extends AbstractFeature { /** * @constructor - * @param layer The Layer that owns this Feature - * @param featureID Unique string to use for the name of this Feature + * @param {Layer} layer - The Layer that owns this Feature + * @param {string} featureID - Unique string to use for the name of this Feature */ constructor(layer, featureID) { super(layer, featureID); @@ -72,8 +72,8 @@ export class PixiFeatureLine extends AbstractFeature { /** * update - * @param viewport Pixi viewport to use for rendering - * @param zoom Effective zoom to use for rendering + * @param {Viewport} viewport - Pixi viewport to use for rendering + * @param {number} zoom - Effective zoom to use for rendering */ update(viewport, zoom) { if (!this.dirty) return; // nothing to do @@ -272,7 +272,7 @@ export class PixiFeatureLine extends AbstractFeature { // // Fix for Rapid#648: If we're drawing, we don't need to hit ourselves. // bhousel 3/23 sometimes we do! -// if (this.drawing) { +// if (this._classes.has('drawing')) { // this.container.hitArea = null; // return; // } @@ -297,9 +297,9 @@ export class PixiFeatureLine extends AbstractFeature { * Show/Hide halo (expects `this._bufferdata` to be already set up by `updateHitArea` as a PIXI.Polygon) */ updateHalo() { - const showHover = (this.visible && this.hovered); - const showSelect = (this.visible && this.selected); - const showHighlight = (this.visible && this.highlighted); + const showHover = (this.visible && this._classes.has('hover')); + const showSelect = (this.visible && this._classes.has('select')); + const showHighlight = (this.visible && this._classes.has('highlight')); // Hover if (showHover) { if (!this.container.filters) { @@ -356,10 +356,10 @@ export class PixiFeatureLine extends AbstractFeature { /** * style - * @param obj Style `Object` (contents depends on the Feature type) + * @param {Object} obj - Style `Object` (contents depends on the Feature type) * - * 'point' - see `PixiFeaturePoint.js` - * 'line'/'polygon' - see `StyleSystem.js` + * 'point' - @see `PixiFeaturePoint.js` + * 'line'/'polygon' - @see `StyleSystem.js` */ get style() { return this._style; diff --git a/modules/pixi/PixiFeaturePoint.js b/modules/pixi/PixiFeaturePoint.js index 1ca04350a..a2763c33d 100644 --- a/modules/pixi/PixiFeaturePoint.js +++ b/modules/pixi/PixiFeaturePoint.js @@ -22,8 +22,8 @@ export class PixiFeaturePoint extends AbstractFeature { /** * @constructor - * @param layer The Layer that owns this Feature - * @param featureID Unique string to use for the name of this Feature + * @param {Layer} layer - The Layer that owns this Feature + * @param {string} featureID - Unique string to use for the name of this Feature */ constructor(layer, featureID) { super(layer, featureID); @@ -69,8 +69,8 @@ export class PixiFeaturePoint extends AbstractFeature { /** * update - * @param viewport Pixi viewport to use for rendering - * @param zoom Effective zoom to use for rendering + * @param {Viewport} viewport - Pixi viewport to use for rendering + * @param {number} zoom - Effective zoom to use for rendering */ update(viewport, zoom) { if (!this.dirty) return; // nothing to do @@ -93,8 +93,8 @@ export class PixiFeaturePoint extends AbstractFeature { /** * updateGeometry - * @param viewport Pixi viewport to use for rendering - * @param zoom Effective zoom to use for rendering + * @param {Viewport} viewport - Pixi viewport to use for rendering + * @param {number} zoom - Effective zoom to use for rendering */ updateGeometry(viewport, zoom) { if (!this.geometry.dirty) return; @@ -114,7 +114,8 @@ export class PixiFeaturePoint extends AbstractFeature { /** * updateStyle - * @param zoom Effective zoom to use for rendering + * @param {Viewport} viewport - Pixi viewport to use for rendering + * @param {number} zoom - Effective zoom to use for rendering */ updateStyle(viewport, zoom) { if (!this._styleDirty) return; @@ -186,7 +187,7 @@ export class PixiFeaturePoint extends AbstractFeature { vfSprite.anchor.set(0.5, 0.5); // middle, middle // Make the active photo image pop out at the user - if (this.selectphoto) { + if (this._classes.has('selectphoto')) { this.container.zIndex = 99000; } @@ -274,7 +275,7 @@ export class PixiFeaturePoint extends AbstractFeature { if (!this.visible) return; // Fix for Rapid#648: If we're drawing, we don't need to hit ourselves. - if (this.drawing) { + if (this._classes.has('drawing')) { this.container.hitArea = null; return; } @@ -312,9 +313,9 @@ export class PixiFeaturePoint extends AbstractFeature { * Show/Hide halo (requires `this.container.hitArea` to be already set up by `updateHitArea` as a supported shape) */ updateHalo() { - const showHover = (this.visible && this.hovered); - const showSelect = (this.visible && this.selected && !this.virtual); - const showHighlight = (this.visible && this.highlighted); + const showHover = (this.visible && this._classes.has('hover')); + const showSelect = (this.visible && this._classes.has('select') && !this.virtual); + const showHighlight = (this.visible && this._classes.has('highlight')); // Hover if (showHover) { @@ -374,10 +375,10 @@ export class PixiFeaturePoint extends AbstractFeature { /** * style - * @param obj Style `Object` (contents depends on the Feature type) + * @param {Object} obj - Style `Object` (contents depends on the Feature type) * - * 'point' - see `PixiFeaturePoint.js` - * 'line'/'polygon' - see `StyleSystem.js` + * 'point' - @see `PixiFeaturePoint.js` + * 'line'/'polygon' - @see `StyleSystem.js` */ get style() { return this._style; diff --git a/modules/pixi/PixiFeaturePolygon.js b/modules/pixi/PixiFeaturePolygon.js index f7b5e8a88..215f575a0 100644 --- a/modules/pixi/PixiFeaturePolygon.js +++ b/modules/pixi/PixiFeaturePolygon.js @@ -27,8 +27,8 @@ export class PixiFeaturePolygon extends AbstractFeature { /** * @constructor - * @param layer The Layer that owns this Feature - * @param featureID Unique string to use for the name of this Feature + * @param {Layer} layer - The Layer that owns this Feature + * @param {string} featureID - Unique string to use for the name of this Feature */ constructor(layer, featureID) { super(layer, featureID); @@ -109,8 +109,8 @@ export class PixiFeaturePolygon extends AbstractFeature { /** * update - * @param viewport Pixi viewport to use for rendering - * @param zoom Effective zoom to use for rendering + * @param {Viewport} viewport - Pixi viewport to use for rendering + * @param {number} zoom - Effective zoom to use for rendering */ update(viewport, zoom) { if (!this.dirty) return; // nothing to do @@ -414,9 +414,9 @@ export class PixiFeaturePolygon extends AbstractFeature { */ updateHalo() { const wireframeMode = this.context.systems.map.wireframeMode; - const showHover = (this.visible && this.hovered); - const showSelect = (this.visible && this.selected); - const showHighlight = (this.visible && this.highlighted); + const showHover = (this.visible && this._classes.has('hover')); + const showSelect = (this.visible && this._classes.has('select')); + const showHighlight = (this.visible && this._classes.has('highlight')); // Hover if (showHover) { @@ -473,10 +473,10 @@ export class PixiFeaturePolygon extends AbstractFeature { /** * style - * @param obj Style `Object` (contents depends on the Feature type) + * @param {Object} obj - Style `Object` (contents depends on the Feature type) * - * 'point' - see `PixiFeaturePoint.js` - * 'line'/'polygon' - see `StyleSystem.js` + * 'point' - @see `PixiFeaturePoint.js` + * 'line'/'polygon' - @see `StyleSystem.js` */ get style() { return this._style; diff --git a/modules/pixi/PixiGeometry.js b/modules/pixi/PixiGeometry.js index bc24eac12..93a35e09f 100644 --- a/modules/pixi/PixiGeometry.js +++ b/modules/pixi/PixiGeometry.js @@ -90,8 +90,7 @@ export class PixiGeometry { /** * update - * @param viewport Pixi viewport to use for rendering - * @param zoom Effective zoom to use for rendering + * @param {Viewport} viewport - Pixi viewport to use for rendering */ update(viewport) { if (!this.dirty || !this.origCoords || !this.origExtent) return; // nothing to do @@ -253,7 +252,7 @@ export class PixiGeometry { /** * setCoords - * @param data Geometry `Array` (contents depends on the Feature type) + * @param {Array<*>} data - Geometry `Array` (contents depends on the Feature type) * * 'point' - Single wgs84 coordinate * [lon, lat] @@ -296,8 +295,8 @@ export class PixiGeometry { /** * _inferType * Determines what kind of geometry we were passed. - * @param arr Geometry `Array` (contents depends on the Feature type) - * @return 'point', 'line', 'polygon' or null + * @param {Array<*>} arr - Geometry `Array` (contents depends on the Feature type) + * @return {string?} 'point', 'line', 'polygon' or null */ _inferType(data) { const a = Array.isArray(data) && data[0]; @@ -312,5 +311,4 @@ export class PixiGeometry { return null; } - } diff --git a/modules/pixi/PixiLayerKartaPhotos.js b/modules/pixi/PixiLayerKartaPhotos.js index 74714db3d..254aee861 100644 --- a/modules/pixi/PixiLayerKartaPhotos.js +++ b/modules/pixi/PixiLayerKartaPhotos.js @@ -191,7 +191,7 @@ export class PixiLayerKartaPhotos extends AbstractLayer { if (feature.dirty) { const style = Object.assign({}, MARKERSTYLE); // todo handle pano - if (feature.selectphoto) { // selected photo style + if (feature.hasClass('selectphoto')) { // selected photo style // style.viewfieldAngles = [this._viewerCompassAngle ?? d.ca]; if (Number.isFinite(d.ca)) { style.viewfieldAngles = [d.ca]; // ca = camera angle diff --git a/modules/pixi/PixiLayerMapillaryPhotos.js b/modules/pixi/PixiLayerMapillaryPhotos.js index 01dca0372..2bdeed40b 100644 --- a/modules/pixi/PixiLayerMapillaryPhotos.js +++ b/modules/pixi/PixiLayerMapillaryPhotos.js @@ -278,7 +278,7 @@ export class PixiLayerMapillaryPhotos extends AbstractLayer { if (feature.dirty) { const style = Object.assign({}, MARKERSTYLE); - if (feature.selectphoto) { // selected photo style + if (feature.hasClass('selectphoto')) { // selected photo style style.viewfieldAngles = [this._viewerBearing ?? d.ca]; style.viewfieldName = 'viewfield'; style.viewfieldTint = MAPILLARY_SELECTED; diff --git a/modules/pixi/PixiLayerOsm.js b/modules/pixi/PixiLayerOsm.js index bfd1b5a5c..338f87761 100644 --- a/modules/pixi/PixiLayerOsm.js +++ b/modules/pixi/PixiLayerOsm.js @@ -188,9 +188,9 @@ export class PixiLayerOsm extends AbstractLayer { // and parent-child data links have been established. // Gather ids related for the selected/hovered/drawing features. - const selectedIDs = this._classHasData.get('selected') ?? new Set(); - const hoveredIDs = this._classHasData.get('hovered') ?? new Set(); - const drawingIDs = this._classHasData.get('drawing') ?? new Set(); + const selectedIDs = this.getDataWithClass('select'); + const hoveredIDs = this.getDataWithClass('hover'); + const drawingIDs = this.getDataWithClass('drawing'); const dataIDs = new Set([...selectedIDs, ...hoveredIDs, ...drawingIDs]); // Experiment: avoid showing child vertices/midpoints for too small parents diff --git a/modules/pixi/PixiLayerStreetsidePhotos.js b/modules/pixi/PixiLayerStreetsidePhotos.js index ef9683ba3..84bfddf7f 100644 --- a/modules/pixi/PixiLayerStreetsidePhotos.js +++ b/modules/pixi/PixiLayerStreetsidePhotos.js @@ -233,7 +233,7 @@ export class PixiLayerStreetsidePhotos extends AbstractLayer { const yaw = viewer?.getYaw() ?? 0; const fov = viewer?.getHfov() ?? 45; - if (feature.selectphoto) { // selected photo style + if (feature.hasClass('selectphoto')) { // selected photo style style.viewfieldAngles = [d.ca + yaw]; style.viewfieldName = 'viewfield'; style.viewfieldTint = STREETSIDE_SELECTED; diff --git a/modules/pixi/PixiRenderer.js b/modules/pixi/PixiRenderer.js index 286efdf5e..45ae526b5 100644 --- a/modules/pixi/PixiRenderer.js +++ b/modules/pixi/PixiRenderer.js @@ -177,7 +177,7 @@ export class PixiRenderer extends EventEmitter { * Respond to any change in selection (called on mode change) */ _onModeChange(mode) { - this.scene.clearClass('selected'); + this.scene.clearClass('select'); for (const [datumID, datum] of this.context.selectedData()) { let layerID = null; @@ -191,14 +191,14 @@ export class PixiRenderer extends EventEmitter { layerID = 'rapid'; } else if (datum.__featurehash__) { // custom data layerID = 'custom-data'; - } else if (mode.id === 'select-osm') { // an OSM feature + } else if (mode.id === 'select-osm' || mode.id === 'drag-node') { // an OSM feature layerID = 'osm'; } else { // other selectable things (photos?) - we will not select-style them for now :( } if (layerID) { - this.scene.classData(layerID, datumID, 'selected'); + this.scene.setClass('select', layerID, datumID); } } @@ -225,12 +225,12 @@ export class PixiRenderer extends EventEmitter { ui.sidebar.hover(hoverData ? [hoverData] : []); } - scene.clearClass('hovered'); + scene.clearClass('hover'); if (layer && dataID) { // Only set hover class if this target isn't currently drawing - const drawingIDs = layer._classHasData.get('drawing') ?? new Set(); + const drawingIDs = layer.getDataWithClass('drawing'); if (!drawingIDs.has(dataID)) { - scene.classData(layer.id, dataID, 'hovered'); + layer.setClass('hover', dataID); } } diff --git a/modules/pixi/PixiScene.js b/modules/pixi/PixiScene.js index 8fee0d252..0606b377a 100644 --- a/modules/pixi/PixiScene.js +++ b/modules/pixi/PixiScene.js @@ -41,7 +41,7 @@ function asSet(vals) { * - `layerID` - A unique identifier for the layer, for example 'osm' * - `featureID` - A unique identifier for the feature, for example 'osm-w-123-fill' * - `dataID` - A feature may have data bound to it, for example OSM identifier like 'w-123' - * - `classID` - A class identifier like 'hovered' or 'selected' + * - `classID` - A pseudoclass identifier like 'hover' or 'select' * * Properties you can access: * `groups` `Map (groupID -> PIXI.Container)` of all groups @@ -236,33 +236,33 @@ export class PixiScene extends EventEmitter { /** - * classData - * Sets a dataID as being classed a certain way (e.g. 'hovered') + * setClass + * Sets a dataID as being classed a certain way (e.g. 'hover') + * @param classID `String` classID (e.g. 'hover') * @param layerID `String` layerID (e.g. 'osm') * @param dataID `String` dataID (e.g. 'r123') - * @param classID `String` classID (e.g. 'hovered') */ - classData(layerID, dataID, classID) { - this.layers.get(layerID)?.classData(dataID, classID); + setClass(classID, layerID, dataID) { + this.layers.get(layerID)?.setClass(classID, dataID); } /** - * unclassData - * Unsets a dataID from being classed a certain way (e.g. 'hovered') + * unsetClass + * Unsets a dataID from being classed a certain way (e.g. 'hover') + * @param classID `String` classID (e.g. 'hover') * @param layerID `String` layerID (e.g. 'osm') * @param dataID `String` dataID (e.g. 'r123') - * @param classID `String` classID (e.g. 'hovered') */ - unclassData(layerID, dataID, classID) { - this.layers.get(layerID)?.unclassData(dataID, classID); + unsetClass(classID, layerID, dataID) { + this.layers.get(layerID)?.unsetClass(classID, dataID); } /** * clearClass * Clear out all uses of the given classID across all layers. - * @param classID `String` classID (e.g. 'hovered') + * @param classID `String` classID (e.g. 'hover') */ clearClass(classID) { for (const layer of this.layers.values()) { diff --git a/modules/util/util.js b/modules/util/util.js index 409dffc16..8947ad061 100644 --- a/modules/util/util.js +++ b/modules/util/util.js @@ -79,25 +79,28 @@ export function geojsonExtent(geojson) { export function utilHighlightEntities(entityIDs, highlighted, context) { const editor = context.systems.editor; const map = context.systems.map; + const scene = map.scene; if (!scene) return; // called too soon? + const layer = scene.layers.get('osm'); + if (highlighted) { for (const entityID of entityIDs) { - scene.classData('osm', entityID, 'highlighted'); + layer.setClass('highlight', entityID); // When highlighting a relation, try to highlight its members. if (entityID[0] === 'r') { const relation = editor.staging.graph.hasEntity(entityID); if (!relation) continue; for (const member of relation.members) { - scene.classData('osm', member.id, 'highlighted'); + layer.setClass('highlight', member.id); } } } } else { - scene.clearClass('highlighted'); + layer.clearClass('highlight'); } map.immediateRedraw();