Files
iD/modules/services/taginfo.js
Quincy Morgan a1af118f0e Ensure locales and presets are loaded before the UI loads (close #7406)
Consolidate localization behavior and init to a coreLocalizer function and singleton
Explicitly support `en-US` locale
Rename coreData to coreFileFetcher and export a singleton rather than using a property of coreContext
Add `apiConnections` property of coreContext to simplify adding a source switcher
Replace some init functions with re-callable, promise-supporting `ensureLoaded` functions
Make coreContext itself load the UI if a container has been specified at init time
Fix code tests
2020-03-31 12:23:31 -07:00

384 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import _debounce from 'lodash-es/debounce';
import { json as d3_json } from 'd3-fetch';
import { utilObjectOmit, utilQsString } from '../util';
import { localizer } from '../core/localizer';
var apibase = 'https://taginfo.openstreetmap.org/api/4/';
var _inflight = {};
var _popularKeys = {};
var _taginfoCache = {};
var tag_sorts = {
point: 'count_nodes',
vertex: 'count_nodes',
area: 'count_ways',
line: 'count_ways'
};
var tag_sort_members = {
point: 'count_node_members',
vertex: 'count_node_members',
area: 'count_way_members',
line: 'count_way_members',
relation: 'count_relation_members'
};
var tag_filters = {
point: 'nodes',
vertex: 'nodes',
area: 'ways',
line: 'ways'
};
var tag_members_fractions = {
point: 'count_node_members_fraction',
vertex: 'count_node_members_fraction',
area: 'count_way_members_fraction',
line: 'count_way_members_fraction',
relation: 'count_relation_members_fraction'
};
function sets(params, n, o) {
if (params.geometry && o[params.geometry]) {
params[n] = o[params.geometry];
}
return params;
}
function setFilter(params) {
return sets(params, 'filter', tag_filters);
}
function setSort(params) {
return sets(params, 'sortname', tag_sorts);
}
function setSortMembers(params) {
return sets(params, 'sortname', tag_sort_members);
}
function clean(params) {
return utilObjectOmit(params, ['geometry', 'debounce']);
}
function filterKeys(type) {
var count_type = type ? 'count_' + type : 'count_all';
return function(d) {
return parseFloat(d[count_type]) > 2500 || d.in_wiki;
};
}
function filterMultikeys(prefix) {
return function(d) {
// d.key begins with prefix, and d.key contains no additional ':'s
var re = new RegExp('^' + prefix + '(.*)$');
var matches = d.key.match(re) || [];
return (matches.length === 2 && matches[1].indexOf(':') === -1);
};
}
function filterValues(allowUpperCase) {
return function(d) {
if (d.value.match(/[;,]/) !== null) return false; // exclude some punctuation
if (!allowUpperCase && d.value.match(/[A-Z*]/) !== null) return false; // exclude uppercase letters
return parseFloat(d.fraction) > 0.0;
};
}
function filterRoles(geometry) {
return function(d) {
if (d.role === '') return false; // exclude empty role
if (d.role.match(/[A-Z*;,]/) !== null) return false; // exclude uppercase letters and some punctuation
return parseFloat(d[tag_members_fractions[geometry]]) > 0.0;
};
}
function valKey(d) {
return {
value: d.key,
title: d.key
};
}
function valKeyDescription(d) {
var obj = {
value: d.value,
title: d.description || d.value
};
if (d.count) {
obj.count = d.count;
}
return obj;
}
function roleKey(d) {
return {
value: d.role,
title: d.role
};
}
// sort keys with ':' lower than keys without ':'
function sortKeys(a, b) {
return (a.key.indexOf(':') === -1 && b.key.indexOf(':') !== -1) ? -1
: (a.key.indexOf(':') !== -1 && b.key.indexOf(':') === -1) ? 1
: 0;
}
var debouncedRequest = _debounce(request, 300, { leading: false });
function request(url, params, exactMatch, callback, loaded) {
if (_inflight[url]) return;
if (checkCache(url, params, exactMatch, callback)) return;
var controller = new AbortController();
_inflight[url] = controller;
d3_json(url, { signal: controller.signal })
.then(function(result) {
delete _inflight[url];
if (loaded) loaded(null, result);
})
.catch(function(err) {
delete _inflight[url];
if (err.name === 'AbortError') return;
if (loaded) loaded(err.message);
});
}
function checkCache(url, params, exactMatch, callback) {
var rp = params.rp || 25;
var testQuery = params.query || '';
var testUrl = url;
do {
var hit = _taginfoCache[testUrl];
// exact match, or shorter match yielding fewer than max results (rp)
if (hit && (url === testUrl || hit.length < rp)) {
callback(null, hit);
return true;
}
// don't try to shorten the query
if (exactMatch || !testQuery.length) return false;
// do shorten the query to see if we already have a cached result
// that has returned fewer than max results (rp)
testQuery = testQuery.slice(0, -1);
testUrl = url.replace(/&query=(.*?)&/, '&query=' + testQuery + '&');
} while (testQuery.length >= 0);
return false;
}
export default {
init: function() {
_inflight = {};
_taginfoCache = {};
_popularKeys = {
// manually exclude some keys #5377, #7485
postal_code: true,
full_name: true,
loc_name: true,
reg_name: true,
short_name: true,
sorting_name: true,
artist_name: true,
nat_name: true,
long_name: true,
'bridge:name': true
};
// Fetch popular keys. We'll exclude these from `values`
// lookups because they stress taginfo, and they aren't likely
// to yield meaningful autocomplete results.. see #3955
var params = {
rp: 100,
sortname: 'values_all',
sortorder: 'desc',
page: 1,
debounce: false,
lang: localizer.languageCode()
};
this.keys(params, function(err, data) {
if (err) return;
data.forEach(function(d) {
if (d.value === 'opening_hours') return; // exception
_popularKeys[d.value] = true;
});
});
},
reset: function() {
Object.values(_inflight).forEach(function(controller) { controller.abort(); });
_inflight = {};
},
keys: function(params, callback) {
var doRequest = params.debounce ? debouncedRequest : request;
params = clean(setSort(params));
params = Object.assign({
rp: 10,
sortname: 'count_all',
sortorder: 'desc',
page: 1,
lang: localizer.languageCode()
}, params);
var url = apibase + 'keys/all?' + utilQsString(params);
doRequest(url, params, false, callback, function(err, d) {
if (err) {
callback(err);
} else {
var f = filterKeys(params.filter);
var result = d.data.filter(f).sort(sortKeys).map(valKey);
_taginfoCache[url] = result;
callback(null, result);
}
});
},
multikeys: function(params, callback) {
var doRequest = params.debounce ? debouncedRequest : request;
params = clean(setSort(params));
params = Object.assign({
rp: 25,
sortname: 'count_all',
sortorder: 'desc',
page: 1,
lang: localizer.languageCode()
}, params);
var prefix = params.query;
var url = apibase + 'keys/all?' + utilQsString(params);
doRequest(url, params, true, callback, function(err, d) {
if (err) {
callback(err);
} else {
var f = filterMultikeys(prefix);
var result = d.data.filter(f).map(valKey);
_taginfoCache[url] = result;
callback(null, result);
}
});
},
values: function(params, callback) {
// Exclude popular keys from values lookups.. see #3955
var key = params.key;
if (key && _popularKeys[key]) {
callback(null, []);
return;
}
var doRequest = params.debounce ? debouncedRequest : request;
params = clean(setSort(setFilter(params)));
params = Object.assign({
rp: 25,
sortname: 'count_all',
sortorder: 'desc',
page: 1,
lang: localizer.languageCode()
}, params);
var url = apibase + 'key/values?' + utilQsString(params);
doRequest(url, params, false, callback, function(err, d) {
if (err) {
callback(err);
} else {
// In most cases we prefer taginfo value results with lowercase letters.
// A few OSM keys expect values to contain uppercase values (see #3377).
// This is not an exhaustive list (e.g. `name` also has uppercase values)
// but these are the fields where taginfo value lookup is most useful.
var re = /network|taxon|genus|species|brand|grape_variety|royal_cypher|listed_status|booth|rating|stars|:output|_hours|_times|_ref|manufacturer|country|target|brewery/;
var allowUpperCase = re.test(params.key);
var f = filterValues(allowUpperCase);
var result = d.data.filter(f).map(valKeyDescription);
_taginfoCache[url] = result;
callback(null, result);
}
});
},
roles: function(params, callback) {
var doRequest = params.debounce ? debouncedRequest : request;
var geometry = params.geometry;
params = clean(setSortMembers(params));
params = Object.assign({
rp: 25,
sortname: 'count_all_members',
sortorder: 'desc',
page: 1,
lang: localizer.languageCode()
}, params);
var url = apibase + 'relation/roles?' + utilQsString(params);
doRequest(url, params, true, callback, function(err, d) {
if (err) {
callback(err);
} else {
var f = filterRoles(geometry);
var result = d.data.filter(f).map(roleKey);
_taginfoCache[url] = result;
callback(null, result);
}
});
},
docs: function(params, callback) {
var doRequest = params.debounce ? debouncedRequest : request;
params = clean(setSort(params));
var path = 'key/wiki_pages?';
if (params.value) {
path = 'tag/wiki_pages?';
} else if (params.rtype) {
path = 'relation/wiki_pages?';
}
var url = apibase + path + utilQsString(params);
doRequest(url, params, true, callback, function(err, d) {
if (err) {
callback(err);
} else {
_taginfoCache[url] = d.data;
callback(null, d.data);
}
});
},
apibase: function(_) {
if (!arguments.length) return apibase;
apibase = _;
return this;
}
};