From c029d3bb97f31d66d0abbd39836feb0fe15b983c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 2 Dec 2024 17:03:00 -0500 Subject: [PATCH] Add URL parameter for OSM notes (closes #1630) This adds support for a urlhash parameter `¬e=val` where `val` can be 'true' to enable the layer or a noteID to select Example: `note=true` -or- `note=` (in other words, it works like the maproulette parameter) --- modules/core/MapSystem.js | 119 ++++++++++++++++++++++++++++++++-- modules/core/UrlHashSystem.js | 1 + modules/ui/UiFeatureList.js | 28 ++------ 3 files changed, 118 insertions(+), 30 deletions(-) diff --git a/modules/core/MapSystem.js b/modules/core/MapSystem.js index 8aa86f944..9c9def544 100644 --- a/modules/core/MapSystem.js +++ b/modules/core/MapSystem.js @@ -5,6 +5,7 @@ import { } from '@rapid-sdk/math'; import { AbstractSystem } from './AbstractSystem.js'; +import { QAItem } from '../osm/index.js'; import { utilTotalExtent } from '../util/index.js'; const TILESIZE = 256; @@ -311,6 +312,9 @@ export class MapSystem extends AbstractSystem { * @param prevParams Map(key -> value) of the previous hash parameters */ _hashchange(currParams, prevParams) { + const context = this.context; + const scene = context.systems.gfx.scene; + // map const newMap = currParams.get('map'); const oldMap = prevParams.get('map'); @@ -345,18 +349,56 @@ export class MapSystem extends AbstractSystem { } } } + + // note + // Support opening notes layer with a URL parameter: + // e.g. `note=true` -or- + // e.g. `note=` + const newNote = currParams.get('note') || ''; + const oldNote = prevParams.get('note') || ''; + if (newNote !== oldNote) { + let isEnabled = false; + let noteID = null; + + const vals = newNote.split(',').map(s => s.trim().toLowerCase()).filter(Boolean); + for (const val of vals) { + if (val === 'true') { + isEnabled = true; + continue; + } + // Try the value as a number, but reject things like NaN, null, Infinity + const num = +val; + const valIsNumber = (!isNaN(num) && isFinite(num)); + if (valIsNumber) { + isEnabled = true; + noteID = num; // for now, just select the first one + break; + } + } + + if (noteID) { + this.selectNoteID(noteID); + } else if (isEnabled) { + scene.enableLayers('notes'); + } else { + scene.disableLayers('notes'); + } + } } /** * _updateHash - * Push changes in map state to the urlhash + * Push changes in map state to the urlhash. + * This gets called on 'draw', so fairly frequently */ _updateHash() { const context = this.context; + const scene = context.systems.gfx.scene; const urlhash = context.systems.urlhash; const viewport = context.viewport; + // map const [lon, lat] = viewport.centerLoc(); const transform = viewport.transform; const zoom = transform.zoom; @@ -376,6 +418,28 @@ export class MapSystem extends AbstractSystem { } urlhash.setParam('map', val); + + + // note + const layer = scene.layers.get('notes'); + let noteID; + const [pair] = context.selectedData(); // get the first thing in the Map() + const [datumID, datum] = pair || []; + if (datum instanceof QAItem && datum.service === 'osm') { + noteID = datumID; + } + + // `note=true` -or- `note=` + if (layer?.enabled) { + if (noteID) { + urlhash.setParam('note', noteID); + } else { + urlhash.setParam('note', 'true'); + } + } else { + urlhash.setParam('note', null); + } + } @@ -649,17 +713,24 @@ export class MapSystem extends AbstractSystem { /** * selectEntityID * Selects an entity by ID, loading it first if needed - * @param entityID entityID to select - * @param fitToEntity Whether to force fit the map view to show the entity + * @param {string} entityID - entityID to select + * @param {boolean} fitEntity - Whether to force fit the map view to show the entity */ - selectEntityID(entityID, fitToEntity = false) { + selectEntityID(entityID, fitEntity = false) { const context = this.context; const editor = context.systems.editor; + const scene = context.systems.gfx.scene; const viewport = context.viewport; + if (!entityID) { + context.enter('browse'); + return; + } + const gotEntity = (entity) => { const selectedIDs = context.selectedIDs(); if (context.mode?.id !== 'select-osm' || !selectedIDs.includes(entityID)) { + scene.enableLayers('osm'); context.enter('select-osm', { selection: { osm: [entity.id] }} ); } @@ -670,7 +741,7 @@ export class MapSystem extends AbstractSystem { const isTooSmall = (viewport.transform.zoom < entityZoom - 2); // Can't reasonably see it, or we're forcing the fit. - if (fitToEntity || isOffscreen || isTooSmall) { + if (fitEntity || isOffscreen || isTooSmall) { this.fitEntities(entity); } }; @@ -679,7 +750,6 @@ export class MapSystem extends AbstractSystem { let entity = currGraph.hasEntity(entityID); if (entity) { // have it already gotEntity(entity); - } else { // need to load it first context.loadEntity(entityID, (err, result) => { if (err) return; @@ -691,6 +761,43 @@ export class MapSystem extends AbstractSystem { } + /** + * selectNoteID + * Selects a note by ID, loading it first if needed + * @param {string} noteID - noteID to select + */ + selectNoteID(noteID) { + const context = this.context; + const osm = context.services.osm; + const scene = context.systems.gfx.scene; + + if (!noteID || !osm) { + context.enter('browse'); + return; + } + + const gotNote = (note) => { + scene.enableLayers('notes'); + const selection = new Map().set(note.id, note); + context.enter('select', { selection: selection }); + this.centerZoomEase(note.loc, 19); + }; + + let note = osm.getNote(noteID); + if (note) { + gotNote(note); + } else { // need to load it first + osm.loadNote(noteID, (err) => { + if (err) return; + note = osm.getNote(noteID); + if (note) { + gotNote(note); + } + }); + } + } + + // convenience methods for zooming in and out _zoomIn(delta) { return this.setMapParams(undefined, ~~this.zoom() + delta, undefined, 250); } _zoomOut(delta) { return this.setMapParams(undefined, ~~this.zoom() - delta, undefined, 250); } diff --git a/modules/core/UrlHashSystem.js b/modules/core/UrlHashSystem.js index ad021eac2..a9b166df8 100644 --- a/modules/core/UrlHashSystem.js +++ b/modules/core/UrlHashSystem.js @@ -54,6 +54,7 @@ export class UrlHashSystem extends AbstractSystem { * __`locale`__ - A code specifying the localization to use, affecting the language, layout, and keyboard shortcuts. Multiple codes may be specified in order of preference. The first valid code will be the locale, while the rest will be used as fallbacks if certain text hasn't been translated. The default locale preferences are set by the browser. * __`rtl=true`__ - Force Rapid into right-to-left mode (useful for testing). * __`maproulette`__ - Enable the MapRoulette task layer, e.g.`maproulette=true` -or- `maproulette=` +* __`note`__ - Enable the Notes layer, e.g.`note=true` -or- `note=` * __`overlays`__ - A comma-separated list of imagery sourceIDs to display as overlays * __`photo`__ - The layerID and photoID of a photo to select, e.g `photo=mapillary/fztgSDtLpa08ohPZFZjeRQ` * __`photo_overlay`__ - The street-level photo overlay layers to enable. diff --git a/modules/ui/UiFeatureList.js b/modules/ui/UiFeatureList.js index ecd3a9829..199945f97 100644 --- a/modules/ui/UiFeatureList.js +++ b/modules/ui/UiFeatureList.js @@ -353,36 +353,16 @@ export class UiFeatureList { const context = this.context; const map = context.systems.map; - const osm = context.services.osm; - const scene = context.systems.gfx.scene; if (d.location) { map.centerZoomEase([d.location[1], d.location[0]], 19); } else if (d.id !== -1) { // looks like an OSM Entity utilHighlightEntities([d.id], false, context); - map.selectEntityID(d.id, true); // select and fit , download first if necessary - - } else if (osm && d.noteID) { // looks like an OSM Note - const selectNote = (note) => { - scene.enableLayers('notes'); - map.centerZoomEase(note.loc, 19); - const selection = new Map().set(note.id, note); - context.enter('select', { selection: selection }); - }; - - let note = osm.getNote(d.noteID); - if (note) { - selectNote(note); - } else { - osm.loadNote(d.noteID, (err) => { - if (err) return; - note = osm.getNote(d.noteID); - if (note) { - selectNote(note); - } - }); - } + map.selectEntityID(d.id, true); // select and fit, download first if necessary + + } else if (d.noteID) { // looks like an OSM Note + map.selectNoteID(d.noteID); } }