Files
iD/modules/util/util.js
Quincy Morgan 13ddeecfa7 Fix code tests for #8276 for real this time
Add utilFetchJson to get around some quirks of d3.json and use it for coreFileFetcher
Load real general English locale strings at the beginning of code tests
2021-02-23 11:50:00 -05:00

586 lines
17 KiB
JavaScript

import { remove as removeDiacritics } from 'diacritics';
import { fixRTLTextForSvg, rtlRegex } from './svg_paths_rtl_fix';
import { presetManager } from '../presets';
import { t, localizer } from '../core/localizer';
import { utilArrayUnion } from './array';
import { utilDetect } from './detect';
import { geoExtent } from '../geo/extent';
export function utilTagText(entity) {
var obj = (entity && entity.tags) || {};
return Object.keys(obj)
.map(function(k) { return k + '=' + obj[k]; })
.join(', ');
}
export function utilTotalExtent(array, graph) {
var extent = geoExtent();
var val, entity;
for (var i = 0; i < array.length; i++) {
val = array[i];
entity = typeof val === 'string' ? graph.hasEntity(val) : val;
if (entity) {
extent._extend(entity.extent(graph));
}
}
return extent;
}
export function utilTagDiff(oldTags, newTags) {
var tagDiff = [];
var keys = utilArrayUnion(Object.keys(oldTags), Object.keys(newTags)).sort();
keys.forEach(function(k) {
var oldVal = oldTags[k];
var newVal = newTags[k];
if ((oldVal || oldVal === '') && (newVal === undefined || newVal !== oldVal)) {
tagDiff.push({
type: '-',
key: k,
oldVal: oldVal,
newVal: newVal,
display: '- ' + k + '=' + oldVal
});
}
if ((newVal || newVal === '') && (oldVal === undefined || newVal !== oldVal)) {
tagDiff.push({
type: '+',
key: k,
oldVal: oldVal,
newVal: newVal,
display: '+ ' + k + '=' + newVal
});
}
});
return tagDiff;
}
export function utilEntitySelector(ids) {
return ids.length ? '.' + ids.join(',.') : 'nothing';
}
// returns an selector to select entity ids for:
// - entityIDs passed in
// - shallow descendant entityIDs for any of those entities that are relations
export function utilEntityOrMemberSelector(ids, graph) {
var seen = new Set(ids);
ids.forEach(collectShallowDescendants);
return utilEntitySelector(Array.from(seen));
function collectShallowDescendants(id) {
var entity = graph.hasEntity(id);
if (!entity || entity.type !== 'relation') return;
entity.members
.map(function(member) { return member.id; })
.forEach(function(id) { seen.add(id); });
}
}
// returns an selector to select entity ids for:
// - entityIDs passed in
// - deep descendant entityIDs for any of those entities that are relations
export function utilEntityOrDeepMemberSelector(ids, graph) {
return utilEntitySelector(utilEntityAndDeepMemberIDs(ids, graph));
}
// returns an selector to select entity ids for:
// - entityIDs passed in
// - deep descendant entityIDs for any of those entities that are relations
export function utilEntityAndDeepMemberIDs(ids, graph) {
var seen = new Set();
ids.forEach(collectDeepDescendants);
return Array.from(seen);
function collectDeepDescendants(id) {
if (seen.has(id)) return;
seen.add(id);
var entity = graph.hasEntity(id);
if (!entity || entity.type !== 'relation') return;
entity.members
.map(function(member) { return member.id; })
.forEach(collectDeepDescendants); // recurse
}
}
// returns an selector to select entity ids for:
// - deep descendant entityIDs for any of those entities that are relations
export function utilDeepMemberSelector(ids, graph, skipMultipolgonMembers) {
var idsSet = new Set(ids);
var seen = new Set();
var returners = new Set();
ids.forEach(collectDeepDescendants);
return utilEntitySelector(Array.from(returners));
function collectDeepDescendants(id) {
if (seen.has(id)) return;
seen.add(id);
if (!idsSet.has(id)) {
returners.add(id);
}
var entity = graph.hasEntity(id);
if (!entity || entity.type !== 'relation') return;
if (skipMultipolgonMembers && entity.isMultipolygon()) return;
entity.members
.map(function(member) { return member.id; })
.forEach(collectDeepDescendants); // recurse
}
}
// Adds or removes highlight styling for the specified entities
export function utilHighlightEntities(ids, highlighted, context) {
context.surface()
.selectAll(utilEntityOrDeepMemberSelector(ids, context.graph()))
.classed('highlighted', highlighted);
}
// returns an Array that is the union of:
// - nodes for any nodeIDs passed in
// - child nodes of any wayIDs passed in
// - descendant member and child nodes of relationIDs passed in
export function utilGetAllNodes(ids, graph) {
var seen = new Set();
var nodes = new Set();
ids.forEach(collectNodes);
return Array.from(nodes);
function collectNodes(id) {
if (seen.has(id)) return;
seen.add(id);
var entity = graph.hasEntity(id);
if (!entity) return;
if (entity.type === 'node') {
nodes.add(entity);
} else if (entity.type === 'way') {
entity.nodes.forEach(collectNodes);
} else {
entity.members
.map(function(member) { return member.id; })
.forEach(collectNodes); // recurse
}
}
}
export function utilDisplayName(entity) {
var localizedNameKey = 'name:' + localizer.languageCode().toLowerCase();
var name = entity.tags[localizedNameKey] || entity.tags.name || '';
if (name) return name;
var tags = {
direction: entity.tags.direction,
from: entity.tags.from,
network: entity.tags.cycle_network || entity.tags.network,
ref: entity.tags.ref,
to: entity.tags.to,
via: entity.tags.via
};
var keyComponents = [];
if (tags.network) {
keyComponents.push('network');
}
if (tags.ref) {
keyComponents.push('ref');
}
// Routes may need more disambiguation based on direction or destination
if (entity.tags.route) {
if (tags.direction) {
keyComponents.push('direction');
} else if (tags.from && tags.to) {
keyComponents.push('from');
keyComponents.push('to');
if (tags.via) {
keyComponents.push('via');
}
}
}
if (keyComponents.length) {
name = t('inspector.display_name.' + keyComponents.join('_'), tags);
}
return name;
}
export function utilDisplayNameForPath(entity) {
var name = utilDisplayName(entity);
var isFirefox = utilDetect().browser.toLowerCase().indexOf('firefox') > -1;
if (!isFirefox && name && rtlRegex.test(name)) {
name = fixRTLTextForSvg(name);
}
return name;
}
export function utilDisplayType(id) {
return {
n: t('inspector.node'),
w: t('inspector.way'),
r: t('inspector.relation')
}[id.charAt(0)];
}
export function utilDisplayLabel(entity, graphOrGeometry) {
var displayName = utilDisplayName(entity);
if (displayName) {
// use the display name if there is one
return displayName;
}
var preset = typeof graphOrGeometry === 'string' ?
presetManager.matchTags(entity.tags, graphOrGeometry) :
presetManager.match(entity, graphOrGeometry);
if (preset && preset.name()) {
// use the preset name if there is a match
return preset.name();
}
// fallback to the display type (node/way/relation)
return utilDisplayType(entity.id);
}
export function utilEntityRoot(entityType) {
return {
node: 'n',
way: 'w',
relation: 'r'
}[entityType];
}
// Returns a single object containing the tags of all the given entities.
// Example:
// {
// highway: 'service',
// service: 'parking_aisle'
// }
// +
// {
// highway: 'service',
// service: 'driveway',
// width: '3'
// }
// =
// {
// highway: 'service',
// service: [ 'driveway', 'parking_aisle' ],
// width: [ '3', undefined ]
// }
export function utilCombinedTags(entityIDs, graph) {
var tags = {};
var tagCounts = {};
var allKeys = new Set();
var entities = entityIDs.map(function(entityID) {
return graph.hasEntity(entityID);
}).filter(Boolean);
// gather the aggregate keys
entities.forEach(function(entity) {
var keys = Object.keys(entity.tags).filter(Boolean);
keys.forEach(function(key) {
allKeys.add(key);
});
});
entities.forEach(function(entity) {
allKeys.forEach(function(key) {
var value = entity.tags[key]; // purposely allow `undefined`
if (!tags.hasOwnProperty(key)) {
// first value, set as raw
tags[key] = value;
} else {
if (!Array.isArray(tags[key])) {
if (tags[key] !== value) {
// first alternate value, replace single value with array
tags[key] = [tags[key], value];
}
} else { // type is array
if (tags[key].indexOf(value) === -1) {
// subsequent alternate value, add to array
tags[key].push(value);
}
}
}
var tagHash = key + '=' + value;
if (!tagCounts[tagHash]) tagCounts[tagHash] = 0;
tagCounts[tagHash] += 1;
});
});
for (var key in tags) {
if (!Array.isArray(tags[key])) continue;
// sort values by frequency then alphabetically
tags[key] = tags[key].sort(function(val1, val2) {
var key = key; // capture
var count2 = tagCounts[key + '=' + val2];
var count1 = tagCounts[key + '=' + val1];
if (count2 !== count1) {
return count2 - count1;
}
if (val2 && val1) {
return val1.localeCompare(val2);
}
return val1 ? 1 : -1;
});
}
return tags;
}
export function utilStringQs(str) {
var i = 0; // advance past any leading '?' or '#' characters
while (i < str.length && (str[i] === '?' || str[i] === '#')) i++;
str = str.slice(i);
return str.split('&').reduce(function(obj, pair){
var parts = pair.split('=');
if (parts.length === 2) {
obj[parts[0]] = (null === parts[1]) ? '' : decodeURIComponent(parts[1]);
}
return obj;
}, {});
}
export function utilQsString(obj, noencode) {
// encode everything except special characters used in certain hash parameters:
// "/" in map states, ":", ",", {" and "}" in background
function softEncode(s) {
return encodeURIComponent(s).replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent);
}
return Object.keys(obj).sort().map(function(key) {
return encodeURIComponent(key) + '=' + (
noencode ? softEncode(obj[key]) : encodeURIComponent(obj[key]));
}).join('&');
}
export function utilPrefixDOMProperty(property) {
var prefixes = ['webkit', 'ms', 'moz', 'o'];
var i = -1;
var n = prefixes.length;
var s = document.body;
if (property in s) return property;
property = property.substr(0, 1).toUpperCase() + property.substr(1);
while (++i < n) {
if (prefixes[i] + property in s) {
return prefixes[i] + property;
}
}
return false;
}
export function utilPrefixCSSProperty(property) {
var prefixes = ['webkit', 'ms', 'Moz', 'O'];
var i = -1;
var n = prefixes.length;
var s = document.body.style;
if (property.toLowerCase() in s) {
return property.toLowerCase();
}
while (++i < n) {
if (prefixes[i] + property in s) {
return '-' + prefixes[i].toLowerCase() + property.replace(/([A-Z])/g, '-$1').toLowerCase();
}
}
return false;
}
var transformProperty;
export function utilSetTransform(el, x, y, scale) {
var prop = transformProperty = transformProperty || utilPrefixCSSProperty('Transform');
var translate = utilDetect().opera ? 'translate(' + x + 'px,' + y + 'px)'
: 'translate3d(' + x + 'px,' + y + 'px,0)';
return el.style(prop, translate + (scale ? ' scale(' + scale + ')' : ''));
}
// Calculates Levenshtein distance between two strings
// see: https://en.wikipedia.org/wiki/Levenshtein_distance
// first converts the strings to lowercase and replaces diacritic marks with ascii equivalents.
export function utilEditDistance(a, b) {
a = removeDiacritics(a.toLowerCase());
b = removeDiacritics(b.toLowerCase());
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
var matrix = [];
var i, j;
for (i = 0; i <= b.length; i++) { matrix[i] = [i]; }
for (j = 0; j <= a.length; j++) { matrix[0][j] = j; }
for (i = 1; i <= b.length; i++) {
for (j = 1; j <= a.length; j++) {
if (b.charAt(i-1) === a.charAt(j-1)) {
matrix[i][j] = matrix[i-1][j-1];
} else {
matrix[i][j] = Math.min(matrix[i-1][j-1] + 1, // substitution
Math.min(matrix[i][j-1] + 1, // insertion
matrix[i-1][j] + 1)); // deletion
}
}
}
return matrix[b.length][a.length];
}
// a d3.mouse-alike which
// 1. Only works on HTML elements, not SVG
// 2. Does not cause style recalculation
export function utilFastMouse(container) {
var rect = container.getBoundingClientRect();
var rectLeft = rect.left;
var rectTop = rect.top;
var clientLeft = +container.clientLeft;
var clientTop = +container.clientTop;
return function(e) {
return [
e.clientX - rectLeft - clientLeft,
e.clientY - rectTop - clientTop
];
};
}
export function utilAsyncMap(inputs, func, callback) {
var remaining = inputs.length;
var results = [];
var errors = [];
inputs.forEach(function(d, i) {
func(d, function done(err, data) {
errors[i] = err;
results[i] = data;
remaining--;
if (!remaining) callback(errors, results);
});
});
}
// wraps an index to an interval [0..length-1]
export function utilWrap(index, length) {
if (index < 0) {
index += Math.ceil(-index/length)*length;
}
return index % length;
}
/**
* a replacement for functor
*
* @param {*} value any value
* @returns {Function} a function that returns that value or the value if it's a function
*/
export function utilFunctor(value) {
if (typeof value === 'function') return value;
return function() {
return value;
};
}
export function utilNoAuto(selection) {
var isText = (selection.size() && selection.node().tagName.toLowerCase() === 'textarea');
return selection
// assign 'new-password' even for non-password fields to prevent browsers (Chrome) ignoring 'off'
.attr('autocomplete', 'new-password')
.attr('autocorrect', 'off')
.attr('autocapitalize', 'off')
.attr('spellcheck', isText ? 'true' : 'false');
}
// https://stackoverflow.com/questions/194846/is-there-any-kind-of-hash-code-function-in-javascript
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
export function utilHashcode(str) {
var hash = 0;
if (str.length === 0) {
return hash;
}
for (var i = 0; i < str.length; i++) {
var char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
// Returns version of `str` with all runs of special characters replaced by `_`;
// suitable for HTML ids, classes, selectors, etc.
export function utilSafeClassName(str) {
return str.toLowerCase().replace(/[^a-z0-9]+/g, '_');
}
// Returns string based on `val` that is highly unlikely to collide with an id
// used previously or that's present elsewhere in the document. Useful for preventing
// browser-provided autofills or when embedding iD on pages with unknown elements.
export function utilUniqueDomId(val) {
return 'ideditor-' + utilSafeClassName(val.toString()) + '-' + new Date().getTime().toString();
}
// Returns the length of `str` in unicode characters. This can be less than
// `String.length()` since a single unicode character can be composed of multiple
// JavaScript UTF-16 code units.
export function utilUnicodeCharsCount(str) {
// Native ES2015 implementations of `Array.from` split strings into unicode characters
return Array.from(str).length;
}
// Returns a new string representing `str` cut from its start to `limit` length
// in unicode characters. Note that this runs the risk of splitting graphemes.
export function utilUnicodeCharsTruncated(str, limit) {
return Array.from(str).slice(0, limit).join('');
}
// Variation of d3.json (https://github.com/d3/d3-fetch/blob/master/src/json.js)
export function utilFetchJson(resourse, init) {
return fetch(resourse, init)
.then((response) => {
// fetch in PhantomJS tests may return ok=false and status=0 even if it's okay
if ((!response.ok && response.status !== 0) || !response.json) throw new Error(response.status + ' ' + response.statusText);
if (response.status === 204 || response.status === 205) return;
return response.json();
});
}