modules/behavior/hash.js (160 lines of code) (raw):
import { select as d3_select } from 'd3-selection';
import { geoSphericalDistance } from '@id-sdk/math';
import { utilArrayIdentical, utilObjectOmit, utilQsString, utilStringQs } from '@id-sdk/util';
import _throttle from 'lodash-es/throttle';
import { t } from '../core/localizer';
import { modeBrowse } from '../modes/browse';
import { modeSelect } from '../modes/select';
import { utilDisplayLabel } from '../util';
export function behaviorHash(context) {
// cached window.location.hash
var _cachedHash = null;
// allowable latitude range
var _latitudeLimit = 90 - 1e-8;
function computedHashParameters() {
var map = context.map();
var center = map.center();
var zoom = map.zoom();
var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
var oldParams = utilObjectOmit(utilStringQs(window.location.hash),
['comment', 'source', 'hashtags', 'walkthrough']
);
var newParams = {};
delete oldParams.id;
var selected = context.selectedIDs().filter(function(id) {
return context.hasEntity(id);
});
if (selected.length) {
newParams.id = selected.join(',');
}
newParams.map = zoom.toFixed(2) +
'/' + center[1].toFixed(precision) +
'/' + center[0].toFixed(precision);
return Object.assign(oldParams, newParams);
}
function computedHash() {
return '#' + utilQsString(computedHashParameters(), true);
}
function computedTitle(includeChangeCount) {
var baseTitle = context.documentTitleBase() || 'iD';
var contextual;
var changeCount;
var titleID;
var selected = context.selectedIDs().filter(function(id) {
return context.hasEntity(id);
});
if (selected.length) {
var firstLabel = utilDisplayLabel(context.entity(selected[0]), context.graph());
if (selected.length > 1 ) {
contextual = t('title.labeled_and_more', {
labeled: firstLabel,
count: selected.length - 1
});
} else {
contextual = firstLabel;
}
titleID = 'context';
}
if (includeChangeCount) {
changeCount = context.history().difference().summary().length;
if (changeCount > 0) {
titleID = contextual ? 'changes_context' : 'changes';
}
}
if (titleID) {
return t('title.format.' + titleID, {
changes: changeCount,
base: baseTitle,
context: contextual
});
}
return baseTitle;
}
function updateTitle(includeChangeCount) {
if (!context.setsDocumentTitle()) return;
var newTitle = computedTitle(includeChangeCount);
if (document.title !== newTitle) {
document.title = newTitle;
}
}
function updateHashIfNeeded() {
if (context.inIntro()) return;
var latestHash = computedHash();
if (_cachedHash !== latestHash) {
_cachedHash = latestHash;
// Update the URL hash without affecting the browser navigation stack,
// though unavoidably creating a browser history entry
window.history.replaceState(null, computedTitle(false /* includeChangeCount */), latestHash);
// set the title we want displayed for the browser tab/window
updateTitle(true /* includeChangeCount */);
}
}
var _throttledUpdate = _throttle(updateHashIfNeeded, 500);
var _throttledUpdateTitle = _throttle(function() {
updateTitle(true /* includeChangeCount */);
}, 500);
function hashchange() {
// ignore spurious hashchange events
if (window.location.hash === _cachedHash) return;
_cachedHash = window.location.hash;
var q = utilStringQs(_cachedHash);
var mapArgs = (q.map || '').split('/').map(Number);
if (mapArgs.length < 3 || mapArgs.some(isNaN)) {
// replace bogus hash
updateHashIfNeeded();
} else {
// don't update if the new hash already reflects the state of iD
if (_cachedHash === computedHash()) return;
var mode = context.mode();
context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);
if (q.id && mode) {
var ids = q.id.split(',').filter(function(id) {
return context.hasEntity(id);
});
if (ids.length &&
(mode.id === 'browse' || (mode.id === 'select' && !utilArrayIdentical(mode.selectedIDs(), ids)))) {
context.enter(modeSelect(context, ids));
return;
}
}
var center = context.map().center();
var dist = geoSphericalDistance(center, [mapArgs[2], mapArgs[1]]);
var maxdist = 500;
// Don't allow the hash location to change too much while drawing
// This can happen if the user accidentally hit the back button. #3996
if (mode && mode.id.match(/^draw/) !== null && dist > maxdist) {
context.enter(modeBrowse(context));
return;
}
}
}
function behavior() {
context.map()
.on('move.behaviorHash', _throttledUpdate);
context.history()
.on('change.behaviorHash', _throttledUpdateTitle);
context
.on('enter.behaviorHash', _throttledUpdate);
d3_select(window)
.on('hashchange.behaviorHash', hashchange);
if (window.location.hash) {
var q = utilStringQs(window.location.hash);
if (q.id) {
//if (!context.history().hasRestorableChanges()) {
// targeting specific features: download, select, and zoom to them
context.zoomToEntity(q.id.split(',')[0], !q.map);
//}
}
if (q.walkthrough === 'true') {
behavior.startWalkthrough = true;
}
if (q.map) {
behavior.hadHash = true;
}
hashchange();
updateTitle(false);
}
}
behavior.off = function() {
_throttledUpdate.cancel();
_throttledUpdateTitle.cancel();
context.map()
.on('move.behaviorHash', null);
context
.on('enter.behaviorHash', null);
d3_select(window)
.on('hashchange.behaviorHash', null);
window.location.hash = '';
};
return behavior;
}