Skip to content

Commit

Permalink
Add URL parameter for OSM notes
Browse files Browse the repository at this point in the history
(closes #1630)

This adds support for a urlhash parameter `&note=val`
where `val` can be 'true' to enable the layer or a noteID to select
Example:  `note=true` -or- `note=<noteID>`

(in other words, it works like the maproulette parameter)
  • Loading branch information
bhousel committed Dec 2, 2024
1 parent 5ca43ef commit c029d3b
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 30 deletions.
119 changes: 113 additions & 6 deletions modules/core/MapSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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=<noteID>`
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;
Expand All @@ -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=<noteID>`
if (layer?.enabled) {
if (noteID) {
urlhash.setParam('note', noteID);
} else {
urlhash.setParam('note', 'true');
}
} else {
urlhash.setParam('note', null);
}

}


Expand Down Expand Up @@ -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] }} );
}

Expand All @@ -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);
}
};
Expand All @@ -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;
Expand All @@ -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); }
Expand Down
1 change: 1 addition & 0 deletions modules/core/UrlHashSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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=<challengeIDs>`
* __`note`__ - Enable the Notes layer, e.g.`note=true` -or- `note=<noteID>`
* __`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.
Expand Down
28 changes: 4 additions & 24 deletions modules/ui/UiFeatureList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down

0 comments on commit c029d3b

Please sign in to comment.