Fix merge conflicts

This commit is contained in:
Nikola Plesa
2020-10-15 18:15:57 +02:00
502 changed files with 8920 additions and 6324 deletions
+5 -5
View File
@@ -40,7 +40,7 @@ export function actionCircularize(wayId, projection, maxAngle) {
var sign = d3_polygonArea(points) > 0 ? 1 : -1;
var ids, i, j, k;
// we need atleast two key nodes for the algorithm to work
// we need at least two key nodes for the algorithm to work
if (!keyNodes.length) {
keyNodes = [nodes[0]];
keyPoints = [points[0]];
@@ -55,8 +55,8 @@ export function actionCircularize(wayId, projection, maxAngle) {
}
// key points and nodes are those connected to the ways,
// they are projected onto the circle, inbetween nodes are moved
// to constant intervals between key nodes, extra inbetween nodes are
// they are projected onto the circle, in between nodes are moved
// to constant intervals between key nodes, extra in between nodes are
// added if necessary.
for (i = 0; i < keyPoints.length; i++) {
var nextKeyNodeIndex = (i + 1) % keyNodes.length;
@@ -119,7 +119,7 @@ export function actionCircularize(wayId, projection, maxAngle) {
graph = graph.replace(node);
}
// add new inbetween nodes if necessary
// add new in between nodes if necessary
for (j = 0; j < numberNewPoints; j++) {
angle = startAngle + (indexRange + j) * eachAngle;
loc = projection.invert([
@@ -147,7 +147,7 @@ export function actionCircularize(wayId, projection, maxAngle) {
// Check for other ways that share these keyNodes..
// If keyNodes are adjacent in both ways,
// we can add inBetween nodes to that shared way too..
// we can add inBetweenNodes to that shared way too..
if (indexRange === 1 && inBetweenNodes.length) {
var startIndex1 = way.nodes.lastIndexOf(startNode.id);
var endIndex1 = way.nodes.lastIndexOf(endNode.id);
+1 -1
View File
@@ -13,7 +13,7 @@ export function actionDeleteMultiple(ids) {
var action = function(graph) {
ids.forEach(function(id) {
if (graph.hasEntity(id)) { // It may have been deleted aready.
if (graph.hasEntity(id)) { // It may have been deleted already.
graph = actions[graph.entity(id).type](id)(graph);
}
});
+1 -1
View File
@@ -1,7 +1,7 @@
import { osmNode } from '../osm/node';
// Disconect the ways at the given node.
// Disconnect the ways at the given node.
//
// Optionally, disconnect only the given ways.
//
+20 -2
View File
@@ -50,7 +50,18 @@ export function actionExtract(entityID) {
extractedLoc = entity.extent(graph).center();
}
var isBuilding = entity.tags.building && entity.tags.building !== 'no';
var indoorAreaValues = {
area: true,
corridor: true,
elevator: true,
level: true,
room: true
};
var isBuilding = (entity.tags.building && entity.tags.building !== 'no') ||
(entity.tags['building:part'] && entity.tags['building:part'] !== 'no');
var isIndoorArea = fromGeometry === 'area' && entity.tags.indoor && indoorAreaValues[entity.tags.indoor];
var entityTags = Object.assign({}, entity.tags); // shallow copy
var pointTags = {};
@@ -71,6 +82,10 @@ export function actionExtract(entityID) {
key.match(/^building:.{1,}/) ||
key.match(/^roof:.{1,}/)) continue;
}
// leave `indoor` tag on the area
if (isIndoorArea && key === 'indoor') {
continue;
}
// copy the tag from the entity to the point
pointTags[key] = entityTags[key];
@@ -79,13 +94,16 @@ export function actionExtract(entityID) {
if (keysToCopyAndRetain.indexOf(key) !== -1 ||
key.match(/^addr:.{1,}/)) {
continue;
} else if (isIndoorArea && key === 'level') {
// leave `level` on both features
continue;
}
// remove the tag from the entity
delete entityTags[key];
}
if (!isBuilding && fromGeometry === 'area') {
if (!isBuilding && !isIndoorArea && fromGeometry === 'area') {
// ensure that areas keep area geometry
entityTags.area = 'yes';
}
+1
View File
@@ -30,6 +30,7 @@ export { actionRestrictTurn } from './restrict_turn';
export { actionReverse } from './reverse';
export { actionRevert } from './revert';
export { actionRotate } from './rotate';
export { actionScale } from './scale';
export { actionSplit } from './split';
export { actionStraightenNodes } from './straighten_nodes';
export { actionStraightenWay } from './straighten_way';
+24
View File
@@ -0,0 +1,24 @@
import { utilGetAllNodes } from '../util';
export function actionScale(ids, pivotLoc, scaleFactor, projection) {
return function(graph) {
return graph.update(function(graph) {
let point, radial;
utilGetAllNodes(ids, graph).forEach(function(node) {
point = projection(node.loc);
radial = [
point[0] - pivotLoc[0],
point[1] - pivotLoc[1]
];
point = [
pivotLoc[0] + (scaleFactor * radial[0]),
pivotLoc[1] + (scaleFactor * radial[1])
];
graph = graph.replace(node.move(projection.invert(point)));
});
});
};
}
+130 -51
View File
@@ -1,9 +1,9 @@
import { actionAddMember } from './add_member';
import { geoSphericalDistance } from '../geo';
import { geoSphericalDistance } from '../geo/geo';
import { osmIsOldMultipolygonOuterMember } from '../osm/multipolygon';
import { osmRelation } from '../osm/relation';
import { osmWay } from '../osm/way';
import { utilArrayIntersection, utilWrap } from '../util';
import { utilArrayIntersection, utilWrap, utilArrayUniq } from '../util';
// Split a way at the given node.
@@ -20,11 +20,23 @@ import { utilArrayIntersection, utilWrap } from '../util';
// Reference:
// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as
//
export function actionSplit(nodeId, newWayIds) {
export function actionSplit(nodeIds, newWayIds) {
// accept single ID for backwards-compatiblity
if (typeof nodeIds === 'string') nodeIds = [nodeIds];
var _wayIDs;
// the strategy for picking which way will have a new version and which way is newly created
var _keepHistoryOn = 'longest'; // 'longest', 'first'
// The IDs of the ways actually created by running this action
var createdWayIDs = [];
var _createdWayIDs = [];
function dist(graph, nA, nB) {
var locA = graph.entity(nA).loc;
var locB = graph.entity(nB).loc;
var epsilon = 1e-6;
return (locA && locB) ? geoSphericalDistance(locA, locB) : epsilon;
}
// If the way is closed, we need to search for a partner node
// to split the way at.
@@ -47,23 +59,16 @@ export function actionSplit(nodeId, newWayIds) {
return utilWrap(index, nodes.length);
}
function dist(nA, nB) {
var locA = graph.entity(nA).loc;
var locB = graph.entity(nB).loc;
var epsilon = 1e-6;
return (locA && locB) ? geoSphericalDistance(locA, locB) : epsilon;
}
// calculate lengths
length = 0;
for (i = wrap(idxA + 1); i !== idxA; i = wrap(i + 1)) {
length += dist(nodes[i], nodes[wrap(i - 1)]);
length += dist(graph, nodes[i], nodes[wrap(i - 1)]);
lengths[i] = length;
}
length = 0;
for (i = wrap(idxA - 1); i !== idxA; i = wrap(i - 1)) {
length += dist(nodes[i], nodes[wrap(i + 1)]);
length += dist(graph, nodes[i], nodes[wrap(i + 1)]);
if (length < lengths[i]) {
lengths[i] = length;
}
@@ -71,7 +76,7 @@ export function actionSplit(nodeId, newWayIds) {
// determine best opposite node to split
for (i = 0; i < nodes.length; i++) {
var cost = lengths[i] / dist(nodes[idxA], nodes[i]);
var cost = lengths[i] / dist(graph, nodes[idxA], nodes[i]);
if (cost > best) {
idxB = i;
best = cost;
@@ -81,8 +86,15 @@ export function actionSplit(nodeId, newWayIds) {
return idxB;
}
function totalLengthBetweenNodes(graph, nodes) {
var totalLength = 0;
for (var i = 0; i < nodes.length - 1; i++) {
totalLength += dist(graph, nodes[i], nodes[i + 1]);
}
return totalLength;
}
function split(graph, wayA, newWayId) {
function split(graph, nodeId, wayA, newWayId) {
var wayB = osmWay({ id: newWayId, tags: wayA.tags }); // `wayB` is the NEW way
var origNodes = wayA.nodes.slice();
var nodesA;
@@ -108,8 +120,48 @@ export function actionSplit(nodeId, newWayIds) {
nodesB = wayA.nodes.slice(idx);
}
wayA = wayA.update({ nodes: nodesA });
wayB = wayB.update({ nodes: nodesB });
var lengthA = totalLengthBetweenNodes(graph, nodesA);
var lengthB = totalLengthBetweenNodes(graph, nodesB);
if (_keepHistoryOn === 'longest' &&
lengthB > lengthA) {
// keep the history on the longer way, regardless of the node count
wayA = wayA.update({ nodes: nodesB });
wayB = wayB.update({ nodes: nodesA });
var temp = lengthA;
lengthA = lengthB;
lengthB = temp;
} else {
wayA = wayA.update({ nodes: nodesA });
wayB = wayB.update({ nodes: nodesB });
}
if (wayA.tags.step_count) {
// divide up the the step count proportionally between the two ways
var stepCount = parseFloat(wayA.tags.step_count);
if (stepCount &&
// ensure a number
isFinite(stepCount) &&
// ensure positive
stepCount > 0 &&
// ensure integer
Math.round(stepCount) === stepCount) {
var tagsA = Object.assign({}, wayA.tags);
var tagsB = Object.assign({}, wayB.tags);
var ratioA = lengthA / (lengthA + lengthB);
var countA = Math.round(stepCount * ratioA);
tagsA.step_count = countA.toString();
tagsB.step_count = (stepCount - countA).toString();
wayA = wayA.update({ tags: tagsA });
wayB = wayB.update({ tags: tagsB });
}
}
graph = graph.replace(wayA);
graph = graph.replace(wayB);
@@ -204,57 +256,77 @@ export function actionSplit(nodeId, newWayIds) {
graph = graph.replace(wayB.update({ tags: {} }));
}
createdWayIDs.push(wayB.id);
_createdWayIDs.push(wayB.id);
return graph;
}
var action = function(graph) {
var candidates = action.ways(graph);
createdWayIDs = [];
for (var i = 0; i < candidates.length; i++) {
graph = split(graph, candidates[i], newWayIds && newWayIds[i]);
_createdWayIDs = [];
var newWayIndex = 0;
for (var i = 0; i < nodeIds.length; i++) {
var nodeId = nodeIds[i];
var candidates = action.waysForNode(nodeId, graph);
for (var j = 0; j < candidates.length; j++) {
graph = split(graph, nodeId, candidates[j], newWayIds && newWayIds[newWayIndex]);
newWayIndex += 1;
}
}
return graph;
};
action.getCreatedWayIDs = function() {
return createdWayIDs;
return _createdWayIDs;
};
action.waysForNode = function(nodeId, graph) {
var node = graph.entity(nodeId);
var splittableParents = graph.parentWays(node).filter(isSplittable);
if (!_wayIDs) {
// If the ways to split aren't specified, only split the lines.
// If there are no lines to split, split the areas.
var hasLine = splittableParents.some(function(parent) {
return parent.geometry(graph) === 'line';
});
if (hasLine) {
return splittableParents.filter(function(parent) {
return parent.geometry(graph) === 'line';
});
}
}
return splittableParents;
function isSplittable(parent) {
// If the ways to split are specified, ignore everything else.
if (_wayIDs && _wayIDs.indexOf(parent.id) === -1) return false;
// We can fake splitting closed ways at their endpoints...
if (parent.isClosed()) return true;
// otherwise, we can't split nodes at their endpoints.
for (var i = 1; i < parent.nodes.length - 1; i++) {
if (parent.nodes[i] === nodeId) return true;
}
return false;
}
};
action.ways = function(graph) {
var node = graph.entity(nodeId);
var parents = graph.parentWays(node);
var hasLines = parents.some(function(parent) {
return parent.geometry(graph) === 'line';
});
return parents.filter(function(parent) {
if (_wayIDs && _wayIDs.indexOf(parent.id) === -1)
return false;
if (!_wayIDs && hasLines && parent.geometry(graph) !== 'line')
return false;
if (parent.isClosed()) {
return true;
}
for (var i = 1; i < parent.nodes.length - 1; i++) {
if (parent.nodes[i] === nodeId) {
return true;
}
}
return false;
});
return utilArrayUniq([].concat.apply([], nodeIds.map(function(nodeId) {
return action.waysForNode(nodeId, graph);
})));
};
action.disabled = function(graph) {
var candidates = action.ways(graph);
if (candidates.length === 0 || (_wayIDs && _wayIDs.length !== candidates.length)) {
return 'not_eligible';
for (var i = 0; i < nodeIds.length; i++) {
var nodeId = nodeIds[i];
var candidates = action.waysForNode(nodeId, graph);
if (candidates.length === 0 || (_wayIDs && _wayIDs.length !== candidates.length)) {
return 'not_eligible';
}
}
};
@@ -266,5 +338,12 @@ export function actionSplit(nodeId, newWayIds) {
};
action.keepHistoryOn = function(val) {
if (!arguments.length) return _keepHistoryOn;
_keepHistoryOn = val;
return action;
};
return action;
}
+7
View File
@@ -7,9 +7,16 @@ export function actionUpgradeTags(entityId, oldTags, replaceTags) {
var semiIndex;
for (var oldTagKey in oldTags) {
if (!(oldTagKey in tags)) continue;
// wildcard match
if (oldTags[oldTagKey] === '*') {
// note the value since we might need to transfer it
transferValue = tags[oldTagKey];
delete tags[oldTagKey];
// exact match
} else if (oldTags[oldTagKey] === tags[oldTagKey]) {
delete tags[oldTagKey];
// match is within semicolon-delimited values
} else {
var vals = tags[oldTagKey].split(';').filter(Boolean);
var oldIndex = vals.indexOf(oldTags[oldTagKey]);
-1
View File
@@ -149,7 +149,6 @@ export function behaviorDrag() {
function behavior(selection) {
_pointerId = null;
var matchesSelector = utilPrefixDOMProperty('matchesSelector');
var delegate = pointerdown;
+14 -14
View File
@@ -167,7 +167,7 @@ export function behaviorDrawWay(context, wayID, mode, startGraph) {
if (includeDrawNode) {
if (parentWay.isClosed()) {
// don't test the last segment for closed ways - #4655
// (still test the first segement)
// (still test the first segment)
nodes.pop();
}
} else { // discount the draw node
@@ -195,23 +195,23 @@ export function behaviorDrawWay(context, wayID, mode, startGraph) {
var nextMode;
if (context.graph() === startGraph) { // we've undone back to the beginning
if (context.graph() === startGraph) {
// We've undone back to the initial state before we started drawing.
// Just exit the draw mode without undoing whatever we did before
// we entered the draw mode.
nextMode = modeSelect(context, [wayID]);
} else {
context.history()
.on('undone.draw', null);
// remove whatever segment was drawn previously
context.undo();
// The `undo` only removed the temporary edit, so here we have to
// manually undo to actually remove the last node we added. We can't
// use the `undo` function since the initial "add" graph doesn't have
// an annotation and so cannot be undone to.
context.pop(1);
if (context.graph() === startGraph) { // we've undone back to the beginning
nextMode = modeSelect(context, [wayID]);
} else {
// continue drawing
nextMode = mode;
}
// continue drawing
nextMode = mode;
}
// clear the redo stack by adding and removing an edit
// clear the redo stack by adding and removing a blank edit
context.perform(actionNoop());
context.pop(1);
@@ -242,7 +242,7 @@ export function behaviorDrawWay(context, wayID, mode, startGraph) {
_headNodeID = typeof _nodeIndex === 'number' ? _origWay.nodes[_nodeIndex] :
(_origWay.isClosed() ? _origWay.nodes[_origWay.nodes.length - 2] : _origWay.nodes[_origWay.nodes.length - 1]);
_wayGeometry = _origWay.geometry(context.graph());
_annotation = t((_origWay.isDegenerate() ?
_annotation = t((_origWay.nodes.length === (_origWay.isClosed() ? 2 : 1) ?
'operations.start.annotation.' :
'operations.continue.annotation.') + _wayGeometry
);
+5 -5
View File
@@ -62,7 +62,7 @@ export function behaviorHash(context) {
if (selected.length > 1 ) {
contextual = t('title.labeled_and_more', {
labeled: firstLabel,
count: (selected.length - 1).toString()
count: selected.length - 1
});
} else {
contextual = firstLabel;
@@ -140,12 +140,12 @@ export function behaviorHash(context) {
context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);
if (q.id) {
if (q.id && mode) {
var ids = q.id.split(',').filter(function(id) {
return context.hasEntity(id);
});
var skip = mode && mode.id === 'select' && utilArrayIdentical(mode.selectedIDs(), ids);
if (ids.length && !skip) {
if (ids.length &&
(mode.id === 'browse' || (mode.id === 'select' && !utilArrayIdentical(mode.selectedIDs(), ids)))) {
context.enter(modeSelect(context, ids));
return;
}
@@ -156,7 +156,7 @@ export function behaviorHash(context) {
var maxdist = 500;
// Don't allow the hash location to change too much while drawing
// This can happen if the user accidently hit the back button. #3996
// 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;
+5 -2
View File
@@ -9,7 +9,10 @@ export function behaviorOperation(context) {
// prevent operations during low zoom selection
if (!context.map().withinEditableZoom()) return;
if (_operation.availableForKeypress && !_operation.availableForKeypress()) return;
d3_event.preventDefault();
var disabled = _operation.disabled();
if (disabled) {
@@ -17,14 +20,14 @@ export function behaviorOperation(context) {
.duration(4000)
.iconName('#iD-operation-' + _operation.id)
.iconClass('operation disabled')
.text(_operation.tooltip)();
.label(_operation.tooltip)();
} else {
context.ui().flash
.duration(2000)
.iconName('#iD-operation-' + _operation.id)
.iconClass('operation')
.text(_operation.annotation() || _operation.title)();
.label(_operation.annotation() || _operation.title)();
if (_operation.point) _operation.point(null);
_operation();
+6 -3
View File
@@ -26,7 +26,7 @@ export function coreContext() {
let context = utilRebind({}, dispatch, 'on');
let _deferred = new Set();
context.version = '2.18.4-dev';
context.version = '2.19.0-dev';
context.privacyVersion = '20200407';
// iD will alter the hash so cache the parameters intended to setup the session
@@ -559,7 +559,6 @@ export function coreContext() {
_map.init();
_validator.init();
_features.init();
_photos.init();
if (services.maprules && context.initialHashParams.maprules) {
d3_json(context.initialHashParams.maprules)
@@ -571,7 +570,11 @@ export function coreContext() {
}
// if the container isn't available, e.g. when testing, don't load the UI
if (!context.container().empty()) _ui.ensureLoaded();
if (!context.container().empty()) {
_ui.ensureLoaded().then(function() {
_photos.init();
});
}
}
};
+150 -63
View File
@@ -1,6 +1,7 @@
import { fileFetcher } from './file_fetcher';
import { utilDetect } from '../util/detect';
import { utilStringQs } from '../util';
import { utilArrayUniq } from '../util/array';
let _mainLocalizer = coreLocalizer(); // singleton
let _t = _mainLocalizer.t;
@@ -20,10 +21,13 @@ export function coreLocalizer() {
let _dataLanguages = {};
// `localeData` is an object containing all _supported_ locale codes -> language info.
// `_dataLocales` is an object containing all _supported_ locale codes -> language info.
// * `rtl` - right-to-left or left-to-right text direction
// * `pct` - the percent of strings translated; 1 = 100%, full coverage
//
// {
// en: { rtl: false, languageNames: {…}, scriptNames: {…} },
// de: { rtl: false, languageNames: {…}, scriptNames: {…} },
// en: { rtl: false, pct: {…} },
// de: { rtl: false, pct: {…} },
// …
// }
let _dataLocales = {};
@@ -36,8 +40,10 @@ export function coreLocalizer() {
// }
let _localeStrings = {};
// the current locale parameters
// the current locale
let _localeCode = 'en-US';
// `_localeCodes` must contain `_localeCode` first, optionally followed by fallbacks
let _localeCodes = ['en-US', 'en'];
let _languageCode = 'en';
let _textDirection = 'ltr';
let _usesMetric = false;
@@ -46,6 +52,7 @@ export function coreLocalizer() {
// getters for the current locale parameters
localizer.localeCode = () => _localeCode;
localizer.localeCodes = () => _localeCodes;
localizer.languageCode = () => _languageCode;
localizer.textDirection = () => _textDirection;
localizer.usesMetric = () => _usesMetric;
@@ -75,7 +82,7 @@ export function coreLocalizer() {
if (_loadPromise) return _loadPromise;
return _loadPromise = Promise.all([
// load the list of langauges
// load the list of languages
fileFetcher.get('languages'),
// load the list of supported locales
fileFetcher.get('locales')
@@ -86,16 +93,24 @@ export function coreLocalizer() {
})
.then(() => {
let requestedLocales = (_preferredLocaleCodes || [])
// list of locales preferred by the browser in priority order
.concat(utilDetect().browserLocales);
_localeCode = bestSupportedLocale(requestedLocales);
// List of locales preferred by the browser in priority order.
.concat(utilDetect().browserLocales)
// fallback to English since it's the only guaranteed complete language
.concat(['en']);
return Promise.all([
// always load the English locale strings as fallbacks
localizer.loadLocale('en'),
// load the preferred locale
localizer.loadLocale(_localeCode)
]);
_localeCodes = localesToUseFrom(requestedLocales);
// Run iD in the highest-priority locale; the rest are fallbacks
_localeCode = _localeCodes[0];
// Will always return the index for `en` if nothing else
const fullCoverageIndex = _localeCodes.findIndex(function(locale) {
return _dataLocales[locale].pct === 1;
});
// We only need to load locales up until we find one with full coverage
const loadStringsPromises = _localeCodes.slice(0, fullCoverageIndex + 1).map(function(code) {
return localizer.loadLocale(code);
});
return Promise.all(loadStringsPromises);
})
.then(() => {
updateForCurrentLocale();
@@ -103,36 +118,23 @@ export function coreLocalizer() {
.catch(err => console.error(err)); // eslint-disable-line
};
// Returns the best locale from `locales` supported by iD, if any
function bestSupportedLocale(locales) {
// Returns the locales from `requestedLocales` supported by iD that we should use
function localesToUseFrom(requestedLocales) {
let supportedLocales = _dataLocales;
for (let i in locales) {
let locale = locales[i];
if (locale.includes('-')) { // full locale ('es-ES')
let toUse = [];
for (let i in requestedLocales) {
let locale = requestedLocales[i];
if (supportedLocales[locale]) toUse.push(locale);
if (supportedLocales[locale]) return locale;
// If full locale not supported ('es-FAKE'), fallback to the base ('es')
if (locale.includes('-')) {
// Full locale ('es-ES'), add fallback to the base ('es')
let langPart = locale.split('-')[0];
if (supportedLocales[langPart]) return langPart;
} else { // base locale ('es')
// prefer a lower-priority full locale with this base ('es' < 'es-ES')
let fullLocale = locales.find((locale2, index) => {
return index > i &&
locale2 !== locale &&
locale2.split('-')[0] === locale &&
supportedLocales[locale2];
});
if (fullLocale) return fullLocale;
if (supportedLocales[locale]) return locale;
if (supportedLocales[langPart]) toUse.push(langPart);
}
}
return null;
// remove duplicates
return utilArrayUniq(toUse);
}
function updateForCurrentLocale() {
@@ -152,8 +154,10 @@ export function coreLocalizer() {
_textDirection = currentData && currentData.rtl ? 'rtl' : 'ltr';
}
_languageNames = currentData && currentData.languageNames;
_scriptNames = currentData && currentData.scriptNames;
let locale = _localeCode;
if (locale.toLowerCase() === 'en-us') locale = 'en';
_languageNames = _localeStrings[locale].languageNames;
_scriptNames = _localeStrings[locale].scriptNames;
_usesMetric = _localeCode.slice(-3).toLowerCase() !== '-us';
}
@@ -191,28 +195,48 @@ export function coreLocalizer() {
});
};
localizer.pluralRule = function(number) {
return pluralRule(number, _localeCode);
};
// Returns the plural rule for the given `number` with the given `localeCode`.
// One of: `zero`, `one`, `two`, `few`, `many`, `other`
function pluralRule(number, localeCode) {
// modern browsers have this functionality built-in
const rules = 'Intl' in window && Intl.PluralRules && new Intl.PluralRules(localeCode);
if (rules) {
return rules.select(number);
}
// fallback to basic one/other, as in English
if (number === 1) return 'one';
return 'other';
}
/**
* Given a string identifier, try to find that string in the current
* language, and return it. This function will be called recursively
* with locale `en` if a string can not be found in the requested language.
* Try to find that string in `locale` or the current `_localeCode` matching
* the given `stringId`. If no string can be found in the requested locale,
* we'll recurse down all the `_localeCodes` until one is found.
*
* @param {string} s string identifier
* @param {string} stringId string identifier
* @param {object?} replacements token replacements and default string
* @param {string?} locale locale to use (defaults to currentLocale)
* @return {string?} localized string
*/
localizer.t = function(s, replacements, locale) {
localizer.tInfo = function(stringId, replacements, locale) {
locale = locale || _localeCode;
// US English is the default
if (locale.toLowerCase() === 'en-us') locale = 'en';
let path = s
let path = stringId
.split('.')
.map(s => s.replace(/<TX_DOT>/g, '.'))
.reverse();
let result = _localeStrings[locale];
let stringsKey = locale;
// US English is the default
if (stringsKey.toLowerCase() === 'en-us') stringsKey = 'en';
let result = _localeStrings[stringsKey];
while (result !== undefined && path.length) {
result = result[path.pop()];
@@ -220,32 +244,95 @@ export function coreLocalizer() {
if (result !== undefined) {
if (replacements) {
for (let k in replacements) {
const token = `{${k}}`;
const regex = new RegExp(token, 'g');
result = result.replace(regex, replacements[k]);
if (typeof result === 'object' && Object.keys(result).length) {
// If plural forms are provided, dig one level deeper based on the
// first numeric token replacement provided.
const number = Object.values(replacements).find(function(value) {
return typeof value === 'number';
});
if (number !== undefined) {
const rule = pluralRule(number, locale);
if (result[rule]) {
result = result[rule];
} else {
// We're pretty sure this should be a plural but no string
// could be found for the given rule. Just pick the first
// string and hope it makes sense.
result = Object.values(result)[0];
}
}
}
if (typeof result === 'string') {
for (let key in replacements) {
let value = replacements[key];
if (typeof value === 'number' && value.toLocaleString) {
// format numbers for the locale
value = value.toLocaleString(locale, {
style: 'decimal',
useGrouping: true,
minimumFractionDigits: 0
});
}
const token = `{${key}}`;
const regex = new RegExp(token, 'g');
result = result.replace(regex, value);
}
}
}
return result;
if (typeof result === 'string') {
// found a localized string!
return {
text: result,
locale: locale
};
}
}
// no localized string found...
if (locale !== 'en') {
return localizer.t(s, replacements, 'en'); // fallback - recurse with 'en'
// attempt to fallback to a lower-priority language
let index = _localeCodes.indexOf(locale);
if (index >= 0 && index < _localeCodes.length - 1) {
// eventually this will be 'en' or another locale with 100% coverage
let fallback = _localeCodes[index + 1];
return localizer.tInfo(stringId, replacements, fallback);
}
if (replacements && 'default' in replacements) {
return replacements.default; // fallback - replacements.default
// Fallback to a default value if one is specified in `replacements`
return {
text: replacements.default,
locale: null
};
}
const missing = `Missing ${locale} translation: ${s}`;
const missing = `Missing ${locale} translation: ${stringId}`;
if (typeof console !== 'undefined') console.error(missing); // eslint-disable-line
return missing;
return {
text: missing,
locale: 'en'
};
};
// Returns only the localized text, discarding the locale info
localizer.t = function(stringId, replacements, locale) {
return localizer.tInfo(stringId, replacements, locale).text;
};
// Returns the localized text wrapped in an HTML element encoding the locale info
localizer.t.html = function(stringId, replacements, locale) {
const info = localizer.tInfo(stringId, replacements, locale);
// text may be empty or undefined if `replacements.default` is
return info.text ? localizer.htmlForLocalizedText(info.text, info.locale) : '';
};
localizer.htmlForLocalizedText = function(text, localeCode) {
return `<span class="localized-text" lang="${localeCode || 'unknown'}">${text}</span>`;
};
localizer.languageName = (code, options) => {
if (_languageNames[code]) { // name in locale langauge
if (_languageNames[code]) { // name in locale language
// e.g. "German"
return _languageNames[code];
}
@@ -260,9 +347,9 @@ export function coreLocalizer() {
return localizer.t('translate.language_and_code', { language: langInfo.nativeName, code: code });
} else if (langInfo.base && langInfo.script) {
const base = langInfo.base; // the code of the langauge this is based on
const base = langInfo.base; // the code of the language this is based on
if (_languageNames[base]) { // base language name in locale langauge
if (_languageNames[base]) { // base language name in locale language
const scriptCode = langInfo.script;
const script = _scriptNames[scriptCode] || scriptCode;
// e.g. "Serbian (Cyrillic)"
+1 -1
View File
@@ -26,7 +26,7 @@ export function coreTree(head) {
function segmentBBox(segment) {
var extent = segment.extent(head);
// extent can be null if the node entites aren't in the graph for some reason
// extent can be null if the node entities aren't in the graph for some reason
if (!extent) return null;
var bbox = extent.bbox();
+2 -1
View File
@@ -58,7 +58,7 @@ export function validationIssue(attrs) {
if (issue.severity === 'warning') {
// allow ignoring any issue that's not an error
fixes.push(new validationIssueFix({
title: t('issues.fix.ignore_issue.title'),
title: t.html('issues.fix.ignore_issue.title'),
icon: 'iD-icon-close',
onClick: function() {
context.validator().ignoreIssue(this.issue.id);
@@ -67,6 +67,7 @@ export function validationIssue(attrs) {
}
fixes.forEach(function(fix) {
// the id doesn't matter as long as it's unique to this issue/fix
fix.id = fix.title;
// add a reference to the issue for use in actions
fix.issue = issue;
+1 -1
View File
@@ -413,7 +413,7 @@ export function coreValidator(context) {
.on('redone.validator', validator.validate); // redo
// but not on 'change' (e.g. while drawing)
// When user chages editing modes:
// When user changes editing modes:
context
.on('exit.validator', validator.validate);
+1 -1
View File
@@ -37,7 +37,7 @@ export { geoVecEqual } from './vector.js';
export { geoVecFloor } from './vector.js';
export { geoVecInterp } from './vector.js';
export { geoVecLength } from './vector.js';
export { geoVecLengthSquare } from '/vector.js';
export { geoVecLengthSquare } from './vector.js';
export { geoVecNormalize } from './vector.js';
export { geoVecNormalizedDot } from './vector.js';
export { geoVecProject } from './vector.js';
+1 -2
View File
@@ -10,8 +10,7 @@ export function modeAddNote(context) {
var mode = {
id: 'add-note',
button: 'note',
title: t('modes.add_note.title'),
description: t('modes.add_note.description'),
description: t.html('modes.add_note.description'),
key: t('modes.add_note.key')
};
+8 -5
View File
@@ -138,7 +138,8 @@ export function modeDragNode(context) {
if (hasHidden) {
context.ui().flash
.duration(4000)
.text(t('modes.drag_node.connected_to_hidden'))();
.iconName('#iD-icon-no')
.label(t('modes.drag_node.connected_to_hidden'))();
}
return drag.cancel();
}
@@ -237,7 +238,8 @@ export function modeDragNode(context) {
if (!nope) { // about to nope - show hint
context.ui().flash
.duration(4000)
.text(t('operations.connect.' + isInvalid,
.iconName('#iD-icon-no')
.label(t('operations.connect.' + isInvalid,
{ relation: presetManager.item('type/restriction').name() }
))();
}
@@ -245,12 +247,13 @@ export function modeDragNode(context) {
var errorID = isInvalid === 'line' ? 'lines' : 'areas';
context.ui().flash
.duration(3000)
.text(t('self_intersection.error.' + errorID))();
.iconName('#iD-icon-no')
.label(t('self_intersection.error.' + errorID))();
} else {
if (nope) { // about to un-nope, remove hint
context.ui().flash
.duration(1)
.text('')();
.label('')();
}
}
@@ -324,7 +327,7 @@ export function modeDragNode(context) {
for (k = 0; k < rings.length; k++) {
if (k === activeIndex) continue;
// make sure active ring doesnt cross passive rings
// make sure active ring doesn't cross passive rings
if (geoHasLineIntersections(rings[activeIndex].nodes, rings[k].nodes, entity.id)) {
return 'multipolygonRing';
}
+2 -1
View File
@@ -11,7 +11,8 @@ export function modeDrawArea(context, wayID, startGraph, button) {
var behavior = behaviorDrawWay(context, wayID, mode, startGraph)
.on('rejectedSelfIntersection.modeDrawArea', function() {
context.ui().flash
.text(t('self_intersection.error.areas'))();
.iconName('#iD-icon-no')
.label(t('self_intersection.error.areas'))();
});
mode.wayID = wayID;
+2 -1
View File
@@ -11,7 +11,8 @@ export function modeDrawLine(context, wayID, startGraph, button, affix, continui
var behavior = behaviorDrawWay(context, wayID, mode, startGraph)
.on('rejectedSelfIntersection.modeDrawLine', function() {
context.ui().flash
.text(t('self_intersection.error.lines'))();
.iconName('#iD-icon-no')
.label(t('self_intersection.error.lines'))();
});
mode.wayID = wayID;
+1 -1
View File
@@ -39,7 +39,7 @@ export function modeMove(context, entityIDs, baseGraph) {
];
var annotation = entityIDs.length === 1 ?
t('operations.move.annotation.' + context.graph().geometry(entityIDs[0])) :
t('operations.move.annotation.multiple');
t('operations.move.annotation.feature', { n: entityIDs.length });
var _prevGraph;
var _cache;
+1 -1
View File
@@ -43,7 +43,7 @@ export function modeRotate(context, entityIDs) {
];
var annotation = entityIDs.length === 1 ?
t('operations.rotate.annotation.' + context.graph().geometry(entityIDs[0])) :
t('operations.rotate.annotation.multiple');
t('operations.rotate.annotation.feature', { n: entityIDs.length });
var _prevGraph;
var _prevAngle;
+98 -21
View File
@@ -5,6 +5,7 @@ import { t } from '../core/localizer';
import { actionAddMidpoint } from '../actions/add_midpoint';
import { actionDeleteRelation } from '../actions/delete_relation';
import { actionMove } from '../actions/move';
import { actionScale } from '../actions/scale';
import { behaviorBreathe } from '../behavior/breathe';
import { behaviorHover } from '../behavior/hover';
@@ -14,7 +15,7 @@ import { behaviorSelect } from '../behavior/select';
import { operationMove } from '../operations/move';
import { geoExtent, geoChooseEdge } from '../geo';
import { geoExtent, geoChooseEdge, geoMetersToLat, geoMetersToLon } from '../geo';
import { modeBrowse } from './browse';
import { modeDragNode } from './drag_node';
import { modeDragNote } from './drag_note';
@@ -23,7 +24,7 @@ import * as Operations from '../operations/index';
import { uiCmd } from '../ui/cmd';
import {
utilArrayIntersection, utilDeepMemberSelector, utilEntityOrDeepMemberSelector,
utilEntitySelector, utilKeybinding
utilEntitySelector, utilKeybinding, utilTotalExtent, utilGetAllNodes
} from '../util';
@@ -92,7 +93,7 @@ export function modeSelect(context, selectedIDs) {
for (var i = 0; i < selectedIDs.length; i++) {
var entity = context.hasEntity(selectedIDs[i]);
if (!entity || entity.geometry(graph) !== 'vertex') {
return []; // selection includes some not vertexes
return []; // selection includes some not vertices
}
var currParents = graph.parentWays(entity).map(function(w) { return w.id; });
@@ -176,19 +177,15 @@ export function modeSelect(context, selectedIDs) {
_operations = Object.values(Operations)
.map(function(o) { return o(context, selectedIDs); })
.filter(function(o) { return o.available() && o.id !== 'delete' && o.id !== 'downgrade' && o.id !== 'copy'; });
var copyOperation = Operations.operationCopy(context, selectedIDs);
if (copyOperation.available()) {
// group copy operation with delete/downgrade
_operations.push(copyOperation);
}
var downgradeOperation = Operations.operationDowngrade(context, selectedIDs);
// don't allow delete if downgrade is available
var lastOperation = !context.inIntro() && downgradeOperation.available() ? downgradeOperation : Operations.operationDelete(context, selectedIDs);
_operations.push(lastOperation);
.filter(function(o) { return o.id !== 'delete' && o.id !== 'downgrade' && o.id !== 'copy'; })
.concat([
// group copy/downgrade/delete operation together at the end of the list
Operations.operationCopy(context, selectedIDs),
Operations.operationDowngrade(context, selectedIDs),
Operations.operationDelete(context, selectedIDs)
]).filter(function(operation) {
return operation.available();
});
_operations.forEach(function(operation) {
if (operation.behavior) {
@@ -239,10 +236,14 @@ export function modeSelect(context, selectedIDs) {
.on(uiCmd('⇧↑'), nudgeSelection([0, -10]))
.on(uiCmd('⇧→'), nudgeSelection([10, 0]))
.on(uiCmd('⇧↓'), nudgeSelection([0, 10]))
.on(uiCmd('⇧←'), nudgeSelection([-100, 0]))
.on(uiCmd('⇧↑'), nudgeSelection([0, -100]))
.on(uiCmd('⇧→'), nudgeSelection([100, 0]))
.on(uiCmd('⇧↓'), nudgeSelection([0, 100]))
.on(uiCmd('⇧←'), nudgeSelection([-100, 0]))
.on(uiCmd('⇧↑'), nudgeSelection([0, -100]))
.on(uiCmd('⇧→'), nudgeSelection([100, 0]))
.on(uiCmd('⇧↓'), nudgeSelection([0, 100]))
.on(utilKeybinding.plusKeys.map((key) => uiCmd('⇧' + key)), scaleSelection(1.05))
.on(utilKeybinding.plusKeys.map((key) => uiCmd('⇧⌥' + key)), scaleSelection(Math.pow(1.05, 5)))
.on(utilKeybinding.minusKeys.map((key) => uiCmd('⇧' + key)), scaleSelection(1/1.05))
.on(utilKeybinding.minusKeys.map((key) => uiCmd('⇧⌥' + key)), scaleSelection(1/Math.pow(1.05, 5)))
.on(['\\', 'pause'], nextParent)
.on('⎋', esc, true);
@@ -300,9 +301,85 @@ export function modeSelect(context, selectedIDs) {
.duration(4000)
.iconName('#iD-operation-' + moveOp.id)
.iconClass('operation disabled')
.text(moveOp.tooltip)();
.label(moveOp.tooltip)();
} else {
context.perform(actionMove(selectedIDs, delta, context.projection), moveOp.annotation());
context.validator().validate();
}
};
}
function scaleSelection(factor) {
return function() {
// prevent scaling during low zoom selection
if (!context.map().withinEditableZoom()) return;
let nodes = utilGetAllNodes(selectedIDs, context.graph());
let isUp = factor > 1;
// can only scale if multiple nodes are selected
if (nodes.length <= 1) return;
let extent = utilTotalExtent(selectedIDs, context.graph());
// These disabled checks would normally be handled by an operation
// object, but we don't want an actual scale operation at this point.
function scalingDisabled() {
if (tooSmall()) {
return 'too_small';
} else if (extent.percentContainedIn(context.map().extent()) < 0.8) {
return 'too_large';
} else if (someMissing() || selectedIDs.some(incompleteRelation)) {
return 'not_downloaded';
} else if (selectedIDs.some(context.hasHiddenConnections)) {
return 'connected_to_hidden';
}
return false;
function tooSmall() {
if (isUp) return false;
let dLon = Math.abs(extent[1][0] - extent[0][0]);
let dLat = Math.abs(extent[1][1] - extent[0][1]);
return dLon < geoMetersToLon(1, extent[1][1]) &&
dLat < geoMetersToLat(1);
}
function someMissing() {
if (context.inIntro()) return false;
let osm = context.connection();
if (osm) {
let missing = nodes.filter(function(n) { return !osm.isDataLoaded(n.loc); });
if (missing.length) {
missing.forEach(function(loc) { context.loadTileAtLoc(loc); });
return true;
}
}
return false;
}
function incompleteRelation(id) {
let entity = context.entity(id);
return entity.type === 'relation' && !entity.isComplete(context.graph());
}
}
const disabled = scalingDisabled();
if (disabled) {
let multi = (selectedIDs.length === 1 ? 'single' : 'multiple');
context.ui().flash
.duration(4000)
.iconName('#iD-icon-no')
.iconClass('operation disabled')
.label(t('operations.scale.' + disabled + '.' + multi))();
} else {
const pivot = context.projection(extent.center());
const annotation = t('operations.scale.annotation.' + (isUp ? 'up' : 'down') + '.feature', { n: selectedIDs.length });
context.perform(actionScale(selectedIDs, pivot, factor, context.projection), annotation);
context.validator().validate();
}
};
}
+1 -1
View File
@@ -102,7 +102,7 @@ export function operationCircularize(context, selectedIDs) {
operation.annotation = function() {
return t('operations.circularize.annotation.' + _amount);
return t('operations.circularize.annotation.feature', { n: _actions.length });
};
+24 -18
View File
@@ -5,45 +5,51 @@ import { utilArrayGroupBy } from '../util';
export function operationContinue(context, selectedIDs) {
var graph = context.graph();
var entities = selectedIDs.map(function(id) { return graph.entity(id); });
var geometries = Object.assign(
var _entities = selectedIDs.map(function(id) { return context.graph().entity(id); });
var _geometries = Object.assign(
{ line: [], vertex: [] },
utilArrayGroupBy(entities, function(entity) { return entity.geometry(graph); })
utilArrayGroupBy(_entities, function(entity) { return entity.geometry(context.graph()); })
);
var vertex = geometries.vertex[0];
var _vertex = _geometries.vertex.length && _geometries.vertex[0];
function candidateWays() {
return graph.parentWays(vertex).filter(function(parent) {
return parent.geometry(graph) === 'line' &&
return _vertex ? context.graph().parentWays(_vertex).filter(function(parent) {
return parent.geometry(context.graph()) === 'line' &&
!parent.isClosed() &&
parent.affix(vertex.id) &&
(geometries.line.length === 0 || geometries.line[0] === parent);
});
parent.affix(_vertex.id) &&
(_geometries.line.length === 0 || _geometries.line[0] === parent);
}) : [];
}
var _candidates = candidateWays();
var operation = function() {
var candidate = candidateWays()[0];
var candidate = _candidates[0];
context.enter(
modeDrawLine(context, candidate.id, context.graph(), 'line', candidate.affix(vertex.id), true)
modeDrawLine(context, candidate.id, context.graph(), 'line', candidate.affix(_vertex.id), true)
);
};
operation.relatedEntityIds = function() {
return _candidates.length ? [_candidates[0].id] : [];
};
operation.available = function() {
return geometries.vertex.length === 1 &&
geometries.line.length <= 1 &&
!context.features().hasHiddenConnections(vertex, context.graph());
return _geometries.vertex.length === 1 &&
_geometries.line.length <= 1 &&
!context.features().hasHiddenConnections(_vertex, context.graph());
};
operation.disabled = function() {
var candidates = candidateWays();
if (candidates.length === 0) {
if (_candidates.length === 0) {
return 'not_eligible';
} else if (candidates.length > 1) {
} else if (_candidates.length > 1) {
return 'multiple';
}
+10 -18
View File
@@ -1,5 +1,3 @@
import { event as d3_event } from 'd3-selection';
import { t } from '../core/localizer';
import { behaviorOperation } from '../behavior/operation';
import { uiCmd } from '../ui/cmd';
@@ -7,8 +5,6 @@ import { utilArrayGroupBy, utilTotalExtent } from '../util';
export function operationCopy(context, selectedIDs) {
var _multi = selectedIDs.length === 1 ? 'single' : 'multiple';
function getFilteredIdsToCopy() {
return selectedIDs.filter(function(selectedID) {
var entity = context.graph().hasEntity(selectedID);
@@ -19,10 +15,6 @@ export function operationCopy(context, selectedIDs) {
var operation = function() {
if (!getSelectionText()) {
d3_event.preventDefault();
}
var graph = context.graph();
var selected = groupEntities(getFilteredIdsToCopy(), graph);
var canCopy = [];
@@ -97,11 +89,6 @@ export function operationCopy(context, selectedIDs) {
}
function getSelectionText() {
return window.getSelection().toString();
}
operation.available = function() {
return getFilteredIdsToCopy().length > 0;
};
@@ -116,18 +103,23 @@ export function operationCopy(context, selectedIDs) {
};
operation.availableForKeypress = function() {
var selection = window.getSelection && window.getSelection();
// if the user has text selected then let them copy that, not the selected feature
return !selection || !selection.toString();
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.copy.' + disable + '.' + _multi) :
t('operations.copy.description' + '.' + _multi);
t('operations.copy.' + disable, { n: selectedIDs.length }) :
t('operations.copy.description', { n: selectedIDs.length });
};
operation.annotation = function() {
return selectedIDs.length === 1 ?
t('operations.copy.annotation.single') :
t('operations.copy.annotation.multiple', { n: selectedIDs.length.toString() });
return t('operations.copy.annotation', { n: selectedIDs.length });
};
+1 -1
View File
@@ -139,7 +139,7 @@ export function operationDelete(context, selectedIDs) {
operation.annotation = function() {
return selectedIDs.length === 1 ?
t('operations.delete.annotation.' + context.graph().geometry(selectedIDs[0])) :
t('operations.delete.annotation.multiple', { n: selectedIDs.length });
t('operations.delete.annotation.feature', { n: selectedIDs.length });
};
+30 -6
View File
@@ -1,6 +1,7 @@
import { t } from '../core/localizer';
import { actionDisconnect } from '../actions/disconnect';
import { behaviorOperation } from '../behavior/operation';
import { utilArrayUniq } from '../util/array';
import { utilGetAllNodes, utilTotalExtent } from '../util/util';
@@ -21,13 +22,16 @@ export function operationDisconnect(context, selectedIDs) {
}
});
var _extent, _nodes, _coords, _descriptionID = '', _annotationID = 'features';
var _coords, _descriptionID = '', _annotationID = 'features';
var _disconnectingVertexIds = [];
var _disconnectingWayIds = [];
if (_vertexIDs.length > 0) {
// At the selected vertices, disconnect the selected ways, if any, else
// disconnect all connected ways
_extent = utilTotalExtent(_vertexIDs, context.graph());
_disconnectingVertexIds = _vertexIDs;
_vertexIDs.forEach(function(vertexID) {
var action = actionDisconnect(vertexID);
@@ -40,6 +44,11 @@ export function operationDisconnect(context, selectedIDs) {
action.limitWays(waysIDsForVertex);
}
_actions.push(action);
_disconnectingWayIds = _disconnectingWayIds
.concat(context.graph().parentWays(context.graph().entity(vertexID)).map(d => d.id));
});
_disconnectingWayIds = utilArrayUniq(_disconnectingWayIds).filter(function(id) {
return _wayIDs.indexOf(id) === -1;
});
_descriptionID += _actions.length === 1 ? 'single_point.' : 'multiple_points.';
@@ -56,16 +65,17 @@ export function operationDisconnect(context, selectedIDs) {
var ways = _wayIDs.map(function(id) {
return context.entity(id);
});
_nodes = utilGetAllNodes(_wayIDs, context.graph());
_coords = _nodes.map(function(n) { return n.loc; });
_extent = utilTotalExtent(ways, context.graph());
var nodes = utilGetAllNodes(_wayIDs, context.graph());
_coords = nodes.map(function(n) { return n.loc; });
// actions for connected nodes shared by at least two selected ways
var sharedActions = [];
var sharedNodes = [];
// actions for connected nodes
var unsharedActions = [];
var unsharedNodes = [];
_nodes.forEach(function(node) {
nodes.forEach(function(node) {
var action = actionDisconnect(node.id).limitWays(_wayIDs);
if (action.disabled(context.graph()) !== 'not_connected') {
@@ -80,8 +90,10 @@ export function operationDisconnect(context, selectedIDs) {
if (count > 1) {
sharedActions.push(action);
sharedNodes.push(node);
} else {
unsharedActions.push(action);
unsharedNodes.push(node);
}
}
});
@@ -92,11 +104,13 @@ export function operationDisconnect(context, selectedIDs) {
if (sharedActions.length) {
// if any nodes are shared, only disconnect the selected ways from each other
_actions = sharedActions;
_disconnectingVertexIds = sharedNodes.map(node => node.id);
_descriptionID += 'conjoined';
_annotationID = 'from_each_other';
} else {
// if no nodes are shared, disconnect the selected ways from all connected ways
_actions = unsharedActions;
_disconnectingVertexIds = unsharedNodes.map(node => node.id);
if (_wayIDs.length === 1) {
_descriptionID += context.graph().geometry(_wayIDs[0]);
} else {
@@ -105,6 +119,8 @@ export function operationDisconnect(context, selectedIDs) {
}
}
var _extent = utilTotalExtent(_disconnectingVertexIds, context.graph());
var operation = function() {
context.perform(function(graph) {
@@ -115,6 +131,14 @@ export function operationDisconnect(context, selectedIDs) {
};
operation.relatedEntityIds = function() {
if (_vertexIDs.length) {
return _disconnectingWayIds;
}
return _disconnectingVertexIds;
};
operation.available = function() {
if (_actions.length === 0) return false;
if (_otherIDs.length !== 0) return false;
+29 -21
View File
@@ -6,26 +6,31 @@ import { uiCmd } from '../ui/cmd';
import { presetManager } from '../presets';
export function operationDowngrade(context, selectedIDs) {
var affectedFeatureCount = 0;
var downgradeType;
var _affectedFeatureCount = 0;
var _downgradeType = downgradeTypeForEntityIDs(selectedIDs);
setDowngradeTypeForEntityIDs();
var _multi = _affectedFeatureCount === 1 ? 'single' : 'multiple';
var multi = affectedFeatureCount === 1 ? 'single' : 'multiple';
function setDowngradeTypeForEntityIDs() {
for (var i in selectedIDs) {
var entityID = selectedIDs[i];
function downgradeTypeForEntityIDs(entityIds) {
var downgradeType;
_affectedFeatureCount = 0;
for (var i in entityIds) {
var entityID = entityIds[i];
var type = downgradeTypeForEntityID(entityID);
if (type) {
affectedFeatureCount += 1;
_affectedFeatureCount += 1;
if (downgradeType && type !== downgradeType) {
downgradeType = 'building_address';
if (downgradeType !== 'generic' && type !== 'generic') {
downgradeType = 'building_address';
} else {
downgradeType = 'generic';
}
} else {
downgradeType = type;
}
}
}
return downgradeType;
}
function downgradeTypeForEntityID(entityID) {
@@ -43,12 +48,16 @@ export function operationDowngrade(context, selectedIDs) {
return 'address';
}
if (entity.geometry(graph) === 'area' &&
var geometry = entity.geometry(graph);
if (geometry === 'area' &&
entity.tags.building &&
!preset.tags.building) {
return 'building';
}
if (geometry === 'vertex' && Object.keys(entity.tags).length) {
return 'generic';
}
return null;
}
@@ -72,8 +81,7 @@ export function operationDowngrade(context, selectedIDs) {
key.match(/^building:.{1,}/) ||
key.match(/^roof:.{1,}/)) continue;
}
// keep address tags for buildings too
if (key.match(/^addr:.{1,}/)) continue;
if (type !== 'generic' && key.match(/^addr:.{1,}/)) continue;
delete tags[key];
}
@@ -90,7 +98,7 @@ export function operationDowngrade(context, selectedIDs) {
operation.available = function () {
return downgradeType;
return _downgradeType;
};
@@ -110,24 +118,24 @@ export function operationDowngrade(context, selectedIDs) {
operation.tooltip = function () {
var disable = operation.disabled();
return disable ?
t('operations.downgrade.' + disable + '.' + multi) :
t('operations.downgrade.description.' + downgradeType);
t('operations.downgrade.' + disable + '.' + _multi) :
t('operations.downgrade.description.' + _downgradeType);
};
operation.annotation = function () {
var suffix;
if (downgradeType === 'building_address') {
suffix = 'multiple';
if (_downgradeType === 'building_address') {
suffix = 'generic';
} else {
suffix = downgradeType + '.' + multi;
suffix = _downgradeType;
}
return t('operations.downgrade.annotation.' + suffix, { n: affectedFeatureCount});
return t('operations.downgrade.annotation.' + suffix, { n: _affectedFeatureCount});
};
operation.id = 'downgrade';
operation.keys = [uiCmd('⌘⌫'), uiCmd('⌘⌦'), uiCmd('⌦')];
operation.keys = [uiCmd('')];
operation.title = t('operations.downgrade.title');
operation.behavior = behaviorOperation(context).which(operation);
+1 -1
View File
@@ -79,7 +79,7 @@ export function operationExtract(context, selectedIDs) {
operation.annotation = function () {
return t('operations.extract.annotation.' + _amount, { n: selectedIDs.length });
return t('operations.extract.annotation', { n: selectedIDs.length });
};
+1 -1
View File
@@ -67,7 +67,7 @@ export function operationMove(context, selectedIDs) {
operation.annotation = function() {
return selectedIDs.length === 1 ?
t('operations.move.annotation.' + context.graph().geometry(selectedIDs[0])) :
t('operations.move.annotation.multiple');
t('operations.move.annotation.feature', { n: selectedIDs.length });
};
+1 -1
View File
@@ -125,7 +125,7 @@ export function operationOrthogonalize(context, selectedIDs) {
operation.annotation = function() {
return t('operations.orthogonalize.annotation.' + _type + '.' + _amount);
return t('operations.orthogonalize.annotation.' + _type, { n: _actions.length });
};
+2 -6
View File
@@ -78,16 +78,12 @@ export function operationPaste(context) {
if (!ids.length) {
return t('operations.paste.nothing_copied');
}
return ids.length === 1 ?
t('operations.paste.description.single', { feature: utilDisplayLabel(oldGraph.entity(ids[0]), oldGraph) }) :
t('operations.paste.description.multiple', { n: ids.length.toString() });
return t('operations.paste.description', { feature: utilDisplayLabel(oldGraph.entity(ids[0]), oldGraph), n: ids.length });
};
operation.annotation = function() {
var ids = context.copyIDs();
return ids.length === 1 ?
t('operations.paste.annotation.single') :
t('operations.paste.annotation.multiple', { n: ids.length.toString() });
return t('operations.paste.annotation', { n: ids.length });
};
operation.id = 'paste';
+1 -1
View File
@@ -83,7 +83,7 @@ export function operationReflect(context, selectedIDs, axis) {
operation.annotation = function() {
return t('operations.reflect.annotation.' + axis + '.' + multi);
return t('operations.reflect.annotation.' + axis + '.feature', { n: selectedIDs.length });
};
+5 -4
View File
@@ -41,9 +41,9 @@ export function operationReverse(context, selectedIDs) {
var entity = context.hasEntity(act.entityID());
return entity && entity.type === 'node';
}).length;
var typeID = nodeActionCount === 0 ? 'line' : (nodeActionCount === acts.length ? 'point' : 'features');
if (typeID !== 'features' && acts.length > 1) typeID += 's';
return typeID;
if (nodeActionCount === 0) return 'line';
if (nodeActionCount === acts.length) return 'point';
return 'feature';
}
@@ -63,7 +63,8 @@ export function operationReverse(context, selectedIDs) {
operation.annotation = function() {
return t('operations.reverse.annotation.' + reverseTypeID());
var acts = actions();
return t('operations.reverse.annotation.' + reverseTypeID(), { n: acts.length });
};
+1 -1
View File
@@ -67,7 +67,7 @@ export function operationRotate(context, selectedIDs) {
operation.annotation = function() {
return selectedIDs.length === 1 ?
t('operations.rotate.annotation.' + context.graph().geometry(selectedIDs[0])) :
t('operations.rotate.annotation.multiple');
t('operations.rotate.annotation.feature', { n: selectedIDs.length });
};
+41 -25
View File
@@ -5,60 +5,76 @@ import { modeSelect } from '../modes/select';
export function operationSplit(context, selectedIDs) {
var vertices = selectedIDs
.filter(function(id) { return context.graph().geometry(id) === 'vertex'; });
var entityID = vertices[0];
var action = actionSplit(entityID);
var ways = [];
var _vertexIds = selectedIDs.filter(function(id) {
return context.graph().geometry(id) === 'vertex';
});
var _selectedWayIds = selectedIDs.filter(function(id) {
var entity = context.graph().hasEntity(id);
return entity && entity.type === 'way';
});
var _isAvailable = _vertexIds.length > 0 &&
_vertexIds.length + _selectedWayIds.length === selectedIDs.length;
var _action = actionSplit(_vertexIds);
var _ways = [];
var _geometry = 'feature';
var _waysAmount = 'single';
var _nodesAmount = _vertexIds.length === 1 ? 'single' : 'multiple';
if (vertices.length === 1) {
if (entityID && selectedIDs.length > 1) {
var ids = selectedIDs.filter(function(id) { return id !== entityID; });
action.limitWays(ids);
if (_isAvailable) {
if (_selectedWayIds.length) _action.limitWays(_selectedWayIds);
_ways = _action.ways(context.graph());
var geometries = {};
_ways.forEach(function(way) {
geometries[way.geometry(context.graph())] = true;
});
if (Object.keys(geometries).length === 1) {
_geometry = Object.keys(geometries)[0];
}
ways = action.ways(context.graph());
_waysAmount = _ways.length === 1 ? 'single' : 'multiple';
}
var operation = function() {
var difference = context.perform(action, operation.annotation());
context.enter(modeSelect(context, difference.extantIDs()));
var difference = context.perform(_action, operation.annotation());
// select both the nodes and the ways so the mapper can immediately disconnect them if desired
var idsToSelect = _vertexIds.concat(difference.extantIDs().filter(function(id) {
// filter out relations that may have had member additions
return context.entity(id).type === 'way';
}));
context.enter(modeSelect(context, idsToSelect));
};
operation.relatedEntityIds = function() {
return _selectedWayIds.length ? [] : _ways.map(way => way.id);
};
operation.available = function() {
return vertices.length === 1;
return _isAvailable;
};
operation.disabled = function() {
var reason = action.disabled(context.graph());
var reason = _action.disabled(context.graph());
if (reason) {
return reason;
} else if (selectedIDs.some(context.hasHiddenConnections)) {
return 'connected_to_hidden';
}
return false;
};
operation.tooltip = function() {
var disable = operation.disabled();
if (disable) {
return t('operations.split.' + disable);
} else if (ways.length === 1) {
return t('operations.split.description.' + context.graph().geometry(ways[0].id));
} else {
return t('operations.split.description.multiple');
}
if (disable) return t('operations.split.' + disable);
return t('operations.split.description.' + _geometry + '.' + _waysAmount + '.' + _nodesAmount + '_node');
};
operation.annotation = function() {
return ways.length === 1 ?
t('operations.split.annotation.' + context.graph().geometry(ways[0].id)) :
t('operations.split.annotation.multiple', { n: ways.length });
return t('operations.split.annotation.' + _geometry, { n: _ways.length });
};
+4 -4
View File
@@ -20,7 +20,7 @@ export function operationStraighten(context, selectedIDs) {
function chooseAction() {
// straighten selected nodes
if (_wayIDs.length === 0 && _nodeIDs.length > 2) {
_geometry = 'points';
_geometry = 'point';
return actionStraightenNodes(_nodeIDs, context.projection);
// straighten selected ways (possibly between range of 2 selected nodes)
@@ -67,7 +67,7 @@ export function operationStraighten(context, selectedIDs) {
_extent = utilTotalExtent(_nodeIDs, context.graph());
}
_geometry = _wayIDs.length === 1 ? 'line' : 'lines';
_geometry = 'line';
return actionStraightenWay(selectedIDs, context.projection);
}
@@ -125,12 +125,12 @@ export function operationStraighten(context, selectedIDs) {
var disable = operation.disabled();
return disable ?
t('operations.straighten.' + disable + '.' + _amount) :
t('operations.straighten.description.' + _geometry);
t('operations.straighten.description.' + _geometry + (_wayIDs.length === 1 ? '' : 's'));
};
operation.annotation = function() {
return t('operations.straighten.annotation.' + _geometry);
return t('operations.straighten.annotation.' + _geometry, { n: _wayIDs.length ? _wayIDs.length : _nodeIDs.length });
};
+1
View File
@@ -201,6 +201,7 @@ osmEntity.prototype = {
var matchesDeprecatedTags = oldKeys.every(function(oldKey) {
if (!tags[oldKey]) return false;
if (d.old[oldKey] === '*') return true;
if (d.old[oldKey] === tags[oldKey]) return true;
var vals = tags[oldKey].split(';').filter(Boolean);
if (vals.length === 0) {
+2 -2
View File
@@ -164,10 +164,10 @@ export function osmIntersection(graph, startVertexId, maxDistance) {
// actions can be replayed on the main graph exactly in the same order.
// (It is unintuitive, but the order of ways returned from graph.parentWays()
// is arbitrary, depending on how the main graph and vgraph were built)
var splitAll = actionSplit(v.id);
var splitAll = actionSplit([v.id]).keepHistoryOn('first');
if (!splitAll.disabled(vgraph)) {
splitAll.ways(vgraph).forEach(function(way) {
var splitOne = actionSplit(v.id).limitWays([way.id]);
var splitOne = actionSplit([v.id]).limitWays([way.id]).keepHistoryOn('first');
actions.push(splitOne);
vgraph = splitOne(vgraph);
});
+1 -1
View File
@@ -20,7 +20,7 @@ export class QAItem {
}
update(props) {
// You can't override this inital information
// You can't override this initial information
const { loc, service, itemType, id } = this;
Object.keys(props).forEach(prop => this[prop] = props[prop]);
+1
View File
@@ -34,6 +34,7 @@ export function presetCategory(categoryID, category, all) {
_this.matchScore = () => -1;
_this.name = () => t(`presets.categories.${categoryID}.name`, { 'default': categoryID });
_this.nameLabel = () => t.html(`presets.categories.${categoryID}.name`, { 'default': categoryID });
_this.terms = () => [];
+3 -1
View File
@@ -21,8 +21,10 @@ export function presetField(fieldID, field) {
};
_this.t = (scope, options) => t(`presets.fields.${fieldID}.${scope}`, options);
_this.t.html = (scope, options) => t.html(`presets.fields.${fieldID}.${scope}`, options);
_this.label = () => _this.overrideLabel || _this.t('label', { 'default': fieldID });
_this.title = () => _this.overrideLabel || _this.t('label', { 'default': fieldID });
_this.label = () => _this.overrideLabel || _this.t.html('label', { 'default': fieldID });
const _placeholder = _this.placeholder;
_this.placeholder = () => _this.t('placeholder', { 'default': _placeholder });
+30 -20
View File
@@ -79,24 +79,43 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
};
let _textCache = {};
_this.t = (scope, options) => {
const textID = `presets.presets.${presetID}.${scope}`;
if (_textCache[textID]) return _textCache[textID];
return _textCache[textID] = t(textID, options);
return t(textID, options);
};
_this.t.html = (scope, options) => {
const textID = `presets.presets.${presetID}.${scope}`;
return t.html(textID, options);
};
_this.name = () => {
if (_this.suggestion) {
let path = presetID.split('/');
path.pop(); // remove brand name
// NOTE: insert an en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc)
return _this.originalName + ' ' + t('presets.presets.' + path.join('/') + '.name');
}
return _this.t('name', { 'default': _this.originalName });
};
_this.nameLabel = () => {
return _this.t.html('name', { 'default': _this.originalName });
};
_this.subtitle = () => {
if (_this.suggestion) {
let path = presetID.split('/');
path.pop(); // remove brand name
return t('presets.presets.' + path.join('/') + '.name');
}
return null;
};
_this.subtitleLabel = () => {
if (_this.suggestion) {
let path = presetID.split('/');
path.pop(); // remove brand name
return t.html('presets.presets.' + path.join('/') + '.name');
}
return null;
};
_this.terms = () => _this.t('terms', { 'default': _this.originalTerms })
.toLowerCase().trim().split(/\s*,+\s*/);
@@ -115,7 +134,7 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
};
_this.reference = (geom) => {
_this.reference = () => {
// Lookup documentation on Wikidata...
const qid = _this.tags.wikidata || _this.tags['brand:wikidata'] || _this.tags['operator:wikidata'];
if (qid) {
@@ -126,15 +145,6 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
let key = _this.originalReference.key || Object.keys(utilObjectOmit(_this.tags, 'name'))[0];
let value = _this.originalReference.value || _this.tags[key];
if (geom === 'relation' && key === 'type') {
if (value in _this.tags) {
key = value;
value = _this.tags[key];
} else {
return { rtype: value };
}
}
if (value === '*') {
return { key: key };
} else {
@@ -254,7 +264,7 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) {
function shouldInherit(f) {
if (f.key && _this.tags[f.key] !== undefined &&
// inherit anyway if multiple values are allowed or just a checkbox
f.type !== 'multiCombo' && f.type !== 'semiCombo' && f.type !== 'check'
f.type !== 'multiCombo' && f.type !== 'semiCombo' && f.type !== 'manyCombo' && f.type !== 'check'
) return false;
return true;
+23 -13
View File
@@ -271,6 +271,7 @@ export function rendererBackground(context) {
context.history().photoOverlaysUsed(photoOverlaysUsed);
};
let _checkedBlocklists;
background.sources = (extent, zoom, includeCurrent) => {
if (!_imageryIndex) return []; // called before init()?
@@ -281,9 +282,22 @@ export function rendererBackground(context) {
const currSource = baseLayer.source();
const osm = context.connection();
const blocklists = osm && osm.imageryBlocklists();
if (blocklists && blocklists !== _checkedBlocklists) {
_imageryIndex.backgrounds.forEach(source => {
source.isBlocked = blocklists.some(function(blocklist) {
return blocklist.test(source.template());
});
});
_checkedBlocklists = blocklists;
}
return _imageryIndex.backgrounds.filter(source => {
if (includeCurrent && currSource === source) return true; // optionally always include the current imagery
if (source.isBlocked) return false; // even bundled sources may be blocked - #7905
if (!source.polygon) return true; // always include imagery with worldwide coverage
if (includeCurrent && currSource === source) return true; // optionally include the current imagery
if (zoom && zoom < 6) return false; // optionally exclude local imagery at low zooms
return visible[source.id]; // include imagery visible in given extent
});
@@ -300,30 +314,26 @@ export function rendererBackground(context) {
background.baseLayerSource = function(d) {
if (!arguments.length) return baseLayer.source();
// test source against OSM imagery blacklists..
// test source against OSM imagery blocklists..
const osm = context.connection();
if (!osm) return background;
const blacklists = osm.imageryBlacklists();
const blocklists = osm.imageryBlocklists();
const template = d.template();
let fail = false;
let tested = 0;
let regex;
for (let i = 0; i < blacklists.length; i++) {
try {
regex = new RegExp(blacklists[i]);
fail = regex.test(template);
tested++;
if (fail) break;
} catch (e) {
/* noop */
}
for (let i = 0; i < blocklists.length; i++) {
regex = blocklists[i];
fail = regex.test(template);
tested++;
if (fail) break;
}
// ensure at least one test was run.
if (!tested) {
regex = new RegExp('.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*');
regex = /.*\.google(apis)?\..*\/(vt|kh)[\?\/].*([xyz]=.*){3}.*/;
fail = regex.test(template);
}
+17 -2
View File
@@ -72,9 +72,15 @@ export function rendererBackgroundSource(data) {
};
source.label = function() {
var id_safe = source.id.replace(/\./g, '<TX_DOT>');
return t.html('imagery.' + id_safe + '.name', { default: _name });
};
source.description = function() {
var id_safe = source.id.replace(/\./g, '<TX_DOT>');
return t('imagery.' + id_safe + '.description', { default: _description });
return t.html('imagery.' + id_safe + '.description', { default: _description });
};
@@ -91,7 +97,7 @@ export function rendererBackgroundSource(data) {
source.imageryUsed = function() {
return name || source.id;
return _name || source.id;
};
@@ -538,6 +544,11 @@ rendererBackgroundSource.None = function() {
};
source.label = function() {
return t.html('background.none');
};
source.imageryUsed = function() {
return null;
};
@@ -560,6 +571,10 @@ rendererBackgroundSource.Custom = function(template) {
return t('background.custom');
};
source.label = function() {
return t.html('background.custom');
};
source.imageryUsed = function() {
// sanitize personal connection tokens - #6801
+1 -1
View File
@@ -500,7 +500,7 @@ export function rendererMap(context) {
);
// On Firefox Windows and Linux we always get +/- the scroll line amount (default 3)
// There doesn't seem to be any scroll accelleration.
// There doesn't seem to be any scroll acceleration.
// This multiplier increases the speed a little bit - #5512
if (detected.os !== 'mac') {
dY *= 5;
+43 -2
View File
@@ -1,5 +1,6 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { services } from '../services';
import { utilRebind } from '../util/rebind';
import { utilQsString, utilStringQs } from '../util';
@@ -136,10 +137,12 @@ export function rendererPhotos(context) {
photos.init = function() {
var hash = utilStringQs(window.location.hash);
if (hash.photo_overlay) {
// support enabling photo layers by default via a URL parameter, e.g. `photo_overlay=openstreetcam;mapillary;streetside`
var hashOverlayIDs = hash.photo_overlay.replace(/;/g, ',').split(',');
hashOverlayIDs.forEach(function(id) {
var layer = context.layers().layer(id);
if (layer) layer.enabled(true);
var layer = _layerIDs.indexOf(id) !== -1 && context.layers().layer(id);
if (layer && !layer.enabled()) layer.enabled(true);
});
}
if (hash.fromDate) {
@@ -151,6 +154,44 @@ export function rendererPhotos(context) {
if (hash.username) {
this.setUsernameFilter(hash.username, false);
}
if (hash.photo) {
// support opening a photo via a URL parameter, e.g. `photo=mapillary-fztgSDtLpa08ohPZFZjeRQ`
var photoIds = hash.photo.replace(/;/g, ',').split(',');
var photoId = photoIds.length && photoIds[0].trim();
var results = /(.*)\/(.*)/g.exec(photoId);
if (results && results.length >= 3) {
var serviceId = results[1];
var photoKey = results[2];
var service = services[serviceId];
if (service && service.ensureViewerLoaded) {
// if we're showing a photo then make sure its layer is enabled too
var layer = _layerIDs.indexOf(serviceId) !== -1 && context.layers().layer(serviceId);
if (layer && !layer.enabled()) layer.enabled(true);
var baselineTime = Date.now();
service.on('loadedImages.rendererPhotos', function() {
// don't open the viewer if too much time has elapsed
if (Date.now() - baselineTime > 45000) {
service.on('loadedImages.rendererPhotos', null);
return;
}
if (!service.cachedImage(photoKey)) return;
service.on('loadedImages.rendererPhotos', null);
service.ensureViewerLoaded(context)
.then(function() {
service
.selectImage(context, photoKey)
.showViewer(context);
});
});
}
}
}
context.layers().on('change.rendererPhotos', updateStorage);
};
+2 -2
View File
@@ -238,7 +238,7 @@ export function rendererTileLayer(context) {
debug
.selectAll('.tile-label-debug-coord')
.text(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; });
.html(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; });
debug
.selectAll('.tile-label-debug-vintage')
@@ -246,7 +246,7 @@ export function rendererTileLayer(context) {
var span = d3_select(this);
var center = context.projection.invert(tileCenter(d));
_source.getMetadata(center, d, function(err, result) {
span.text((result && result.vintage && result.vintage.range) ||
span.html((result && result.vintage && result.vintage.range) ||
t('info_panels.background.vintage') + ': ' + t('info_panels.background.unknown')
);
});
+1 -1
View File
@@ -104,7 +104,7 @@ function cardinalDirection(bearing) {
return t(`QA.improveOSM.directions.${compass[dir]}`);
}
// Errors shouldn't obscure eachother
// Errors shouldn't obscure each other
function preventCoincident(loc, bumpUp) {
let coincident = false;
do {
+270 -151
View File
@@ -5,8 +5,7 @@ import { select as d3_select } from 'd3-selection';
import RBush from 'rbush';
import { geoExtent, geoScaleToZoom } from '../geo';
import { svgDefs } from '../svg/defs';
import { utilArrayUnion, utilQsString, utilRebind, utilTiler } from '../util';
import { utilArrayUnion, utilQsString, utilRebind, utilTiler, utilStringQs } from '../util';
var apibase = 'https://a.mapillary.com/v3/';
@@ -40,30 +39,23 @@ var mapFeatureConfig = {
var maxResults = 1000;
var tileZoom = 14;
var tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
var dispatch = d3_dispatch('loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged');
var dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'nodeChanged');
var _mlyFallback = false;
var _mlyCache;
var _mlyClicks;
var _mlyActiveImage;
var _mlySelectedImageKey;
var _mlyViewer;
var _mlyViewerFilter = ['all'];
var _loadViewerPromise;
var _mlyHighlightedDetection;
var _mlyShowFeatureDetections = false;
var _mlyShowSignDetections = false;
function abortRequest(controller) {
controller.abort();
}
function maxPageAtZoom(z) {
if (z < 15) return 2;
if (z === 15) return 5;
if (z === 16) return 10;
if (z === 17) return 20;
if (z === 18) return 40;
if (z > 18) return 80;
}
function loadTiles(which, url, projection) {
var currZoom = Math.floor(geoScaleToZoom(projection.scale()));
var tiles = tiler.getTiles(projection);
@@ -162,26 +154,6 @@ function loadNextTilePage(which, currZoom, url, tile) {
});
return false; // because no `d` data worth loading into an rbush
// An image detection is a semantic pixel area on an image. The area could indicate
// sky, trees, sidewalk in the image. A detection can be a polygon, a bounding box, or a point.
// Each image_detection feature is a GeoJSON Point (located where the image was taken)
} else if (which === 'image_detections') {
d = {
key: feature.properties.key,
image_key: feature.properties.image_key,
value: feature.properties.value,
package: feature.properties.package,
shape: feature.properties.shape
};
// cache imageKey -> image_detections
if (!cache.forImageKey[d.image_key]) {
cache.forImageKey[d.image_key] = [];
}
cache.forImageKey[d.image_key].push(d);
return false; // because no `d` data worth loading into an rbush
// A map feature is a real world object that can be shown on a map. It could be any object
// recognized from images, manually added in images, or added on the map.
// Each map feature is a GeoJSON Point (located where the feature is)
@@ -226,6 +198,57 @@ function loadNextTilePage(which, currZoom, url, tile) {
});
}
function loadData(which, url) {
var cache = _mlyCache[which];
var options = {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};
var nextUrl = url + '&client_id=' + clientId;
return fetch(nextUrl, options)
.then(function(response) {
if (!response.ok) {
throw new Error(response.status + ' ' + response.statusText);
}
return response.json();
})
.then(function(data) {
if (!data || !data.features || !data.features.length) {
throw new Error('No Data');
}
data.features.forEach(function(feature) {
var d;
if (which === 'image_detections') {
d = {
key: feature.properties.key,
image_key: feature.properties.image_key,
value: feature.properties.value,
package: feature.properties.package,
shape: feature.properties.shape
};
if (!cache.forImageKey[d.image_key]) {
cache.forImageKey[d.image_key] = [];
}
cache.forImageKey[d.image_key].push(d);
}
});
});
}
function maxPageAtZoom(z) {
if (z < 15) return 2;
if (z === 15) return 5;
if (z === 16) return 10;
if (z === 17) return 20;
if (z === 18) return 40;
if (z > 18) return 80;
}
// extract links to pages of API results
function parsePagination(links) {
return links.split(',').map(function(rel) {
@@ -271,7 +294,6 @@ function searchLimited(limit, projection, rtree) {
}
export default {
init: function() {
@@ -300,6 +322,7 @@ export default {
};
_mlySelectedImageKey = null;
_mlyActiveImage = null;
_mlyClicks = [];
},
@@ -362,22 +385,18 @@ export default {
loadSigns: function(projection) {
// if we are looking at signs, we'll actually need to fetch images too
loadTiles('images', apibase + 'images?sort_by=key&', projection);
loadTiles('map_features', apibase + 'map_features?layers=trafficsigns&min_nbr_image_detections=2&sort_by=key&', projection);
loadTiles('image_detections', apibase + 'image_detections?layers=trafficsigns&sort_by=key&', projection);
},
loadMapFeatures: function(projection) {
// if we are looking at signs, we'll actually need to fetch images too
loadTiles('images', apibase + 'images?sort_by=key', projection);
loadTiles('points', apibase + 'map_features?layers=points&min_nbr_image_detections=2&sort_by=key&values=' + mapFeatureConfig.values + '&', projection);
loadTiles('image_detections', apibase + 'image_detections?layers=points&sort_by=key&values=' + mapFeatureConfig.values + '&', projection);
},
loadViewer: function(context) {
ensureViewerLoaded: function(context) {
if (_loadViewerPromise) return _loadViewerPromise;
// add mly-wrapper
var wrap = context.container().select('.photoviewer')
.selectAll('.mly-wrapper')
@@ -389,33 +408,83 @@ export default {
.attr('class', 'photo-wrapper mly-wrapper')
.classed('hide', true);
// load mapillary-viewercss
d3_select('head').selectAll('#ideditor-mapillary-viewercss')
.data([0])
.enter()
.append('link')
.attr('id', 'ideditor-mapillary-viewercss')
.attr('rel', 'stylesheet')
.attr('href', context.asset(viewercss));
var that = this;
// load mapillary-viewerjs
d3_select('head').selectAll('#ideditor-mapillary-viewerjs')
.data([0])
.enter()
.append('script')
.attr('id', 'ideditor-mapillary-viewerjs')
.attr('src', context.asset(viewerjs));
_loadViewerPromise = new Promise((resolve, reject) => {
// load mapillary signs sprite
var defs = context.container().select('defs');
defs.call(svgDefs(context).addSprites, ['mapillary-sprite', 'mapillary-object-sprite'], false /* don't override colors */ );
// Register viewer resize handler
context.ui().photoviewer.on('resize.mapillary', function() {
if (_mlyViewer) {
_mlyViewer.resize();
var loadedCount = 0;
function loaded() {
loadedCount += 1;
// wait until both files are loaded
if (loadedCount === 2) resolve();
}
var head = d3_select('head');
// load mapillary-viewercss
head.selectAll('#ideditor-mapillary-viewercss')
.data([0])
.enter()
.append('link')
.attr('id', 'ideditor-mapillary-viewercss')
.attr('rel', 'stylesheet')
.attr('crossorigin', 'anonymous')
.attr('href', context.asset(viewercss))
.on('load.serviceMapillary', loaded)
.on('error.serviceMapillary', reject);
// load mapillary-viewerjs
head.selectAll('#ideditor-mapillary-viewerjs')
.data([0])
.enter()
.append('script')
.attr('id', 'ideditor-mapillary-viewerjs')
.attr('crossorigin', 'anonymous')
.attr('src', context.asset(viewerjs))
.on('load.serviceMapillary', loaded)
.on('error.serviceMapillary', reject);
})
.catch(function() {
_loadViewerPromise = null;
})
.then(function() {
that.initViewer(context);
});
return _loadViewerPromise;
},
loadSignResources: function(context) {
context.ui().svgDefs.addSprites(['mapillary-sprite'], false /* don't override colors */ );
return this;
},
loadObjectResources: function(context) {
context.ui().svgDefs.addSprites(['mapillary-object-sprite'], false /* don't override colors */ );
return this;
},
resetTags: function() {
if (_mlyViewer && !_mlyFallback) {
_mlyViewer.getComponent('tag').removeAll(); // remove previous detections
}
},
showFeatureDetections: function(value) {
_mlyShowFeatureDetections = value;
if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {
this.resetTags();
}
},
showSignDetections: function(value) {
_mlyShowSignDetections = value;
if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {
this.resetTags();
}
},
filterViewer: function(context) {
@@ -425,7 +494,7 @@ export default {
var toDate = context.photos().toDate();
var username = context.photos().username();
var filter = ['all'];
if (!showsPano) filter.push(['==', 'pano', false]);
if (!showsFlat && showsPano) filter.push(['==', 'pano', true]);
if (username) filter.push(['==', 'username', username]);
@@ -436,7 +505,7 @@ export default {
if (toDate) {
var toTimestamp = new Date(toDate).getTime();
filter.push(['>=', 'capturedAt', toTimestamp]);
}
}
if (_mlyViewer) {
_mlyViewer.setFilter(filter);
@@ -470,6 +539,7 @@ export default {
hideViewer: function(context) {
_mlyActiveImage = null;
_mlySelectedImageKey = null;
if (!_mlyFallback && _mlyViewer) {
@@ -484,8 +554,9 @@ export default {
.selectAll('.photo-wrapper')
.classed('hide', true);
context.container().selectAll('.viewfield-group, .sequence, .icon-detected')
.classed('currentView', false);
this.updateUrlImage(null);
dispatch.call('nodeChanged');
return this.setStyles(context, null, true);
},
@@ -494,74 +565,83 @@ export default {
parsePagination: parsePagination,
updateViewer: function(context, imageKey) {
if (!imageKey) return this;
updateUrlImage: function(imageKey) {
if (!window.mocha) {
var hash = utilStringQs(window.location.hash);
if (imageKey) {
hash.photo = 'mapillary/' + imageKey;
} else {
delete hash.photo;
}
window.location.replace('#' + utilQsString(hash, true));
}
},
if (!_mlyViewer) {
this.initViewer(context, imageKey);
} else {
_mlyViewer.moveToKey(imageKey)
.catch(function(e) { console.error('mly3', e); }); // eslint-disable-line no-console
highlightDetection: function(detection) {
if (detection) {
_mlyHighlightedDetection = detection.detection_key;
}
return this;
},
initViewer: function(context, imageKey) {
initViewer: function(context) {
var that = this;
if (window.Mapillary && imageKey) {
var opts = {
baseImageSize: 320,
component: {
cover: false,
keyboard: false,
tag: true
}
if (!window.Mapillary) return;
var opts = {
baseImageSize: 320,
component: {
cover: false,
keyboard: false,
tag: true
}
};
// Disable components requiring WebGL support
if (!Mapillary.isSupported() && Mapillary.isFallbackSupported()) {
_mlyFallback = true;
opts.component = {
cover: false,
direction: false,
imagePlane: false,
keyboard: false,
mouse: false,
sequence: false,
tag: false,
image: true, // fallback
navigation: true // fallback
};
// Disable components requiring WebGL support
if (!Mapillary.isSupported() && Mapillary.isFallbackSupported()) {
_mlyFallback = true;
opts.component = {
cover: false,
direction: false,
imagePlane: false,
keyboard: false,
mouse: false,
sequence: false,
tag: false,
image: true, // fallback
navigation: true // fallback
};
}
_mlyViewer = new Mapillary.Viewer('ideditor-mly', clientId, null, opts);
_mlyViewer.on('nodechanged', nodeChanged);
_mlyViewer.on('bearingchanged', bearingChanged);
_mlyViewer.moveToKey(imageKey)
.catch(function(e) { console.error('mly3', e); }); // eslint-disable-line no-console
if (_mlyViewerFilter) {
_mlyViewer.setFilter(_mlyViewerFilter);
}
}
_mlyViewer = new Mapillary.Viewer('ideditor-mly', clientId, null, opts);
_mlyViewer.on('nodechanged', nodeChanged);
_mlyViewer.on('bearingchanged', bearingChanged);
if (_mlyViewerFilter) {
_mlyViewer.setFilter(_mlyViewerFilter);
}
// Register viewer resize handler
context.ui().photoviewer.on('resize.mapillary', function() {
if (_mlyViewer) _mlyViewer.resize();
});
// nodeChanged: called after the viewer has changed images and is ready.
//
// There is some logic here to batch up clicks into a _mlyClicks array
// because the user might click on a lot of markers quickly and nodechanged
// may be called out of order asychronously.
// may be called out of order asynchronously.
//
// Clicks are added to the array in `selectedImage` and removed here.
//
function nodeChanged(node) {
if (!_mlyFallback) {
_mlyViewer.getComponent('tag').removeAll(); // remove previous detections
}
that.resetTags();
var clicks = _mlyClicks;
var index = clicks.indexOf(node.key);
var selectedKey = _mlySelectedImageKey;
that.setActiveImage(node);
if (index > -1) { // `nodechanged` initiated from clicking on a marker..
clicks.splice(index, 1); // remove the click
@@ -575,6 +655,7 @@ export default {
context.map().centerEase(loc);
that.selectImage(context, node.key, true);
}
dispatch.call('nodeChanged');
}
function bearingChanged(e) {
@@ -590,8 +671,8 @@ export default {
_mlySelectedImageKey = imageKey;
// Note the datum could be missing, but we'll try to carry on anyway.
// There just might be a delay before user sees detections, captured_at, etc.
this.updateUrlImage(imageKey);
var d = _mlyCache.images.forImageKey[imageKey];
var viewer = context.container().select('.photoviewer');
@@ -604,22 +685,28 @@ export default {
this.setStyles(context, null, true);
// if signs signs are shown, highlight the ones that appear in this image
context.container().selectAll('.layer-mapillary-signs .icon-detected')
.classed('currentView', function(d) {
return d.detections.some(function(detection) {
return detection.image_key === imageKey;
});
});
if (_mlyShowFeatureDetections) {
this.updateDetections(imageKey, apibase + 'image_detections?layers=points&values=' + mapFeatureConfig.values + '&image_keys=' + imageKey);
}
if (d) {
this.updateDetections(d);
if (_mlyShowSignDetections) {
this.updateDetections(imageKey, apibase + 'image_detections?layers=trafficsigns&image_keys=' + imageKey);
}
if (_mlyViewer && imageKey) {
_mlyViewer.moveToKey(imageKey)
.catch(function(e) { console.error('mly3', e); }); // eslint-disable-line no-console
}
return this;
},
getActiveImage: function() {
return _mlyActiveImage;
},
getSelectedImageKey: function() {
return _mlySelectedImageKey;
},
@@ -630,6 +717,21 @@ export default {
},
setActiveImage: function(node) {
if (node) {
_mlyActiveImage = {
ca: node.originalCA,
key: node.key,
loc: [node.originalLatLon.lon, node.originalLatLon.lat],
pano: node.pano
};
} else {
_mlyActiveImage = null;
}
},
// Updates the currently highlighted sequence and selected bubble.
// Reset is only necessary when interacting with the viewport because
// this implicitly changes the currently selected bubble/sequence
@@ -637,8 +739,7 @@ export default {
if (reset) { // reset all layers
context.container().selectAll('.viewfield-group')
.classed('highlighted', false)
.classed('hovered', false)
.classed('currentView', false);
.classed('hovered', false);
context.container().selectAll('.sequence')
.classed('highlighted', false)
@@ -660,8 +761,7 @@ export default {
context.container().selectAll('.layer-mapillary .viewfield-group')
.classed('highlighted', function(d) { return highlightedImageKeys.indexOf(d.key) !== -1; })
.classed('hovered', function(d) { return d.key === hoveredImageKey; })
.classed('currentView', function(d) { return d.key === selectedImageKey; });
.classed('hovered', function(d) { return d.key === hoveredImageKey; });
context.container().selectAll('.layer-mapillary .sequence')
.classed('highlighted', function(d) { return d.properties.key === hoveredSequenceKey; })
@@ -684,29 +784,49 @@ export default {
},
updateDetections: function(d) {
updateDetections: function(imageKey, url) {
if (!_mlyViewer || _mlyFallback) return;
var imageKey = d && d.key;
if (!imageKey) return;
var detections = _mlyCache.image_detections.forImageKey[imageKey] || [];
detections.forEach(function(data) {
var tag = makeTag(data);
if (tag) {
var tagComponent = _mlyViewer.getComponent('tag');
tagComponent.add([tag]);
}
});
if (!_mlyCache.image_detections.forImageKey[imageKey]) {
loadData('image_detections', url)
.then(() => {
showDetections(_mlyCache.image_detections.forImageKey[imageKey] || []);
});
} else {
showDetections(_mlyCache.image_detections.forImageKey[imageKey]);
}
function showDetections(detections) {
detections.forEach(function(data) {
var tag = makeTag(data);
if (tag) {
var tagComponent = _mlyViewer.getComponent('tag');
tagComponent.add([tag]);
}
});
}
function makeTag(data) {
var valueParts = data.value.split('--');
if (valueParts.length !== 3) return;
if (!valueParts.length) return;
var text = valueParts[1].replace(/-/g, ' ');
var tag;
var text;
var color = 0xffffff;
if (_mlyHighlightedDetection === data.key) {
color = 0xffff00;
text = valueParts[1];
if (text === 'flat' || text === 'discrete' || text === 'sign') {
text = valueParts[2];
}
text = text.replace(/-/g, ' ');
text = text.charAt(0).toUpperCase() + text.slice(1);
_mlyHighlightedDetection = null;
}
// Currently only two shapes <Polygon|Point>
if (data.shape.type === 'Polygon') {
var polygonGeometry = new Mapillary
.TagComponent
@@ -717,10 +837,10 @@ export default {
polygonGeometry,
{
text: text,
textColor: 0xffff00,
lineColor: 0xffff00,
textColor: color,
lineColor: color,
lineWidth: 2,
fillColor: 0xffff00,
fillColor: color,
fillOpacity: 0.3,
}
);
@@ -735,8 +855,8 @@ export default {
pointGeometry,
{
text: text,
color: 0xffff00,
textColor: 0xffff00
color: color,
textColor: color
}
);
}
@@ -745,7 +865,6 @@ export default {
}
},
cache: function() {
return _mlyCache;
}
+62 -30
View File
@@ -7,7 +7,7 @@ import RBush from 'rbush';
import { localizer } from '../core/localizer';
import { geoExtent, geoScaleToZoom } from '../geo';
import { utilArrayUnion, utilQsString, utilRebind, utilSetTransform, utilTiler } from '../util';
import { utilArrayUnion, utilQsString, utilRebind, utilSetTransform, utilStringQs, utilTiler } from '../util';
var apibase = 'https://openstreetcam.org';
@@ -21,6 +21,7 @@ var imgZoom = d3_zoom()
.scaleExtent([1, 15]);
var _oscCache;
var _oscSelectedImage;
var _loadViewerPromise;
function abortRequest(controller) {
@@ -117,6 +118,7 @@ function loadNextTilePage(which, currZoom, url, tile) {
_oscCache.sequences[d.sequence_id] = seq;
}
seq.images[d.sequence_index] = d;
_oscCache.images.forImageKey[d.key] = d; // cache imageKey -> image
}
return {
@@ -186,7 +188,7 @@ export default {
}
_oscCache = {
images: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush() },
images: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), forImageKey: {} },
sequences: {}
};
@@ -234,19 +236,27 @@ export default {
},
cachedImage: function(imageKey) {
return _oscCache.images.forImageKey[imageKey];
},
loadImages: function(projection) {
var url = apibase + '/1.0/list/nearby-photos/';
loadTiles('images', url, projection);
},
loadViewer: function(context) {
var that = this;
ensureViewerLoaded: function(context) {
if (_loadViewerPromise) return _loadViewerPromise;
// add osc-wrapper
var wrap = context.container().select('.photoviewer').selectAll('.osc-wrapper')
.data([0]);
var that = this;
var wrapEnter = wrap.enter()
.append('div')
.attr('class', 'photo-wrapper osc-wrapper')
@@ -267,22 +277,22 @@ export default {
controlsEnter
.append('button')
.on('click.back', step(-1))
.text('◄');
.html('◄');
controlsEnter
.append('button')
.on('click.rotate-ccw', rotate(-90))
.text('⤿');
.html('⤿');
controlsEnter
.append('button')
.on('click.rotate-cw', rotate(90))
.text('⤾');
.html('⤾');
controlsEnter
.append('button')
.on('click.forward', step(1))
.text('►');
.html('►');
wrapEnter
.append('div')
@@ -348,10 +358,14 @@ export default {
context.map().centerEase(nextImage.loc);
that
.selectImage(context, nextImage)
.updateViewer(context, nextImage);
.selectImage(context, nextImage.key);
};
}
// don't need any async loading so resolve immediately
_loadViewerPromise = Promise.resolve();
return _loadViewerPromise;
},
@@ -378,6 +392,8 @@ export default {
hideViewer: function(context) {
_oscSelectedImage = null;
this.updateUrlImage(null);
var viewer = context.container().select('.photoviewer');
if (!viewer.empty()) viewer.datum(null);
@@ -393,7 +409,24 @@ export default {
},
updateViewer: function(context, d) {
selectImage: function(context, imageKey) {
var d = this.cachedImage(imageKey);
_oscSelectedImage = d;
this.updateUrlImage(imageKey);
var viewer = context.container().select('.photoviewer');
if (!viewer.empty()) viewer.datum(d);
this.setStyles(context, null, true);
context.container().selectAll('.icon-sign')
.classed('currentView', false);
if (!d) return this;
var wrap = context.container().select('.photoviewer .osc-wrapper');
var imageWrap = wrap.selectAll('.osc-image-wrap');
var attribution = wrap.selectAll('.photo-attribution').html('');
@@ -423,22 +456,22 @@ export default {
.attr('class', 'captured_by')
.attr('target', '_blank')
.attr('href', 'https://openstreetcam.org/user/' + encodeURIComponent(d.captured_by))
.text('@' + d.captured_by);
.html('@' + d.captured_by);
attribution
.append('span')
.text('|');
.html('|');
}
if (d.captured_at) {
attribution
.append('span')
.attr('class', 'captured_at')
.text(localeDateString(d.captured_at));
.html(localeDateString(d.captured_at));
attribution
.append('span')
.text('|');
.html('|');
}
attribution
@@ -446,7 +479,7 @@ export default {
.attr('class', 'image-link')
.attr('target', '_blank')
.attr('href', 'https://openstreetcam.org/details/' + d.sequence_id + '/' + d.sequence_index)
.text('openstreetcam.org');
.html('openstreetcam.org');
}
return this;
@@ -462,20 +495,6 @@ export default {
},
selectImage: function(context, d) {
_oscSelectedImage = d;
var viewer = context.container().select('.photoviewer');
if (!viewer.empty()) viewer.datum(d);
this.setStyles(context, null, true);
context.container().selectAll('.icon-sign')
.classed('currentView', false);
return this;
},
getSelectedImage: function() {
return _oscSelectedImage;
},
@@ -542,6 +561,19 @@ export default {
},
updateUrlImage: function(imageKey) {
if (!window.mocha) {
var hash = utilStringQs(window.location.hash);
if (imageKey) {
hash.photo = 'openstreetcam/' + imageKey;
} else {
delete hash.photo;
}
window.location.replace('#' + utilQsString(hash, true));
}
},
cache: function() {
return _oscCache;
}
+24 -19
View File
@@ -23,8 +23,8 @@ var oauth = osmAuth({
loading: authLoading,
done: authDone
});
var _blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*'];
// hardcode default block of Google Maps
var _imageryBlocklists = [/.*\.google(apis)?\..*\/(vt|kh)[\?\/].*([xyz]=.*){3}.*/];
var _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: new RBush() };
var _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: new RBush() };
var _userCache = { toLoad: {}, user: {} };
@@ -197,11 +197,11 @@ var jsonparsers = {
return new osmNode({
id: uid,
visible: typeof obj.visible === 'boolean' ? obj.visible : true,
version: obj.version.toString(),
changeset: obj.changeset.toString(),
version: obj.version && obj.version.toString(),
changeset: obj.changeset && obj.changeset.toString(),
timestamp: obj.timestamp,
user: obj.user,
uid: obj.uid.toString(),
uid: obj.uid && obj.uid.toString(),
loc: [parseFloat(obj.lon), parseFloat(obj.lat)],
tags: obj.tags
});
@@ -211,11 +211,11 @@ var jsonparsers = {
return new osmWay({
id: uid,
visible: typeof obj.visible === 'boolean' ? obj.visible : true,
version: obj.version.toString(),
changeset: obj.changeset.toString(),
version: obj.version && obj.version.toString(),
changeset: obj.changeset && obj.changeset.toString(),
timestamp: obj.timestamp,
user: obj.user,
uid: obj.uid.toString(),
uid: obj.uid && obj.uid.toString(),
tags: obj.tags,
nodes: getNodesJSON(obj)
});
@@ -225,11 +225,11 @@ var jsonparsers = {
return new osmRelation({
id: uid,
visible: typeof obj.visible === 'boolean' ? obj.visible : true,
version: obj.version.toString(),
changeset: obj.changeset.toString(),
version: obj.version && obj.version.toString(),
changeset: obj.changeset && obj.changeset.toString(),
timestamp: obj.timestamp,
user: obj.user,
uid: obj.uid.toString(),
uid: obj.uid && obj.uid.toString(),
tags: obj.tags,
members: getMembersJSON(obj)
});
@@ -919,17 +919,22 @@ export default {
return callback(err, null);
}
// update blacklists
// update blocklists
var elements = xml.getElementsByTagName('blacklist');
var regexes = [];
for (var i = 0; i < elements.length; i++) {
var regex = elements[i].getAttribute('regex'); // needs unencode?
if (regex) {
regexes.push(regex);
var regexString = elements[i].getAttribute('regex'); // needs unencode?
if (regexString) {
try {
var regex = new RegExp(regexString);
regexes.push(regex);
} catch (e) {
/* noop */
}
}
}
if (regexes.length) {
_blacklists = regexes;
_imageryBlocklists = regexes;
}
if (_rateLimitError) {
@@ -949,7 +954,7 @@ export default {
// Calls `status` and dispatches an `apiStatusChange` event if the returned
// status differs from the cached status.
reloadApiStatus: function() {
// throttle to avoid unncessary API calls
// throttle to avoid unnecessary API calls
if (!this.throttledReloadApiStatus) {
var that = this;
this.throttledReloadApiStatus = _throttle(function() {
@@ -1321,8 +1326,8 @@ export default {
},
imageryBlacklists: function() {
return _blacklists;
imageryBlocklists: function() {
return _imageryBlocklists;
},
+46 -53
View File
@@ -32,22 +32,6 @@ function request(url, callback) {
}
/**
* Get the best string value from the descriptions/labels result
* Note that if mediawiki doesn't recognize language code, it will return all values.
* In that case, fallback to use English.
* @param values object - either descriptions or labels
* @param langCode String
* @returns localized string
*/
function localizedToString(values, langCode) {
if (values) {
values = values[langCode] || values.en;
}
return values ? values.value : '';
}
export default {
init: function() {
@@ -124,7 +108,6 @@ export default {
// {
// key: 'string',
// value: 'string',
// rtype: 'string',
// langCode: 'string'
// }
//
@@ -133,17 +116,21 @@ export default {
var that = this;
var titles = [];
var result = {};
var rtypeSitelink = params.rtype ? ('Relation:' + params.rtype).replace(/_/g, ' ').trim() : false;
var rtypeSitelink = (params.key === 'type' && params.value) ? ('Relation:' + params.value).replace(/_/g, ' ').trim() : false;
var keySitelink = params.key ? this.toSitelink(params.key) : false;
var tagSitelink = (params.key && params.value) ? this.toSitelink(params.key, params.value) : false;
var localeSitelink;
if (params.langCode && _localeIDs[params.langCode] === undefined) {
// If this is the first time we are asking about this locale,
// fetch corresponding entity (if it exists), and cache it.
// If there is no such entry, cache `false` value to avoid re-requesting it.
localeSitelink = ('Locale:' + params.langCode).replace(/_/g, ' ').trim();
titles.push(localeSitelink);
if (params.langCodes) {
params.langCodes.forEach(function(langCode) {
if (_localeIDs[langCode] === undefined) {
// If this is the first time we are asking about this locale,
// fetch corresponding entity (if it exists), and cache it.
// If there is no such entry, cache `false` value to avoid re-requesting it.
localeSitelink = ('Locale:' + langCode).replace(/_/g, ' ').trim();
titles.push(localeSitelink);
}
});
}
if (rtypeSitelink) {
@@ -184,7 +171,7 @@ export default {
action: 'wbgetentities',
sites: 'wiki',
titles: titles.join('|'),
languages: params.langCode,
languages: params.langCodes.join('|'),
languagefallback: 1,
origin: '*',
format: 'json',
@@ -203,9 +190,6 @@ export default {
var localeID = false;
Object.values(d.entities).forEach(function(res) {
if (res.missing !== '') {
// Simplify access to the localized values
res.description = localizedToString(res.descriptions, params.langCode);
res.label = localizedToString(res.labels, params.langCode);
var title = res.sitelinks.wiki.title;
if (title === rtypeSitelink) {
@@ -227,7 +211,7 @@ export default {
if (localeSitelink) {
// If locale ID is not found, store false to prevent repeated queries
that.addLocale(params.langCode, localeID);
that.addLocale(params.langCodes[0], localeID);
}
callback(null, result);
@@ -242,10 +226,6 @@ export default {
// key: 'string', // required
// value: 'string' // optional
// }
// -or-
// {
// rtype: 'rtype' // relation type (e.g. 'multipolygon')
// }
//
// Get an result object used to display tag documentation
// {
@@ -258,8 +238,10 @@ export default {
//
getDocs: function(params, callback) {
var that = this;
var langCode = localizer.localeCode().toLowerCase();
params.langCode = langCode;
var langCodes = localizer.localeCodes().map(function(code) {
return code.toLowerCase();
});
params.langCodes = langCodes;
this.getEntity(params, function(err, data) {
if (err) {
@@ -273,21 +255,33 @@ export default {
return;
}
var i;
var description;
for (i in langCodes) {
let code = langCodes[i];
if (entity.descriptions[code] && entity.descriptions[code].language === code) {
description = entity.descriptions[code];
break;
}
}
if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];
// prepare result
var result = {
title: entity.title,
description: entity.description,
description: description ? description.value : '',
descriptionLocaleCode: description ? description.language : '',
editURL: 'https://wiki.openstreetmap.org/wiki/' + entity.title
};
// add image
if (entity.claims) {
var imageroot;
var image = that.claimToValue(entity, 'P4', langCode);
var image = that.claimToValue(entity, 'P4', langCodes[0]);
if (image) {
imageroot = 'https://commons.wikimedia.org/w/index.php';
} else {
image = that.claimToValue(entity, 'P28', langCode);
image = that.claimToValue(entity, 'P28', langCodes[0]);
if (image) {
imageroot = 'https://wiki.openstreetmap.org/w/index.php';
}
@@ -307,21 +301,20 @@ export default {
var tagWiki = that.monolingualClaimToValueObj(data.tag, 'P31');
var keyWiki = that.monolingualClaimToValueObj(data.key, 'P31');
// If exact language code does not exist, try to find the first part before the '-'
// BUG: in some cases, a more elaborate fallback logic might be needed
var langPrefix = langCode.split('-', 2)[0];
// use the first acceptable wiki page
result.wiki =
getWikiInfo(rtypeWiki, langCode, 'inspector.wiki_reference') ||
getWikiInfo(rtypeWiki, langPrefix, 'inspector.wiki_reference') ||
getWikiInfo(rtypeWiki, 'en', 'inspector.wiki_en_reference') ||
getWikiInfo(tagWiki, langCode, 'inspector.wiki_reference') ||
getWikiInfo(tagWiki, langPrefix, 'inspector.wiki_reference') ||
getWikiInfo(tagWiki, 'en', 'inspector.wiki_en_reference') ||
getWikiInfo(keyWiki, langCode, 'inspector.wiki_reference') ||
getWikiInfo(keyWiki, langPrefix, 'inspector.wiki_reference') ||
getWikiInfo(keyWiki, 'en', 'inspector.wiki_en_reference');
var wikis = [rtypeWiki, tagWiki, keyWiki];
for (i in wikis) {
var wiki = wikis[i];
for (var j in langCodes) {
var code = langCodes[j];
var referenceId = (langCodes[0].split('-')[0] !== 'en' && code.split('-')[0] === 'en') ? 'inspector.wiki_en_reference' : 'inspector.wiki_reference';
var info = getWikiInfo(wiki, code, referenceId);
if (info) {
result.wiki = info;
break;
}
}
if (result.wiki) break;
}
callback(null, result);
+1 -1
View File
@@ -49,7 +49,7 @@ function updateRtree(item, replace) {
}
}
// Issues shouldn't obscure eachother
// Issues shouldn't obscure each other
function preventCoincident(loc) {
let coincident = false;
do {
+133 -102
View File
@@ -15,7 +15,7 @@ import {
geoRotate, geoScaleToZoom, geoVecLength
} from '../geo';
import { utilArrayUnion, utilQsString, utilRebind, utilTiler, utilUniqueDomId } from '../util';
import { utilArrayUnion, utilQsString, utilRebind, utilStringQs, utilTiler, utilUniqueDomId } from '../util';
const bubbleApi = 'https://dev.virtualearth.net/mapcontrol/HumanScaleServices/GetBubbles.ashx?';
@@ -26,7 +26,7 @@ const pannellumViewerJS = 'pannellum-streetside/pannellum.js';
const maxResults = 2000;
const tileZoom = 16.5;
const tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
const dispatch = d3_dispatch('loadedBubbles', 'viewerChanged');
const dispatch = d3_dispatch('loadedImages', 'viewerChanged');
const minHfov = 10; // zoom in degrees: 20, 10, 5
const maxHfov = 90; // zoom out degrees
const defaultHfov = 45;
@@ -36,8 +36,18 @@ let _resolution = 512; // higher numbers are slower - 512, 1024, 2048, 4096
let _currScene = 0;
let _ssCache;
let _pannellumViewer;
let _sceneOptions;
let _dataUrlArray = [];
let _sceneOptions = {
showFullscreenCtrl: false,
autoLoad: true,
compass: true,
yaw: 0,
minHfov: minHfov,
maxHfov: maxHfov,
hfov: defaultHfov,
type: 'cubemap',
cubeMap: []
};
let _loadViewerPromise;
/**
@@ -135,7 +145,7 @@ function loadNextTilePage(which, url, tile) {
connectSequences();
if (which === 'bubbles') {
dispatch.call('loadedBubbles');
dispatch.call('loadedImages');
}
});
}
@@ -276,7 +286,7 @@ function loadCanvas(imageGroup) {
let canvas = document.getElementById('ideditor-canvas' + data[0].imgInfo.face);
const which = { '01': 0, '02': 1, '03': 2, '10': 3, '11': 4, '12': 5 };
let face = data[0].imgInfo.face;
_dataUrlArray[which[face]] = canvas.toDataURL('image/jpeg', 1.0);
_sceneOptions.cubeMap[which[face]] = canvas.toDataURL('image/jpeg', 1.0);
return { status: 'loadCanvas for face ' + data[0].imgInfo.face + 'ok'};
});
}
@@ -423,6 +433,11 @@ export default {
},
cachedImage: function(imageKey) {
return _ssCache.bubbles.points[imageKey];
},
sequences: function(projection) {
const viewport = projection.clipExtent();
const min = [viewport[0][0], viewport[1][1]];
@@ -476,13 +491,9 @@ export default {
},
/**
* loadViewer() create the streeside viewer.
*/
loadViewer: function(context) {
let that = this;
ensureViewerLoaded: function(context) {
let pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';
if (_loadViewerPromise) return _loadViewerPromise;
// create ms-wrapper, a photo wrapper class
let wrap = context.container().select('.photoviewer').selectAll('.ms-wrapper')
@@ -495,6 +506,10 @@ export default {
.attr('class', 'photo-wrapper ms-wrapper')
.classed('hide', true);
let that = this;
let pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';
// inject div to support streetside viewer (pannellum) and attribution line
wrapEnter
.append('div')
@@ -529,12 +544,12 @@ export default {
controlsEnter
.append('button')
.on('click.back', step(-1))
.text('◄');
.html('◄');
controlsEnter
.append('button')
.on('click.forward', step(1))
.text('►');
.html('►');
// create working canvas for stitching together images
@@ -542,24 +557,6 @@ export default {
.merge(wrapEnter)
.call(setupCanvas, true);
// load streetside pannellum viewer css
d3_select('head').selectAll('#ideditor-streetside-viewercss')
.data([0])
.enter()
.append('link')
.attr('id', 'ideditor-streetside-viewercss')
.attr('rel', 'stylesheet')
.attr('href', context.asset(pannellumViewerCSS));
// load streetside pannellum viewer js
d3_select('head').selectAll('#ideditor-streetside-viewerjs')
.data([0])
.enter()
.append('script')
.attr('id', 'ideditor-streetside-viewerjs')
.attr('src', context.asset(pannellumViewerJS));
// Register viewer resize handler
context.ui().photoviewer.on('resize.streetside', () => {
if (_pannellumViewer) {
@@ -567,6 +564,45 @@ export default {
}
});
_loadViewerPromise = new Promise((resolve, reject) => {
let loadedCount = 0;
function loaded() {
loadedCount += 1;
// wait until both files are loaded
if (loadedCount === 2) resolve();
}
const head = d3_select('head');
// load streetside pannellum viewer css
head.selectAll('#ideditor-streetside-viewercss')
.data([0])
.enter()
.append('link')
.attr('id', 'ideditor-streetside-viewercss')
.attr('rel', 'stylesheet')
.attr('crossorigin', 'anonymous')
.attr('href', context.asset(pannellumViewerCSS))
.on('load.serviceStreetside', loaded)
.on('error.serviceStreetside', reject);
// load streetside pannellum viewer js
head.selectAll('#ideditor-streetside-viewerjs')
.data([0])
.enter()
.append('script')
.attr('id', 'ideditor-streetside-viewerjs')
.attr('crossorigin', 'anonymous')
.attr('src', context.asset(pannellumViewerJS))
.on('load.serviceStreetside', loaded)
.on('error.serviceStreetside', reject);
})
.catch(function() {
_loadViewerPromise = null;
});
return _loadViewerPromise;
function step(stepBy) {
return () => {
@@ -628,49 +664,29 @@ export default {
}
});
let nextBubble = nextID && _ssCache.bubbles.points[nextID];
let nextBubble = nextID && that.cachedImage(nextID);
if (!nextBubble) return;
context.map().centerEase(nextBubble.loc);
that.selectImage(context, nextBubble)
.then(response => {
if (response.status === 'ok') {
_sceneOptions.yaw = yaw;
that.showViewer(context);
}
});
that.selectImage(context, nextBubble.key)
.yaw(yaw)
.showViewer(context);
};
}
},
yaw: function(yaw) {
if (typeof yaw !== 'number') return yaw;
_sceneOptions.yaw = yaw;
return this;
},
/**
* showViewer()
*/
showViewer: function(context, yaw) {
if (!_sceneOptions) return;
if (yaw !== undefined) {
_sceneOptions.yaw = yaw;
}
if (!_pannellumViewer) {
this.initViewer();
} else {
// make a new scene
let sceneID = ++_currScene + '';
_pannellumViewer
.addScene(sceneID, _sceneOptions)
.loadScene(sceneID);
// remove previous scene
if (_currScene > 2) {
sceneID = (_currScene - 1) + '';
_pannellumViewer
.removeScene(sceneID);
}
}
showViewer: function(context) {
let wrap = context.container().select('.photoviewer')
.classed('hide', false);
@@ -706,6 +722,8 @@ export default {
context.container().selectAll('.viewfield-group, .sequence, .icon-sign')
.classed('currentView', false);
this.updateUrlImage(null);
return this.setStyles(context, null, true);
},
@@ -713,8 +731,11 @@ export default {
/**
* selectImage().
*/
selectImage: function (context, d) {
selectImage: function (context, key) {
let that = this;
let d = this.cachedImage(key);
let viewer = context.container().select('.photoviewer');
if (!viewer.empty()) viewer.datum(d);
@@ -726,9 +747,11 @@ export default {
wrap.selectAll('.pnlm-load-box') // display "loading.."
.style('display', 'block');
if (!d) {
return Promise.resolve({ status: 'ok' });
}
if (!d) return this;
this.updateUrlImage(key);
_sceneOptions.northOffset = d.ca;
let line1 = attribution
.append('div')
@@ -760,18 +783,14 @@ export default {
hfov: _pannellumViewer.getHfov()
};
that.selectImage(context, d)
.then(response => {
if (response.status === 'ok') {
_sceneOptions = Object.assign(_sceneOptions, viewstate);
that.showViewer(context);
}
});
_sceneOptions = Object.assign(_sceneOptions, viewstate);
that.selectImage(context, d.key)
.showViewer(context);
});
label
.append('span')
.text(t('streetside.hires'));
.html(t.html('streetside.hires'));
let captureInfo = line1
@@ -787,18 +806,18 @@ export default {
.attr('class', 'captured_by')
.attr('target', '_blank')
.attr('href', 'https://www.microsoft.com/en-us/maps/streetside')
.text('©' + yyyy + ' Microsoft');
.html('©' + yyyy + ' Microsoft');
captureInfo
.append('span')
.text('|');
.html('|');
}
if (d.captured_at) {
captureInfo
.append('span')
.attr('class', 'captured_at')
.text(localeTimestamp(d.captured_at));
.html(localeTimestamp(d.captured_at));
}
// Add image links
@@ -812,7 +831,7 @@ export default {
.attr('target', '_blank')
.attr('href', 'https://www.bing.com/maps?cp=' + d.loc[1] + '~' + d.loc[0] +
'&lvl=17&dir=' + d.ca + '&style=x&v=2&sV=1')
.text(t('streetside.view_on_bing'));
.html(t.html('streetside.view_on_bing'));
line2
.append('a')
@@ -820,7 +839,7 @@ export default {
.attr('target', '_blank')
.attr('href', 'https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid=' +
encodeURIComponent(d.key) + '&focus=photo&lat=' + d.loc[1] + '&lng=' + d.loc[0] + '&z=17')
.text(t('streetside.report'));
.html(t.html('streetside.report'));
let bubbleIdQuadKey = d.key.toString(4);
@@ -848,29 +867,28 @@ export default {
});
});
return loadFaces(faces)
.then(() => {
_sceneOptions = {
showFullscreenCtrl: false,
autoLoad: true,
compass: true,
northOffset: d.ca,
yaw: 0,
minHfov: minHfov,
maxHfov: maxHfov,
hfov: defaultHfov,
type: 'cubemap',
cubeMap: [
_dataUrlArray[0],
_dataUrlArray[1],
_dataUrlArray[2],
_dataUrlArray[3],
_dataUrlArray[4],
_dataUrlArray[5]
]
};
return { status: 'ok' };
loadFaces(faces)
.then(function() {
if (!_pannellumViewer) {
that.initViewer();
} else {
// make a new scene
let sceneID = ++_currScene + '';
_pannellumViewer
.addScene(sceneID, _sceneOptions)
.loadScene(sceneID);
// remove previous scene
if (_currScene > 2) {
sceneID = (_currScene - 1) + '';
_pannellumViewer
.removeScene(sceneID);
}
}
});
return this;
},
@@ -935,6 +953,19 @@ export default {
},
updateUrlImage: function(imageKey) {
if (!window.mocha) {
var hash = utilStringQs(window.location.hash);
if (imageKey) {
hash.photo = 'streetside/' + imageKey;
} else {
delete hash.photo;
}
window.location.replace('#' + utilQsString(hash, true));
}
},
/**
* cache().
*/
+18 -14
View File
@@ -1,6 +1,6 @@
import { json as d3_json } from 'd3-fetch';
import { utilArrayUniq, utilQsString } from '../util';
import { utilQsString } from '../util';
import { localizer } from '../core/localizer';
var apibase = 'https://www.wikidata.org/w/api.php?';
@@ -33,7 +33,7 @@ export default {
type: 'item',
// the language to search
language: lang,
// the langauge for the label and description in the result
// the language for the label and description in the result
uselang: lang,
limit: 10,
origin: '*'
@@ -85,15 +85,13 @@ export default {
languagesToQuery: function() {
var localeCode = localizer.localeCode().toLowerCase();
// HACK: en-us isn't a wikidata language. We should really be filtering by
// the languages known to be supported by wikidata.
if (localeCode === 'en-us') localeCode = 'en';
return utilArrayUniq([
localeCode,
localizer.languageCode().toLowerCase(),
'en'
]);
return localizer.localeCodes().map(function(code) {
return code.toLowerCase();
}).filter(function(code) {
// HACK: en-us isn't a wikidata language. We should really be filtering by
// the languages known to be supported by wikidata.
return code !== 'en-us';
});
},
@@ -157,14 +155,20 @@ export default {
var i;
var description;
if (entity.descriptions && Object.keys(entity.descriptions).length > 0) {
description = entity.descriptions[Object.keys(entity.descriptions)[0]].value;
for (i in langs) {
let code = langs[i];
if (entity.descriptions[code] && entity.descriptions[code].language === code) {
description = entity.descriptions[code];
break;
}
}
if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];
// prepare result
var result = {
title: entity.id,
description: description,
description: description ? description.value : '',
descriptionLocaleCode: description ? description.language : '',
editURL: 'https://www.wikidata.org/wiki/' + entity.id
};
+8 -12
View File
@@ -379,28 +379,24 @@ export function svgData(projection, context, dispatch) {
drawData.template = function(val, src) {
if (!arguments.length) return _template;
// test source against OSM imagery blacklists..
// test source against OSM imagery blocklists..
var osm = context.connection();
if (osm) {
var blacklists = osm.imageryBlacklists();
var blocklists = osm.imageryBlocklists();
var fail = false;
var tested = 0;
var regex;
for (var i = 0; i < blacklists.length; i++) {
try {
regex = new RegExp(blacklists[i]);
fail = regex.test(val);
tested++;
if (fail) break;
} catch (e) {
/* noop */
}
for (var i = 0; i < blocklists.length; i++) {
regex = blocklists[i];
fail = regex.test(val);
tested++;
if (fail) break;
}
// ensure at least one test was run.
if (!tested) {
regex = new RegExp('.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*');
regex = /.*\.google(apis)?\..*\/(vt|kh)[\?\/].*([xyz]=.*){3}.*/;
fail = regex.test(val);
}
}
+25 -17
View File
@@ -10,11 +10,17 @@ import { utilArrayUniq } from '../util';
*/
export function svgDefs(context) {
var _defsSelection = d3_select(null);
var _spritesheetIds = [
'iD-sprite', 'maki-sprite', 'temaki-sprite', 'fa-sprite', 'community-sprite'
];
function drawDefs(selection) {
var defs = selection.append('defs');
_defsSelection = selection.append('defs');
// add markers
defs
_defsSelection
.append('marker')
.attr('id', 'ideditor-oneway-marker')
.attr('viewBox', '0 0 10 5')
@@ -37,7 +43,7 @@ export function svgDefs(context) {
// (also, it's slightly nicer if we can control the
// positioning for different tags)
function addSidedMarker(name, color, offset) {
defs
_defsSelection
.append('marker')
.attr('id', 'ideditor-sided-marker-' + name)
.attr('viewBox', '0 0 2 2')
@@ -64,7 +70,7 @@ export function svgDefs(context) {
addSidedMarker('barrier', '#ddd', 1);
addSidedMarker('man_made', '#fff', 0);
defs
_defsSelection
.append('marker')
.attr('id', 'ideditor-viewfield-marker')
.attr('viewBox', '0 0 16 16')
@@ -83,7 +89,7 @@ export function svgDefs(context) {
.attr('stroke-width', '0.5px')
.attr('stroke-opacity', '0.75');
defs
_defsSelection
.append('marker')
.attr('id', 'ideditor-viewfield-marker-wireframe')
.attr('viewBox', '0 0 16 16')
@@ -102,7 +108,7 @@ export function svgDefs(context) {
.attr('stroke-opacity', '0.75');
// add patterns
var patterns = defs.selectAll('pattern')
var patterns = _defsSelection.selectAll('pattern')
.data([
// pattern name, pattern image name
['beach', 'dots'],
@@ -161,7 +167,7 @@ export function svgDefs(context) {
});
// add clip paths
defs.selectAll('clipPath')
_defsSelection.selectAll('clipPath')
.data([12, 18, 20, 32, 45])
.enter()
.append('clipPath')
@@ -173,20 +179,17 @@ export function svgDefs(context) {
.attr('height', function (d) { return d; });
// add symbol spritesheets
defs
.call(drawDefs.addSprites, [
'iD-sprite', 'maki-sprite', 'temaki-sprite', 'fa-sprite', 'tnp-sprite', 'community-sprite'
], true);
addSprites(_spritesheetIds, true);
}
function addSprites(ids, overrideColors) {
_spritesheetIds = utilArrayUniq(_spritesheetIds.concat(ids));
drawDefs.addSprites = function(selection, ids, overrideColors) {
var spritesheets = selection.selectAll('.spritesheet');
var currData = spritesheets.data();
var data = utilArrayUniq(currData.concat(ids));
var spritesheets = _defsSelection
.selectAll('.spritesheet')
.data(_spritesheetIds);
spritesheets
.data(data)
.enter()
.append('g')
.attr('class', function(d) { return 'spritesheet spritesheet-' + d; })
@@ -208,8 +211,13 @@ export function svgDefs(context) {
/* ignore */
});
});
};
spritesheets
.exit()
.remove();
}
drawDefs.addSprites = addSprites;
return drawDefs;
}
+2
View File
@@ -9,6 +9,7 @@ import { svgImproveOSM } from './improveOSM';
import { svgOsmose } from './osmose';
import { svgStreetside } from './streetside';
import { svgMapillaryImages } from './mapillary_images';
import { svgMapillaryPosition } from './mapillary_position';
import { svgMapillarySigns } from './mapillary_signs';
import { svgMapillaryMapFeatures } from './mapillary_map_features';
import { svgOpenstreetcamImages } from './openstreetcam_images';
@@ -31,6 +32,7 @@ export function svgLayers(projection, context) {
{ id: 'osmose', layer: svgOsmose(projection, context, dispatch) },
{ id: 'streetside', layer: svgStreetside(projection, context, dispatch)},
{ id: 'mapillary', layer: svgMapillaryImages(projection, context, dispatch) },
{ id: 'mapillary-position', layer: svgMapillaryPosition(projection, context, dispatch) },
{ id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) },
{ id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },
{ id: 'openstreetcam', layer: svgOpenstreetcamImages(projection, context, dispatch) },
+7 -20
View File
@@ -26,19 +26,6 @@ export function svgMapillaryImages(projection, context, dispatch) {
if (services.mapillary && !_mapillary) {
_mapillary = services.mapillary;
_mapillary.event.on('loadedImages', throttledRedraw);
_mapillary.event.on('bearingChanged', function(e) {
viewerCompassAngle = e;
// avoid updating if the map is currently transformed
// e.g. during drags or easing.
if (context.map().isTransformed()) return;
layer.selectAll('.viewfield-group.currentView')
.filter(function(d) {
return d.pano;
})
.attr('transform', transform);
});
} else if (!services.mapillary && _mapillary) {
_mapillary = null;
}
@@ -89,9 +76,12 @@ export function svgMapillaryImages(projection, context, dispatch) {
if (!service) return;
service
.selectImage(context, d.key)
.updateViewer(context, d.key)
.showViewer(context);
.ensureViewerLoaded(context)
.then(function() {
service
.selectImage(context, d.key)
.showViewer(context);
});
context.map().centerEase(d.loc);
}
@@ -210,7 +200,6 @@ export function svgMapillaryImages(projection, context, dispatch) {
var showViewfields = (z >= minViewfieldZoom);
var service = getService();
var selectedKey = service && service.getSelectedImageKey();
var sequences = (service ? service.sequences(projection) : []);
var images = (service && showMarkers ? service.images(projection) : []);
@@ -256,9 +245,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
var markers = groups
.merge(groupsEnter)
.sort(function(a, b) {
return (a.key === selectedKey) ? 1
: (b.key === selectedKey) ? -1
: b.loc[1] - a.loc[1]; // sort Y
return b.loc[1] - a.loc[1]; // sort Y
})
.attr('transform', transform)
.select('.viewfield-scale');
+19 -5
View File
@@ -33,6 +33,7 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
var service = getService();
if (!service) return;
service.loadObjectResources(context);
editOn();
}
@@ -62,19 +63,29 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
var selectedImageKey = service.getSelectedImageKey();
var imageKey;
var highlightedDetection;
// Pick one of the images the map feature was detected in,
// preference given to an image already selected.
d.detections.forEach(function(detection) {
if (!imageKey || selectedImageKey === detection.image_key) {
imageKey = detection.image_key;
highlightedDetection = detection;
}
});
service
.selectImage(context, imageKey)
.updateViewer(context, imageKey)
.showViewer(context);
if (imageKey === selectedImageKey) {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey)
.showViewer(context);
});
}
}
@@ -172,9 +183,12 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
editOn();
update();
service.loadMapFeatures(projection);
service.showFeatureDetections(true);
} else {
editOff();
}
} else if (service) {
service.showFeatureDetections(false);
}
}
+173
View File
@@ -0,0 +1,173 @@
import _throttle from 'lodash-es/throttle';
import { select as d3_select } from 'd3-selection';
import { svgPointTransform } from './helpers';
import { services } from '../services';
export function svgMapillaryPosition(projection, context) {
var throttledRedraw = _throttle(function () { update(); }, 1000);
var minZoom = 12;
var minViewfieldZoom = 18;
var layer = d3_select(null);
var _mapillary;
var viewerCompassAngle;
function init() {
if (svgMapillaryPosition.initialized) return; // run once
svgMapillaryPosition.initialized = true;
}
function getService() {
if (services.mapillary && !_mapillary) {
_mapillary = services.mapillary;
_mapillary.event.on('nodeChanged', throttledRedraw);
_mapillary.event.on('bearingChanged', function(e) {
viewerCompassAngle = e;
if (context.map().isTransformed()) return;
layer.selectAll('.viewfield-group.currentView')
.filter(function(d) {
return d.pano;
})
.attr('transform', transform);
});
} else if (!services.mapillary && _mapillary) {
_mapillary = null;
}
return _mapillary;
}
function editOn() {
layer.style('display', 'block');
}
function editOff() {
layer.selectAll('.viewfield-group').remove();
layer.style('display', 'none');
}
function transform(d) {
var t = svgPointTransform(projection)(d);
if (d.pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {
t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';
} else if (d.ca) {
t += ' rotate(' + Math.floor(d.ca) + ',0,0)';
}
return t;
}
function update() {
var z = ~~context.map().zoom();
var showViewfields = (z >= minViewfieldZoom);
var service = getService();
var node = service && service.getActiveImage();
var groups = layer.selectAll('.markers').selectAll('.viewfield-group')
.data(node ? [node] : [], function(d) { return d.key; });
// exit
groups.exit()
.remove();
// enter
var groupsEnter = groups.enter()
.append('g')
.attr('class', 'viewfield-group currentView highlighted');
groupsEnter
.append('g')
.attr('class', 'viewfield-scale');
// update
var markers = groups
.merge(groupsEnter)
.attr('transform', transform)
.select('.viewfield-scale');
markers.selectAll('circle')
.data([0])
.enter()
.append('circle')
.attr('dx', '0')
.attr('dy', '0')
.attr('r', '6');
var viewfields = markers.selectAll('.viewfield')
.data(showViewfields ? [0] : []);
viewfields.exit()
.remove();
viewfields.enter()
.insert('path', 'circle')
.attr('class', 'viewfield')
.classed('pano', function() { return this.parentNode.__data__.pano; })
.attr('transform', 'scale(1.5,1.5),translate(-8, -13)')
.attr('d', viewfieldPath);
function viewfieldPath() {
var d = this.parentNode.__data__;
if (d.pano) {
return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';
} else {
return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';
}
}
}
function drawImages(selection) {
var service = getService();
layer = selection.selectAll('.layer-mapillary-position')
.data(service ? [0] : []);
layer.exit()
.remove();
var layerEnter = layer.enter()
.append('g')
.attr('class', 'layer-mapillary-position');
layerEnter
.append('g')
.attr('class', 'markers');
layer = layerEnter
.merge(layer);
if (service && ~~context.map().zoom() >= minZoom) {
editOn();
update();
} else {
editOff();
}
}
drawImages.enabled = function() {
update();
return this;
};
drawImages.supported = function() {
return !!getService();
};
init();
return drawImages;
}
+20 -5
View File
@@ -33,6 +33,7 @@ export function svgMapillarySigns(projection, context, dispatch) {
var service = getService();
if (!service) return;
service.loadSignResources(context);
editOn();
}
@@ -62,19 +63,30 @@ export function svgMapillarySigns(projection, context, dispatch) {
var selectedImageKey = service.getSelectedImageKey();
var imageKey;
var highlightedDetection;
// Pick one of the images the sign was detected in,
// preference given to an image already selected.
d.detections.forEach(function(detection) {
if (!imageKey || selectedImageKey === detection.image_key) {
imageKey = detection.image_key;
highlightedDetection = detection;
}
});
service
.selectImage(context, imageKey)
.updateViewer(context, imageKey)
.showViewer(context);
if (imageKey === selectedImageKey) {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey);
} else {
service.ensureViewerLoaded(context)
.then(function() {
service
.highlightDetection(highlightedDetection)
.selectImage(context, imageKey)
.showViewer(context);
});
}
}
@@ -159,9 +171,12 @@ export function svgMapillarySigns(projection, context, dispatch) {
editOn();
update();
service.loadSigns(projection);
service.showSignDetections(true);
} else {
editOff();
}
} else if (service) {
service.showSignDetections(false);
}
}
+5 -3
View File
@@ -74,9 +74,11 @@ export function svgOpenstreetcamImages(projection, context, dispatch) {
if (!service) return;
service
.selectImage(context, d)
.updateViewer(context, d)
.showViewer(context);
.ensureViewerLoaded(context)
.then(function() {
service.selectImage(context, d.key)
.showViewer(context);
});
context.map().centerEase(d.loc);
}
+8 -8
View File
@@ -31,7 +31,7 @@ export function svgStreetside(projection, context, dispatch) {
_streetside = services.streetside;
_streetside.event
.on('viewerChanged.svgStreetside', viewerChanged)
.on('loadedBubbles.svgStreetside', throttledRedraw);
.on('loadedImages.svgStreetside', throttledRedraw);
} else if (!services.streetside && _streetside) {
_streetside = null;
}
@@ -98,14 +98,14 @@ export function svgStreetside(projection, context, dispatch) {
_selectedSequence = d.sequenceKey;
service
.selectImage(context, d)
.then(response => {
if (response.status === 'ok'){
service.showViewer(context, _viewerYaw);
}
.ensureViewerLoaded(context)
.then(function() {
service
.selectImage(context, d.key)
.yaw(_viewerYaw)
.showViewer(context);
});
context.map().centerEase(d.loc);
}
@@ -316,7 +316,7 @@ export function svgStreetside(projection, context, dispatch) {
/**
* drawImages()
* drawImages is the method that is returned (and that runs) everytime 'svgStreetside()' is called.
* drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.
* 'svgStreetside()' is called from index.js
*/
function drawImages(selection) {
+2 -2
View File
@@ -304,7 +304,7 @@ export function svgVertices(projection, context) {
function addChildVertices(entity) {
// avoid redunant work and infinite recursion of circular relations
// avoid redundant work and infinite recursion of circular relations
if (seenIds[entity.id]) return;
seenIds[entity.id] = true;
@@ -368,7 +368,7 @@ export function svgVertices(projection, context) {
}
// Collect important vertices from the `entities` list..
// (during a paritial redraw, it will not contain everything)
// (during a partial redraw, it will not contain everything)
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
var geometry = entity.geometry(graph);
+10 -9
View File
@@ -30,29 +30,29 @@ export function uiAccount(context) {
.classed('hide', false);
// Link
userLink.append('a')
var userLinkA = userLink.append('a')
.attr('href', osm.userURL(details.display_name))
.attr('target', '_blank');
// Add thumbnail or dont
if (details.image_url) {
userLink.append('img')
userLinkA.append('img')
.attr('class', 'icon pre-text user-icon')
.attr('src', details.image_url);
} else {
userLink
userLinkA
.call(svgIcon('#iD-icon-avatar', 'pre-text light'));
}
// Add user name
userLink.append('span')
userLinkA.append('span')
.attr('class', 'label')
.text(details.display_name);
.html(details.display_name);
logoutLink.append('a')
.attr('class', 'logout')
.attr('href', '#')
.text(t('logout'))
.html(t.html('logout'))
.on('click.logout', function() {
d3_event.preventDefault();
osm.logout();
@@ -62,14 +62,15 @@ export function uiAccount(context) {
return function(selection) {
selection.append('li')
.attr('class', 'logoutLink')
.classed('hide', true);
selection.append('li')
.attr('class', 'userLink')
.classed('hide', true);
selection.append('li')
.attr('class', 'logoutLink')
.classed('hide', true);
if (osm) {
osm.on('change.account', function() { update(selection); });
update(selection);
+2 -2
View File
@@ -56,7 +56,7 @@ export function uiAttribution(context) {
attribution
.append('span')
.attr('class', 'attribution-text')
.text(terms_text);
.html(terms_text);
})
.merge(attributions);
@@ -76,7 +76,7 @@ export function uiAttribution(context) {
.merge(copyright);
copyright
.text(String);
.html(String);
}
+1 -2
View File
@@ -104,11 +104,10 @@ export function uiChangesetEditor(context) {
commentEnter
.append('a')
.attr('target', '_blank')
.attr('tabindex', -1)
.call(svgIcon('#iD-icon-alert', 'inline'))
.attr('href', t('commit.google_warning_link'))
.append('span')
.text(t('commit.google_warning'));
.html(t.html('commit.google_warning'));
commentEnter
.transition()
+5 -3
View File
@@ -6,8 +6,10 @@ import { utilGetSetValue, utilRebind, utilTriggerEvent } from '../util';
// This code assumes that the combobox values will not have duplicate entries.
// It is keyed on the `value` of the entry. Data should be an array of objects like:
// [{
// value: 'display text', // required
// title: 'hover text' // optional
// value: 'string value', // required
// display: 'label html' // optional
// title: 'hover text' // optional
// terms: ['search terms'] // optional
// }, ...]
var _comboHideTimerID;
@@ -380,7 +382,7 @@ export function uiCombobox(context, klass) {
.append('a')
.attr('class', 'combobox-option')
.attr('title', function(d) { return d.title; })
.text(function(d) { return d.display || d.value; })
.html(function(d) { return d.display || d.value; })
.on('mouseenter', _mouseEnterHandler)
.on('mouseleave', _mouseLeaveHandler)
.merge(options)
+19 -22
View File
@@ -213,21 +213,14 @@ export function uiCommit(context) {
var headerTitle = header.enter()
.append('div')
.attr('class', 'header fillL header-container');
.attr('class', 'header fillL');
headerTitle
.append('div')
.attr('class', 'header-block header-block-outer');
headerTitle
.append('div')
.attr('class', 'header-block')
.append('h3')
.text(t('commit.title'));
.html(t.html('commit.title'));
headerTitle
.append('div')
.attr('class', 'header-block header-block-outer header-block-close')
.append('button')
.attr('class', 'close')
.on('click', function() {
@@ -283,7 +276,7 @@ export function uiCommit(context) {
prose = prose.enter()
.append('p')
.attr('class', 'commit-info')
.text(t('commit.upload_explanation'))
.html(t.html('commit.upload_explanation'))
.merge(prose);
// always check if this has changed, but only update prose.html()
@@ -306,12 +299,12 @@ export function uiCommit(context) {
userLink
.append('a')
.attr('class', 'user-info')
.text(user.display_name)
.html(user.display_name)
.attr('href', osm.userURL(user.display_name))
.attr('target', '_blank');
prose
.html(t('commit.upload_explanation_with_user', { user: userLink.html() }));
.html(t.html('commit.upload_explanation_with_user', { user: userLink.html() }));
});
@@ -337,7 +330,7 @@ export function uiCommit(context) {
labelEnter
.append('span')
.text(t('commit.request_review'));
.html(t.html('commit.request_review'));
// Update
requestReview = requestReview
@@ -362,7 +355,7 @@ export function uiCommit(context) {
.attr('class', 'secondary-action button cancel-button')
.append('span')
.attr('class', 'label')
.text(t('commit.cancel'));
.html(t.html('commit.cancel'));
var uploadButton = buttonEnter
.append('button')
@@ -370,7 +363,7 @@ export function uiCommit(context) {
uploadButton.append('span')
.attr('class', 'label')
.text(t('commit.save'));
.html(t.html('commit.save'));
var uploadBlockerTooltipText = getUploadBlockerMessage();
@@ -388,6 +381,12 @@ export function uiCommit(context) {
.on('click.save', function() {
if (!d3_select(this).classed('disabled')) {
this.blur(); // avoid keeping focus on the button - #4641
for (var key in context.changeset.tags) {
// remove any empty keys before upload
if (!key) delete context.changeset.tags[key];
}
context.uploader().save(context.changeset);
}
});
@@ -547,14 +546,12 @@ export function uiCommit(context) {
k = context.cleanTagKey(k);
if (readOnlyTags.indexOf(k) !== -1) return;
if (k !== '' && v !== undefined) {
if (onInput) {
tags[k] = v;
} else {
tags[k] = context.cleanTagValue(v);
}
} else {
if (v === undefined) {
delete tags[k];
} else if (onInput) {
tags[k] = v;
} else {
tags[k] = context.cleanTagValue(v);
}
});
+24 -23
View File
@@ -27,7 +27,7 @@ export function uiCommitWarnings(context) {
containerEnter
.append('h3')
.text(severity === 'warning' ? t('commit.warnings') : t('commit.errors'));
.html(severity === 'warning' ? t.html('commit.warnings') : t.html('commit.errors'));
containerEnter
.append('ul')
@@ -47,28 +47,8 @@ export function uiCommitWarnings(context) {
.append('li')
.attr('class', issueItem);
itemsEnter
.call(svgIcon('#iD-icon-alert', 'pre-text'));
itemsEnter
.append('strong')
.attr('class', 'issue-message');
itemsEnter.filter(function(d) { return d.tooltip; })
.call(uiTooltip()
.title(function(d) { return d.tooltip; })
.placement('top')
);
items = itemsEnter
.merge(items);
items.selectAll('.issue-message')
.text(function(d) {
return d.message(context);
});
items
var buttons = itemsEnter
.append('button')
.on('mouseover', function(d) {
if (d.entityIds) {
context.surface().selectAll(
@@ -86,6 +66,27 @@ export function uiCommitWarnings(context) {
.on('click', function(d) {
context.validator().focusIssue(d);
});
buttons
.call(svgIcon('#iD-icon-alert', 'pre-text'));
buttons
.append('strong')
.attr('class', 'issue-message');
buttons.filter(function(d) { return d.tooltip; })
.call(uiTooltip()
.title(function(d) { return d.tooltip; })
.placement('top')
);
items = itemsEnter
.merge(items);
items.selectAll('.issue-message')
.html(function(d) {
return d.message(context);
});
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export function uiConfirm(selection) {
.on('click.confirm', function() {
modalSelection.remove();
})
.text(t('confirm.okay'))
.html(t.html('confirm.okay'))
.node()
.focus();
+10 -10
View File
@@ -66,7 +66,7 @@ export function uiConflicts(context) {
headerEnter
.append('h3')
.text(t('save.conflict.header'));
.html(t.html('save.conflict.header'));
var bodyEnter = selection.selectAll('.body')
.data([0])
@@ -77,7 +77,7 @@ export function uiConflicts(context) {
var conflictsHelpEnter = bodyEnter
.append('div')
.attr('class', 'conflicts-help')
.text(t('save.conflict.help'));
.html(t.html('save.conflict.help'));
// Download changes link
@@ -110,7 +110,7 @@ export function uiConflicts(context) {
linkEnter
.call(svgIcon('#iD-icon-load', 'inline'))
.append('span')
.text(t('save.conflict.download_changes'));
.html(t.html('save.conflict.download_changes'));
bodyEnter
@@ -123,7 +123,7 @@ export function uiConflicts(context) {
.attr('class', 'conflicts-done')
.attr('opacity', 0)
.style('display', 'none')
.text(t('save.conflict.done'));
.html(t.html('save.conflict.done'));
var buttonsEnter = bodyEnter
.append('div')
@@ -133,13 +133,13 @@ export function uiConflicts(context) {
.append('button')
.attr('disabled', _conflictList.length > 1)
.attr('class', 'action conflicts-button col6')
.text(t('save.title'))
.html(t.html('save.title'))
.on('click.try_again', tryAgain);
buttonsEnter
.append('button')
.attr('class', 'secondary-action conflicts-button col6')
.text(t('confirm.cancel'))
.html(t.html('confirm.cancel'))
.on('click.cancel', cancel);
}
@@ -177,13 +177,13 @@ export function uiConflicts(context) {
conflictEnter
.append('h4')
.attr('class', 'conflict-count')
.text(t('save.conflict.count', { num: index + 1, total: _conflictList.length }));
.html(t.html('save.conflict.count', { num: index + 1, total: _conflictList.length }));
conflictEnter
.append('a')
.attr('class', 'conflict-description')
.attr('href', '#')
.text(function(d) { return d.name; })
.html(function(d) { return d.name; })
.on('click', function(d) {
d3_event.preventDefault();
zoomToEntity(d.id);
@@ -215,7 +215,7 @@ export function uiConflicts(context) {
.data(['previous', 'next'])
.enter()
.append('button')
.text(function(d) { return t('save.conflict.' + d); })
.html(function(d) { return t.html('save.conflict.' + d); })
.attr('class', 'conflict-nav-button action col6')
.attr('disabled', function(d, i) {
return (i === 0 && index === 0) ||
@@ -265,7 +265,7 @@ export function uiConflicts(context) {
labelEnter
.append('span')
.text(function(d) { return d.text; });
.html(function(d) { return d.text; });
// update
choicesEnter
+6 -4
View File
@@ -39,24 +39,26 @@ export function uiContributors(context) {
.attr('class', 'user-link')
.attr('href', function(d) { return osm.userURL(d); })
.attr('target', '_blank')
.text(String);
.html(String);
if (u.length > limit) {
var count = d3_select(document.createElement('span'));
var othersNum = u.length - limit + 1;
count.append('a')
.attr('target', '_blank')
.attr('href', function() {
return osm.changesetsURL(context.map().center(), context.map().zoom());
})
.text(u.length - limit + 1);
.html(othersNum);
wrap.append('span')
.html(t('contributors.truncated_list', { users: userList.html(), count: count.html() }));
.html(t.html('contributors.truncated_list', { n: othersNum, users: userList.html(), count: count.html() }));
} else {
wrap.append('span')
.html(t('contributors.list', { users: userList.html() }));
.html(t.html('contributors.list', { users: userList.html() }));
}
if (!u.length) {
+1 -1
View File
@@ -33,7 +33,7 @@ export function uiDataEditor(context) {
headerEnter
.append('h3')
.text(t('map_data.title'));
.html(t.html('map_data.title'));
var body = selection.selectAll('.body')
+1 -1
View File
@@ -32,7 +32,7 @@ export function uiDataHeader() {
headerEnter
.append('div')
.attr('class', 'data-header-label')
.text(t('map_data.layers.custom.title'));
.html(t.html('map_data.layers.custom.title'));
}
+5 -5
View File
@@ -12,7 +12,7 @@ import { localizer } from '../core/localizer';
export function uiDisclosure(context, key, expandedDefault) {
var dispatch = d3_dispatch('toggled');
var _expanded;
var _title = utilFunctor('');
var _label = utilFunctor('');
var _updatePreference = true;
var _content = function () {};
@@ -49,7 +49,7 @@ export function uiDisclosure(context, key, expandedDefault) {
.classed('expanded', _expanded);
hideToggle.selectAll('.hide-toggle-text')
.text(_title());
.html(_label());
hideToggle.selectAll('.hide-toggle-icon')
.attr('xlink:href', _expanded ? '#iD-icon-down'
@@ -103,9 +103,9 @@ export function uiDisclosure(context, key, expandedDefault) {
};
disclosure.title = function(val) {
if (!arguments.length) return _title;
_title = utilFunctor(val);
disclosure.label = function(val) {
if (!arguments.length) return _label;
_label = utilFunctor(val);
return disclosure;
};
+19 -3
View File
@@ -5,6 +5,7 @@ import { geoVecAdd } from '../geo';
import { localizer } from '../core/localizer';
import { uiTooltip } from './tooltip';
import { utilRebind } from '../util/rebind';
import { utilHighlightEntities } from '../util/util';
import { svgIcon } from '../svg/icon';
@@ -89,6 +90,16 @@ export function uiEditMenu(context) {
.on('pointerdown mousedown', function pointerdown() {
// don't let button presses also act as map input - #1869
d3_event.stopPropagation();
})
.on('mouseenter.highlight', function(d) {
if (!d.relatedEntityIds || d3_select(this).classed('disabled')) return;
utilHighlightEntities(d.relatedEntityIds(), true, context);
})
.on('mouseleave.highlight', function(d) {
if (!d.relatedEntityIds) return;
utilHighlightEntities(d.relatedEntityIds(), false, context);
});
buttonsEnter.each(function(d) {
@@ -109,7 +120,7 @@ export function uiEditMenu(context) {
if (showLabels) {
buttonsEnter.append('span')
.attr('class', 'label')
.text(function(d) {
.html(function(d) {
return d.title;
});
}
@@ -140,6 +151,11 @@ export function uiEditMenu(context) {
function click(operation) {
d3_event.stopPropagation();
if (operation.relatedEntityIds) {
utilHighlightEntities(operation.relatedEntityIds(), false, context);
}
if (operation.disabled()) {
if (lastPointerUpType === 'touch' ||
lastPointerUpType === 'pen') {
@@ -148,7 +164,7 @@ export function uiEditMenu(context) {
.duration(4000)
.iconName('#iD-operation-' + operation.id)
.iconClass('operation disabled')
.text(operation.tooltip)();
.label(operation.tooltip)();
}
} else {
if (lastPointerUpType === 'touch' ||
@@ -157,7 +173,7 @@ export function uiEditMenu(context) {
.duration(2000)
.iconName('#iD-operation-' + operation.id)
.iconClass('operation')
.text(operation.annotation() || operation.title)();
.label(operation.annotation() || operation.title)();
}
operation();
+2 -21
View File
@@ -1,5 +1,4 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { event as d3_event } from 'd3-selection';
import deepEqual from 'fast-deep-equal';
import { presetManager } from '../presets';
@@ -41,7 +40,7 @@ export function uiEntityEditor(context) {
// Enter
var headerEnter = header.enter()
.append('div')
.attr('class', 'header fillL cf');
.attr('class', 'header fillL');
headerEnter
.append('button')
@@ -62,7 +61,7 @@ export function uiEntityEditor(context) {
.merge(headerEnter);
header.selectAll('h3')
.text(_entityIDs.length === 1 ? t('inspector.edit') : t('inspector.edit_features'));
.html(_entityIDs.length === 1 ? t.html('inspector.edit') : t.html('inspector.edit_features'));
header.selectAll('.preset-reset')
.on('click', function() {
@@ -112,24 +111,6 @@ export function uiEntityEditor(context) {
body.call(section.render);
});
body
.selectAll('.key-trap-wrap')
.data([0])
.enter()
.append('div')
.attr('class', 'key-trap-wrap')
.append('input')
.attr('type', 'text')
.attr('class', 'key-trap')
.on('keydown.key-trap', function() {
// On tabbing, send focus back to the first field on the inspector-body
// (probably the `name` field) #4159
if (d3_event.keyCode === 9 && !d3_event.shiftKey) {
d3_event.preventDefault();
body.select('input').node().focus();
}
});
context.history()
.on('change.entity-editor', historyChanged);
+2 -3
View File
@@ -12,7 +12,7 @@ export function uiFeatureInfo(context) {
var hiddenList = features.hidden().map(function(k) {
if (stats[k]) {
count += stats[k];
return String(stats[k]) + ' ' + t('feature.' + k + '.description');
return t('inspector.title_count', { title: t.html('feature.' + k + '.description'), count: stats[k] });
}
}).filter(Boolean);
@@ -28,8 +28,7 @@ export function uiFeatureInfo(context) {
selection.append('a')
.attr('class', 'chip')
.attr('href', '#')
.attr('tabindex', -1)
.html(t('feature_info.hidden_warning', { count: count }))
.html(t.html('feature_info.hidden_warning', { count: count }))
.call(tooltipBehavior)
.on('click', function() {
tooltipBehavior.hide();
+28 -25
View File
@@ -31,16 +31,19 @@ export function uiFeatureList(context) {
function featureList(selection) {
var header = selection
.append('div')
.attr('class', 'header fillL cf');
.attr('class', 'header fillL');
header
.append('h3')
.text(t('inspector.feature_list'));
.html(t.html('inspector.feature_list'));
var searchWrap = selection
.append('div')
.attr('class', 'search-header');
searchWrap
.call(svgIcon('#iD-icon-search', 'pre-text'));
var search = searchWrap
.append('input')
.attr('placeholder', t('inspector.search'))
@@ -50,16 +53,13 @@ export function uiFeatureList(context) {
.on('keydown', keydown)
.on('input', inputevent);
searchWrap
.call(svgIcon('#iD-icon-search', 'pre-text'));
var listWrap = selection
.append('div')
.attr('class', 'inspector-body');
var list = listWrap
.append('div')
.attr('class', 'feature-list cf');
.attr('class', 'feature-list');
context
.on('exit.feature-list', clearSearch);
@@ -89,7 +89,9 @@ export function uiFeatureList(context) {
function keypress() {
var q = search.property('value'),
items = list.selectAll('.feature-list-item');
if (d3_event.keyCode === 13 && q.length && items.size()) { // return
if (d3_event.keyCode === 13 && // ↩ Return
q.length &&
items.size()) {
click(items.datum());
}
}
@@ -122,19 +124,6 @@ export function uiFeatureList(context) {
if (!q) return result;
var idMatch = q.match(/(?:^|\W)(node|way|relation|[nwr])\W?0*([1-9]\d*)(?:\W|$)/i);
if (idMatch) {
var elemType = idMatch[1].charAt(0);
var elemId = idMatch[2];
result.push({
id: elemType + elemId,
geometry: elemType === 'n' ? 'point' : elemType === 'w' ? 'line' : 'relation',
type: elemType === 'n' ? t('inspector.node') : elemType === 'w' ? t('inspector.way') : t('inspector.relation'),
name: elemId
});
}
var locationMatch = sexagesimal.pair(q.toUpperCase()) || q.match(/^(-?\d+\.?\d*)\s+(-?\d+\.?\d*)$/);
if (locationMatch) {
@@ -148,6 +137,20 @@ export function uiFeatureList(context) {
});
}
// A location search takes priority over an ID search
var idMatch = !locationMatch && q.match(/(?:^|\W)(node|way|relation|[nwr])\W?0*([1-9]\d*)(?:\W|$)/i);
if (idMatch) {
var elemType = idMatch[1].charAt(0);
var elemId = idMatch[2];
result.push({
id: elemType + elemId,
geometry: elemType === 'n' ? 'point' : elemType === 'w' ? 'line' : 'relation',
type: elemType === 'n' ? t('inspector.node') : elemType === 'w' ? t('inspector.way') : t('inspector.relation'),
name: elemId
});
}
var allEntities = graph.entities;
var localResults = [];
for (var id in allEntities) {
@@ -253,20 +256,20 @@ export function uiFeatureList(context) {
.attr('class', 'entity-name');
list.selectAll('.no-results-item .entity-name')
.text(t('geocoder.no_results_worldwide'));
.html(t.html('geocoder.no_results_worldwide'));
if (services.geocoder) {
list.selectAll('.geocode-item')
.data([0])
.enter()
.append('button')
.attr('class', 'geocode-item')
.attr('class', 'geocode-item secondary-action')
.on('click', geocoderSearch)
.append('div')
.attr('class', 'label')
.append('span')
.attr('class', 'entity-name')
.text(t('geocoder.search'));
.html(t.html('geocoder.search'));
}
list.selectAll('.no-results-item')
@@ -302,12 +305,12 @@ export function uiFeatureList(context) {
label
.append('span')
.attr('class', 'entity-type')
.text(function(d) { return d.type; });
.html(function(d) { return d.type; });
label
.append('span')
.attr('class', 'entity-name')
.text(function(d) { return d.name; });
.html(function(d) { return d.name; });
enter
.style('opacity', 0)
+3 -5
View File
@@ -30,7 +30,7 @@ export function uiField(context, presetField, entityIDs, options) {
var _locked = false;
var _lockedTip = uiTooltip()
.title(t('inspector.lock.suggestion', { label: field.label }))
.title(t.html('inspector.lock.suggestion', { label: field.label }))
.placement('bottom');
@@ -132,7 +132,7 @@ export function uiField(context, presetField, entityIDs, options) {
textEnter
.append('span')
.attr('class', 'label-textvalue')
.text(function(d) { return d.label(); });
.html(function(d) { return d.label(); });
textEnter
.append('span')
@@ -143,7 +143,6 @@ export function uiField(context, presetField, entityIDs, options) {
.append('button')
.attr('class', 'remove-icon')
.attr('title', t('icons.remove'))
.attr('tabindex', -1)
.call(svgIcon('#iD-operation-delete'));
}
@@ -152,7 +151,6 @@ export function uiField(context, presetField, entityIDs, options) {
.append('button')
.attr('class', 'modified-icon')
.attr('title', t('icons.undo'))
.attr('tabindex', -1)
.call(svgIcon((localizer.textDirection() === 'rtl') ? '#iD-icon-redo' : '#iD-icon-undo'));
}
}
@@ -185,7 +183,7 @@ export function uiField(context, presetField, entityIDs, options) {
// instantiate tag reference
if (options.wrap && options.info) {
var referenceKey = d.key;
var referenceKey = d.key || '';
if (d.type === 'multiCombo') { // lookup key without the trailing ':'
referenceKey = referenceKey.replace(/:$/, '');
}
+13 -14
View File
@@ -55,15 +55,15 @@ export function uiFieldHelp(context, fieldName) {
var fieldHelpHeadings = {};
var replacements = {
distField: t('restriction.controls.distance'),
viaField: t('restriction.controls.via'),
fromShadow: icon('#iD-turn-shadow', 'pre-text shadow from'),
allowShadow: icon('#iD-turn-shadow', 'pre-text shadow allow'),
restrictShadow: icon('#iD-turn-shadow', 'pre-text shadow restrict'),
onlyShadow: icon('#iD-turn-shadow', 'pre-text shadow only'),
allowTurn: icon('#iD-turn-yes', 'pre-text turn'),
restrictTurn: icon('#iD-turn-no', 'pre-text turn'),
onlyTurn: icon('#iD-turn-only', 'pre-text turn')
distField: t.html('restriction.controls.distance'),
viaField: t.html('restriction.controls.via'),
fromShadow: icon('#iD-turn-shadow', 'inline shadow from'),
allowShadow: icon('#iD-turn-shadow', 'inline shadow allow'),
restrictShadow: icon('#iD-turn-shadow', 'inline shadow restrict'),
onlyShadow: icon('#iD-turn-shadow', 'inline shadow only'),
allowTurn: icon('#iD-turn-yes', 'inline turn'),
restrictTurn: icon('#iD-turn-no', 'inline turn'),
onlyTurn: icon('#iD-turn-only', 'inline turn')
};
@@ -74,12 +74,12 @@ export function uiFieldHelp(context, fieldName) {
var subkey = helpkey + '.' + part;
var depth = fieldHelpHeadings[subkey]; // is this subkey a heading?
var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s
return all + hhh + t(subkey, replacements) + '\n\n';
return all + hhh + t.html(subkey, replacements) + '\n\n';
}, '');
return {
key: helpkey,
title: t(helpkey + '.title'),
title: t.html(helpkey + '.title'),
html: marked(text.trim())
};
});
@@ -149,7 +149,6 @@ export function uiFieldHelp(context, fieldName) {
button.enter()
.append('button')
.attr('class', 'field-help-button')
.attr('tabindex', -1)
.call(svgIcon('#iD-icon-help'))
.merge(button)
.on('click', function () {
@@ -198,7 +197,7 @@ export function uiFieldHelp(context, fieldName) {
titleEnter
.append('h2')
.attr('class', ((localizer.textDirection() === 'rtl') ? 'fr' : 'fl'))
.text(t('help.field.' + fieldName + '.title'));
.html(t.html('help.field.' + fieldName + '.title'));
titleEnter
.append('button')
@@ -220,7 +219,7 @@ export function uiFieldHelp(context, fieldName) {
.enter()
.append('div')
.attr('class', 'field-help-nav-item')
.text(function(d) { return d; })
.html(function(d) { return d; })
.on('click', function(d, i) {
d3_event.stopPropagation();
d3_event.preventDefault();
+1 -1
View File
@@ -40,7 +40,7 @@ export function uiFieldAccess(field, context) {
.append('span')
.attr('class', 'label preset-label-access')
.attr('for', function(d) { return 'preset-input-access-' + d; })
.text(function(d) { return field.t('types.' + d); });
.html(function(d) { return field.t.html('types.' + d); });
enter
.append('div')
+10 -10
View File
@@ -36,14 +36,14 @@ export function uiFieldCheck(field, context) {
if (options) {
for (var k in options) {
values.push(k === 'undefined' ? undefined : k);
texts.push(field.t('options.' + k, { 'default': options[k] }));
texts.push(field.t.html('options.' + k, { 'default': options[k] }));
}
} else {
values = [undefined, 'yes'];
texts = [t('inspector.unknown'), t('inspector.check.yes')];
texts = [t.html('inspector.unknown'), t.html('inspector.check.yes')];
if (field.type !== 'defaultCheck') {
values.push('no');
texts.push(t('inspector.check.no'));
texts.push(t.html('inspector.check.no'));
}
}
@@ -59,7 +59,7 @@ export function uiFieldCheck(field, context) {
for (var key in entity.tags) {
if (key in osmOneWayTags && (entity.tags[key] in osmOneWayTags[key])) {
_impliedYes = true;
texts[0] = t('presets.fields.oneway_yes.options.undefined');
texts[0] = t.html('presets.fields.oneway_yes.options.undefined');
break;
}
}
@@ -83,7 +83,7 @@ export function uiFieldCheck(field, context) {
var icon = pseudoDirection ? '#iD-icon-forward' : '#iD-icon-backward';
selection.selectAll('.reverser-span')
.text(t('inspector.check.reverser'))
.html(t.html('inspector.check.reverser'))
.call(svgIcon(icon, 'inline'));
return selection;
@@ -108,13 +108,13 @@ export function uiFieldCheck(field, context) {
enter
.append('span')
.text(texts[0])
.html(texts[0])
.attr('class', 'value');
if (field.type === 'onewayCheck') {
enter
.append('a')
.attr('class', 'reverser button' + (reverserHidden() ? ' hide' : ''))
.append('button')
.attr('class', 'reverser' + (reverserHidden() ? ' hide' : ''))
.attr('href', '#')
.append('span')
.attr('class', 'reverser-span');
@@ -163,7 +163,7 @@ export function uiFieldCheck(field, context) {
}
return graph;
},
t('operations.reverse.annotation')
t('operations.reverse.annotation.line', { n: 1 })
);
// must manually revalidate since no 'change' event was called
@@ -212,7 +212,7 @@ export function uiFieldCheck(field, context) {
.property('checked', isChecked(_value));
text
.text(isMixed ? t('inspector.multiple_values') : textFor(_value))
.html(isMixed ? t.html('inspector.multiple_values') : textFor(_value))
.classed('mixed', isMixed);
label
+110 -104
View File
@@ -11,6 +11,7 @@ import { uiCombobox } from '../combobox';
import { utilArrayUniq, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent, utilUnicodeCharsCount } from '../../util';
export {
uiFieldCombo as uiFieldManyCombo,
uiFieldCombo as uiFieldMultiCombo,
uiFieldCombo as uiFieldNetworkCombo,
uiFieldCombo as uiFieldSemiCombo,
@@ -20,20 +21,18 @@ export {
export function uiFieldCombo(field, context) {
var dispatch = d3_dispatch('change');
var taginfo = services.taginfo;
var isMulti = (field.type === 'multiCombo');
var isNetwork = (field.type === 'networkCombo');
var isSemi = (field.type === 'semiCombo');
var optstrings = field.strings && field.strings.options;
var optarray = field.options;
var snake_case = (field.snake_case || (field.snake_case === undefined));
var caseSensitive = field.caseSensitive;
var combobox = uiCombobox(context, 'combo-' + field.safeid)
.caseSensitive(caseSensitive)
.minItems(isMulti || isSemi ? 1 : 2);
var container = d3_select(null);
var inputWrap = d3_select(null);
var input = d3_select(null);
var _isMulti = (field.type === 'multiCombo' || field.type === 'manyCombo');
var _isNetwork = (field.type === 'networkCombo');
var _isSemi = (field.type === 'semiCombo');
var _optstrings = field.strings && field.strings.options;
var _optarray = field.options;
var _snake_case = (field.snake_case || (field.snake_case === undefined));
var _combobox = uiCombobox(context, 'combo-' + field.safeid)
.caseSensitive(field.caseSensitive)
.minItems(_isMulti || _isSemi ? 1 : 2);
var _container = d3_select(null);
var _inputWrap = d3_select(null);
var _input = d3_select(null);
var _comboData = [];
var _multiData = [];
var _entityIDs = [];
@@ -49,7 +48,7 @@ export function uiFieldCombo(field, context) {
// ensure multiCombo field.key ends with a ':'
if (isMulti && /[^:]$/.test(field.key)) {
if (_isMulti && field.key && /[^:]$/.test(field.key)) {
field.key += ':';
}
@@ -74,7 +73,7 @@ export function uiFieldCombo(field, context) {
function tagValue(dval) {
dval = clean(dval || '');
if (optstrings) {
if (_optstrings) {
var found = _comboData.find(function(o) {
return o.key && clean(o.value) === dval;
});
@@ -87,7 +86,7 @@ export function uiFieldCombo(field, context) {
return 'yes';
}
return (snake_case ? snake(dval) : dval) || undefined;
return (_snake_case ? snake(dval) : dval) || undefined;
}
@@ -96,7 +95,7 @@ export function uiFieldCombo(field, context) {
function displayValue(tval) {
tval = tval || '';
if (optstrings) {
if (_optstrings) {
var found = _comboData.find(function(o) {
return o.key === tval && o.value;
});
@@ -109,7 +108,7 @@ export function uiFieldCombo(field, context) {
return '';
}
return snake_case ? unsnake(tval) : tval;
return _snake_case ? unsnake(tval) : tval;
}
@@ -128,38 +127,39 @@ export function uiFieldCombo(field, context) {
function initCombo(selection, attachTo) {
if (optstrings) {
if (_optstrings) {
selection.attr('readonly', 'readonly');
selection.call(combobox, attachTo);
selection.call(_combobox, attachTo);
setStaticValues(setPlaceholder);
} else if (optarray) {
selection.call(combobox, attachTo);
} else if (_optarray) {
selection.call(_combobox, attachTo);
setStaticValues(setPlaceholder);
} else if (taginfo) {
selection.call(combobox.fetcher(setTaginfoValues), attachTo);
} else if (services.taginfo) {
selection.call(_combobox.fetcher(setTaginfoValues), attachTo);
setTaginfoValues('', setPlaceholder);
}
}
function setStaticValues(callback) {
if (!(optstrings || optarray)) return;
if (!(_optstrings || _optarray)) return;
if (optstrings) {
_comboData = Object.keys(optstrings).map(function(k) {
var v = field.t('options.' + k, { 'default': optstrings[k] });
if (_optstrings) {
_comboData = Object.keys(_optstrings).map(function(k) {
var v = field.t('options.' + k, { 'default': _optstrings[k] });
return {
key: k,
value: v,
title: v
title: v,
display: field.t.html('options.' + k, { 'default': _optstrings[k] })
};
});
} else if (optarray) {
_comboData = optarray.map(function(k) {
var v = snake_case ? unsnake(k) : k;
} else if (_optarray) {
_comboData = _optarray.map(function(k) {
var v = _snake_case ? unsnake(k) : k;
return {
key: k,
value: v,
@@ -168,15 +168,15 @@ export function uiFieldCombo(field, context) {
});
}
combobox.data(objectDifference(_comboData, _multiData));
_combobox.data(objectDifference(_comboData, _multiData));
if (callback) callback(_comboData);
}
function setTaginfoValues(q, callback) {
var fn = isMulti ? 'multikeys' : 'values';
var query = (isMulti ? field.key : '') + q;
var hasCountryPrefix = isNetwork && _countryCode && _countryCode.indexOf(q.toLowerCase()) === 0;
var fn = _isMulti ? 'multikeys' : 'values';
var query = (_isMulti ? field.key : '') + q;
var hasCountryPrefix = _isNetwork && _countryCode && _countryCode.indexOf(q.toLowerCase()) === 0;
if (hasCountryPrefix) {
query = _countryCode + ':';
}
@@ -191,7 +191,7 @@ export function uiFieldCombo(field, context) {
params.geometry = context.graph().geometry(_entityIDs[0]);
}
taginfo[fn](params, function(err, data) {
services.taginfo[fn](params, function(err, data) {
if (err) return;
data = data.filter(function(d) {
@@ -220,16 +220,16 @@ export function uiFieldCombo(field, context) {
}
// hide the caret if there are no suggestions
container.classed('empty-combobox', data.length === 0);
_container.classed('empty-combobox', data.length === 0);
_comboData = data.map(function(d) {
var k = d.value;
if (isMulti) k = k.replace(field.key, '');
var v = snake_case ? unsnake(k) : k;
if (_isMulti) k = k.replace(field.key, '');
var v = _snake_case ? unsnake(k) : k;
return {
key: k,
value: v,
title: isMulti ? v : d.title
title: _isMulti ? v : d.title
};
});
@@ -241,7 +241,7 @@ export function uiFieldCombo(field, context) {
function setPlaceholder(values) {
if (isMulti || isSemi) {
if (_isMulti || _isSemi) {
_staticPlaceholder = field.placeholder() || t('inspector.add');
} else {
var vals = values
@@ -257,13 +257,13 @@ export function uiFieldCombo(field, context) {
}
var ph;
if (!isMulti && !isSemi && _tags && Array.isArray(_tags[field.key])) {
if (!_isMulti && !_isSemi && _tags && Array.isArray(_tags[field.key])) {
ph = t('inspector.multiple_values');
} else {
ph = _staticPlaceholder;
}
container.selectAll('input')
_container.selectAll('input')
.attr('placeholder', ph);
}
@@ -272,17 +272,17 @@ export function uiFieldCombo(field, context) {
var t = {};
var val;
if (isMulti || isSemi) {
val = tagValue(utilGetSetValue(input).replace(/,/g, ';')) || '';
container.classed('active', false);
utilGetSetValue(input, '');
if (_isMulti || _isSemi) {
val = tagValue(utilGetSetValue(_input).replace(/,/g, ';')) || '';
_container.classed('active', false);
utilGetSetValue(_input, '');
var vals = val.split(';').filter(Boolean);
if (!vals.length) return;
if (isMulti) {
if (_isMulti) {
utilArrayUniq(vals).forEach(function(v) {
var key = field.key + v;
var key = (field.key || '') + v;
if (_tags) {
// don't set a multicombo value to 'yes' if it already has a non-'no' value
// e.g. `language:de=main`
@@ -294,16 +294,16 @@ export function uiFieldCombo(field, context) {
t[key] = 'yes';
});
} else if (isSemi) {
} else if (_isSemi) {
var arr = _multiData.map(function(d) { return d.key; });
arr = arr.concat(vals);
t[field.key] = context.cleanTagValue(utilArrayUniq(arr).filter(Boolean).join(';'));
}
window.setTimeout(function() { input.node().focus(); }, 10);
window.setTimeout(function() { _input.node().focus(); }, 10);
} else {
var rawValue = utilGetSetValue(input);
var rawValue = utilGetSetValue(_input);
// don't override multiple values with blank string
if (!rawValue && Array.isArray(_tags[field.key])) return;
@@ -317,11 +317,12 @@ export function uiFieldCombo(field, context) {
function removeMultikey(d) {
d3_event.preventDefault();
d3_event.stopPropagation();
var t = {};
if (isMulti) {
if (_isMulti) {
t[d.key] = undefined;
} else if (isSemi) {
} else if (_isSemi) {
var arr = _multiData.map(function(md) {
return md.key === d.key ? null : md.key;
}).filter(Boolean);
@@ -334,17 +335,17 @@ export function uiFieldCombo(field, context) {
function combo(selection) {
container = selection.selectAll('.form-field-input-wrap')
_container = selection.selectAll('.form-field-input-wrap')
.data([0]);
var type = (isMulti || isSemi) ? 'multicombo': 'combo';
container = container.enter()
var type = (_isMulti || _isSemi) ? 'multicombo': 'combo';
_container = _container.enter()
.append('div')
.attr('class', 'form-field-input-wrap form-field-input-' + type)
.merge(container);
.merge(_container);
if (isMulti || isSemi) {
container = container.selectAll('.chiplist')
if (_isMulti || _isSemi) {
_container = _container.selectAll('.chiplist')
.data([0]);
var listClass = 'chiplist';
@@ -355,67 +356,67 @@ export function uiFieldCombo(field, context) {
listClass += ' full-line-chips';
}
container = container.enter()
_container = _container.enter()
.append('ul')
.attr('class', listClass)
.on('click', function() {
window.setTimeout(function() { input.node().focus(); }, 10);
window.setTimeout(function() { _input.node().focus(); }, 10);
})
.merge(container);
.merge(_container);
inputWrap = container.selectAll('.input-wrap')
_inputWrap = _container.selectAll('.input-wrap')
.data([0]);
inputWrap = inputWrap.enter()
_inputWrap = _inputWrap.enter()
.append('li')
.attr('class', 'input-wrap')
.merge(inputWrap);
.merge(_inputWrap);
input = inputWrap.selectAll('input')
_input = _inputWrap.selectAll('input')
.data([0]);
} else {
input = container.selectAll('input')
_input = _container.selectAll('input')
.data([0]);
}
input = input.enter()
_input = _input.enter()
.append('input')
.attr('type', 'text')
.attr('id', field.domId)
.call(utilNoAuto)
.call(initCombo, selection)
.merge(input);
.merge(_input);
if (isNetwork) {
if (_isNetwork) {
var extent = combinedEntityExtent();
var countryCode = extent && countryCoder.iso1A2Code(extent.center());
_countryCode = countryCode && countryCode.toLowerCase();
}
input
_input
.on('change', change)
.on('blur', change);
input
_input
.on('keydown.field', function() {
switch (d3_event.keyCode) {
case 13: // ↩ Return
input.node().blur(); // blurring also enters the value
_input.node().blur(); // blurring also enters the value
d3_event.stopPropagation();
break;
}
});
if (isMulti || isSemi) {
combobox
if (_isMulti || _isSemi) {
_combobox
.on('accept', function() {
input.node().blur();
input.node().focus();
_input.node().blur();
_input.node().focus();
});
input
.on('focus', function() { container.classed('active', true); });
_input
.on('focus', function() { _container.classed('active', true); });
}
}
@@ -423,19 +424,19 @@ export function uiFieldCombo(field, context) {
combo.tags = function(tags) {
_tags = tags;
if (isMulti || isSemi) {
if (_isMulti || _isSemi) {
_multiData = [];
var maxLength;
if (isMulti) {
if (_isMulti) {
// Build _multiData array containing keys already set..
for (var k in tags) {
if (k.indexOf(field.key) !== 0) continue;
if ((field.key && k.indexOf(field.key) !== 0) || field.keys.indexOf(k) === -1) continue;
var v = tags[k];
if (!v || (typeof v === 'string' && v.toLowerCase() === 'no')) continue;
var suffix = k.substring(field.key.length);
var suffix = field.key ? k.substring(field.key.length) : k;
_multiData.push({
key: k,
value: displayValue(suffix),
@@ -443,13 +444,17 @@ export function uiFieldCombo(field, context) {
});
}
// Set keys for form-field modified (needed for undo and reset buttons)..
field.keys = _multiData.map(function(d) { return d.key; });
if (field.key) {
// Set keys for form-field modified (needed for undo and reset buttons)..
field.keys = _multiData.map(function(d) { return d.key; });
// limit the input length so it fits after prepending the key prefix
maxLength = context.maxCharsForTagKey() - utilUnicodeCharsCount(field.key);
// limit the input length so it fits after prepending the key prefix
maxLength = context.maxCharsForTagKey() - utilUnicodeCharsCount(field.key);
} else {
maxLength = context.maxCharsForTagKey();
}
} else if (isSemi) {
} else if (_isSemi) {
var allValues = [];
var commonValues;
@@ -492,23 +497,23 @@ export function uiFieldCombo(field, context) {
// a negative maxlength doesn't make sense
maxLength = Math.max(0, maxLength);
var allowDragAndDrop = isSemi // only semiCombo values are ordered
var allowDragAndDrop = _isSemi // only semiCombo values are ordered
&& !Array.isArray(tags[field.key]);
// Exclude existing multikeys from combo options..
var available = objectDifference(_comboData, _multiData);
combobox.data(available);
_combobox.data(available);
// Hide 'Add' button if this field uses fixed set of
// translateable optstrings and they're all currently used,
// translateable _optstrings and they're all currently used,
// or if the field is already at its character limit
var hideAdd = (optstrings && !available.length) || maxLength <= 0;
container.selectAll('.chiplist .input-wrap')
var hideAdd = (_optstrings && !available.length) || maxLength <= 0;
_container.selectAll('.chiplist .input-wrap')
.style('display', hideAdd ? 'none' : null);
// Render chips
var chips = container.selectAll('.chip')
var chips = _container.selectAll('.chip')
.data(_multiData);
chips.exit()
@@ -536,12 +541,13 @@ export function uiFieldCombo(field, context) {
}
chips.select('span')
.text(function(d) { return d.value; });
.html(function(d) { return d.value; });
chips.select('a')
.attr('href', '#')
.on('click', removeMultikey)
.attr('class', 'remove')
.text('×');
.html('×');
} else {
var isMixed = Array.isArray(tags[field.key]);
@@ -550,7 +556,7 @@ export function uiFieldCombo(field, context) {
return displayValue(val);
}).filter(Boolean);
utilGetSetValue(input, !isMixed ? displayValue(tags[field.key]) : '')
utilGetSetValue(_input, !isMixed ? displayValue(tags[field.key]) : '')
.attr('title', isMixed ? mixedValues.join('\n') : undefined)
.attr('placeholder', isMixed ? t('inspector.multiple_values') : _staticPlaceholder || '')
.classed('mixed', isMixed);
@@ -585,7 +591,7 @@ export function uiFieldCombo(field, context) {
var draggedTagWidth = d3_select(this).node().offsetWidth;
if (field.key === 'destination') { // meaning tags are full width
container.selectAll('.chip')
_container.selectAll('.chip')
.style('transform', function(d2, index2) {
var node = d3_select(this).node();
@@ -607,7 +613,7 @@ export function uiFieldCombo(field, context) {
return null;
});
} else {
container.selectAll('.chip')
_container.selectAll('.chip')
.each(function(d2, index2) {
var node = d3_select(this).node();
@@ -650,7 +656,7 @@ export function uiFieldCombo(field, context) {
d3_select(this)
.classed('dragging', false);
container.selectAll('.chip')
_container.selectAll('.chip')
.style('transform', null);
if (typeof targetIndex === 'number') {
@@ -678,7 +684,7 @@ export function uiFieldCombo(field, context) {
combo.focus = function() {
input.node().focus();
_input.node().focus();
};
+1 -1
View File
@@ -49,7 +49,7 @@ export function uiFieldCycleway(field, context) {
.append('span')
.attr('class', 'label preset-label-cycleway')
.attr('for', function(d) { return 'preset-input-cycleway-' + stripcolon(d); })
.text(function(d) { return field.t('types.' + d); });
.html(function(d) { return field.t.html('types.' + d); });
enter
.append('div')
+2
View File
@@ -21,6 +21,7 @@ import {
import {
uiFieldCombo,
uiFieldManyCombo,
uiFieldMultiCombo,
uiFieldNetworkCombo,
uiFieldSemiCombo,
@@ -64,6 +65,7 @@ export var uiFields = {
lanes: uiFieldLanes,
localized: uiFieldLocalized,
maxspeed: uiFieldMaxspeed,
manyCombo: uiFieldManyCombo,
multiCombo: uiFieldMultiCombo,
networkCombo: uiFieldNetworkCombo,
number: uiFieldNumber,
-2
View File
@@ -80,7 +80,6 @@ export function uiFieldText(field, context) {
buttons.enter()
.append('button')
.attr('tabindex', -1)
.attr('class', function(d) {
var which = (d === 1 ? 'increment' : 'decrement');
return 'form-field-button ' + which;
@@ -106,7 +105,6 @@ export function uiFieldText(field, context) {
outlinkButton.enter()
.append('button')
.attr('tabindex', -1)
.call(svgIcon('#iD-icon-out-link'))
.attr('class', 'form-field-button foreign-id-permalink')
.attr('title', function() {
+3 -3
View File
@@ -77,7 +77,7 @@ export function uiFieldLanes(field, context) {
.append('text')
.attr('y', 40)
.attr('x', 14)
.text('▲');
.html('▲');
enter
.append('g')
@@ -85,7 +85,7 @@ export function uiFieldLanes(field, context) {
.append('text')
.attr('y', 40)
.attr('x', 14)
.text('▲▼');
.html('▲▼');
enter
.append('g')
@@ -93,7 +93,7 @@ export function uiFieldLanes(field, context) {
.append('text')
.attr('y', 40)
.attr('x', 14)
.text('▼');
.html('▼');
lane = lane
+8 -22
View File
@@ -52,7 +52,7 @@ export function uiFieldLocalized(field, context) {
var _selection = d3_select(null);
var _multilingual = [];
var _buttonTip = uiTooltip()
.title(t('translate.translate'))
.title(t.html('translate.translate'))
.placement('left');
var _wikiTitles;
var _entityIDs = [];
@@ -219,7 +219,6 @@ export function uiFieldLocalized(field, context) {
translateButton = translateButton.enter()
.append('button')
.attr('class', 'localized-add form-field-button')
.attr('tabindex', -1)
.call(svgIcon('#iD-icon-plus'))
.merge(translateButton);
@@ -260,7 +259,6 @@ export function uiFieldLocalized(field, context) {
var preset = presetManager.match(latest, context.graph());
if (preset && preset.suggestion) return; // already accepted
// note: here we are testing against "decorated" names, i.e. 'Starbucks Cafe'
var name = utilGetSetValue(input).trim();
var matched = allSuggestions.filter(function(s) { return name === s.name(); });
@@ -293,25 +291,10 @@ export function uiFieldLocalized(field, context) {
}
// user hit escape, clean whatever preset name appears after the last ' '
// user hit escape
function cancelBrand() {
var name = utilGetSetValue(input);
var clean = cleanName(name);
if (clean !== name) {
utilGetSetValue(input, clean);
dispatch.call('change', this, { name: clean });
}
}
// Remove whatever is after the last ' '
// NOTE: split/join on en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc)
function cleanName(name) {
var parts = name.split(' ');
if (parts.length > 1) {
parts.pop();
name = parts.join(' ');
}
return name;
dispatch.call('change', this, { name: name });
}
@@ -333,14 +316,17 @@ export function uiFieldLocalized(field, context) {
var sTag = s.id.split('/', 2);
var sKey = sTag[0];
var sValue = sTag[1];
var subtitle = s.subtitle();
var name = s.name();
if (subtitle) name += ' ' + subtitle;
var dist = utilEditDistance(value, name.substring(0, value.length));
var matchesPreset = (pKey === sKey && (!pValue || pValue === sValue));
if (dist < 1 || (matchesPreset && dist < 3)) {
var obj = {
value: s.name(),
title: name,
value: name,
display: s.nameLabel() + (subtitle ? ' ' + s.subtitleLabel() : ''),
suggestion: s,
dist: dist + (matchesPreset ? 0 : 1) // penalize if not matched preset
};
@@ -513,7 +499,7 @@ export function uiFieldLocalized(field, context) {
text
.append('span')
.attr('class', 'label-textvalue')
.text(t('translate.localized_translation_label'));
.html(t.html('translate.localized_translation_label'));
text
.append('span')
+5 -5
View File
@@ -64,7 +64,7 @@ export function uiFieldRadio(field, context) {
enter
.append('span')
.text(function(d) { return field.t('options.' + d, { 'default': d }); });
.html(function(d) { return field.t.html('options.' + d, { 'default': d }); });
labels = labels
.merge(enter);
@@ -129,7 +129,7 @@ export function uiFieldRadio(field, context) {
.append('span')
.attr('class', 'label structure-label-type')
.attr('for', 'preset-input-' + selected)
.text(t('inspector.radio.structure.type'));
.html(t.html('inspector.radio.structure.type'));
typeEnter
.append('div')
@@ -174,7 +174,7 @@ export function uiFieldRadio(field, context) {
.append('span')
.attr('class', 'label structure-label-layer')
.attr('for', 'preset-input-layer')
.text(t('inspector.radio.structure.layer'));
.html(t.html('inspector.radio.structure.layer'));
layerEnter
.append('div')
@@ -297,9 +297,9 @@ export function uiFieldRadio(field, context) {
var selection = radios.filter(function() { return this.checked; });
if (selection.empty()) {
placeholder.text(t('inspector.none'));
placeholder.html(t.html('inspector.none'));
} else {
placeholder.text(selection.attr('value'));
placeholder.html(selection.attr('value'));
_oldType[selection.datum()] = tags[selection.datum()];
}
+22 -22
View File
@@ -124,7 +124,7 @@ export function uiFieldRestrictions(field, context) {
distControlEnter
.append('span')
.attr('class', 'restriction-control-label restriction-distance-label')
.text(t('restriction.controls.distance') + ':');
.html(t.html('restriction.controls.distance') + ':');
distControlEnter
.append('input')
@@ -151,7 +151,7 @@ export function uiFieldRestrictions(field, context) {
});
selection.selectAll('.restriction-distance-text')
.text(displayMaxDistance(_maxDistance));
.html(displayMaxDistance(_maxDistance));
var viaControl = selection.selectAll('.restriction-via-way')
@@ -167,7 +167,7 @@ export function uiFieldRestrictions(field, context) {
viaControlEnter
.append('span')
.attr('class', 'restriction-control-label restriction-via-way-label')
.text(t('restriction.controls.via') + ':');
.html(t.html('restriction.controls.via') + ':');
viaControlEnter
.append('input')
@@ -193,7 +193,7 @@ export function uiFieldRestrictions(field, context) {
});
selection.selectAll('.restriction-via-way-text')
.text(displayMaxVia(_maxViaWay));
.html(displayMaxVia(_maxViaWay));
}
@@ -487,7 +487,7 @@ export function uiFieldRestrictions(field, context) {
var clickSelect = (!_fromWayID || _fromWayID !== way.id);
help
.append('div') // "Click to select FROM {fromName}." / "FROM {fromName}"
.html(t('restriction.help.' + (clickSelect ? 'select_from_name' : 'from_name'), {
.html(t.html('restriction.help.' + (clickSelect ? 'select_from_name' : 'from_name'), {
from: placeholders.from,
fromName: displayName(way.id, vgraph)
}));
@@ -497,31 +497,31 @@ export function uiFieldRestrictions(field, context) {
} else if (datum instanceof osmTurn) {
var restrictionType = osmInferRestriction(vgraph, datum, projection);
var turnType = restrictionType.replace(/^(only|no)\_/, '');
var indirect = (datum.direct === false ? t('restriction.help.indirect') : '');
var indirect = (datum.direct === false ? t.html('restriction.help.indirect') : '');
var klass, turnText, nextText;
if (datum.no) {
klass = 'restrict';
turnText = t('restriction.help.turn.no_' + turnType, { indirect: indirect });
nextText = t('restriction.help.turn.only_' + turnType, { indirect: '' });
turnText = t.html('restriction.help.turn.no_' + turnType, { indirect: indirect });
nextText = t.html('restriction.help.turn.only_' + turnType, { indirect: '' });
} else if (datum.only) {
klass = 'only';
turnText = t('restriction.help.turn.only_' + turnType, { indirect: indirect });
nextText = t('restriction.help.turn.allowed_' + turnType, { indirect: '' });
turnText = t.html('restriction.help.turn.only_' + turnType, { indirect: indirect });
nextText = t.html('restriction.help.turn.allowed_' + turnType, { indirect: '' });
} else {
klass = 'allow';
turnText = t('restriction.help.turn.allowed_' + turnType, { indirect: indirect });
nextText = t('restriction.help.turn.no_' + turnType, { indirect: '' });
turnText = t.html('restriction.help.turn.allowed_' + turnType, { indirect: indirect });
nextText = t.html('restriction.help.turn.no_' + turnType, { indirect: '' });
}
help
.append('div') // "NO Right Turn (indirect)"
.attr('class', 'qualifier ' + klass)
.text(turnText);
.html(turnText);
help
.append('div') // "FROM {fromName} TO {toName}"
.html(t('restriction.help.from_name_to_name', {
.html(t.html('restriction.help.from_name_to_name', {
from: placeholders.from,
fromName: displayName(datum.from.way, vgraph),
to: placeholders.to,
@@ -539,7 +539,7 @@ export function uiFieldRestrictions(field, context) {
help
.append('div') // "VIA {viaNames}"
.html(t('restriction.help.via_names', {
.html(t.html('restriction.help.via_names', {
via: placeholders.via,
viaNames: names.join(', ')
}));
@@ -548,7 +548,7 @@ export function uiFieldRestrictions(field, context) {
if (!indirect) {
help
.append('div') // Click for "No Right Turn"
.text(t('restriction.help.toggle', { turn: nextText.trim() }));
.html(t.html('restriction.help.toggle', { turn: nextText.trim() }));
}
highlightPathsFrom(null);
@@ -566,7 +566,7 @@ export function uiFieldRestrictions(field, context) {
if (_fromWayID) {
help
.append('div') // "FROM {fromName}"
.html(t('restriction.help.from_name', {
.html(t.html('restriction.help.from_name', {
from: placeholders.from,
fromName: displayName(_fromWayID, vgraph)
}));
@@ -574,7 +574,7 @@ export function uiFieldRestrictions(field, context) {
} else {
help
.append('div') // "Click to select a FROM segment."
.html(t('restriction.help.select_from', {
.html(t.html('restriction.help.select_from', {
from: placeholders.from
}));
}
@@ -596,14 +596,14 @@ export function uiFieldRestrictions(field, context) {
opts = { distance: t('units.meters', { quantity: maxDist }) };
}
return t('restriction.controls.distance_up_to', opts);
return t.html('restriction.controls.distance_up_to', opts);
}
function displayMaxVia(maxVia) {
return maxVia === 0 ? t('restriction.controls.via_node_only')
: maxVia === 1 ? t('restriction.controls.via_up_to_one')
: t('restriction.controls.via_up_to_two');
return maxVia === 0 ? t.html('restriction.controls.via_node_only')
: maxVia === 1 ? t.html('restriction.controls.via_up_to_one')
: t.html('restriction.controls.via_up_to_two');
}
+5 -5
View File
@@ -84,8 +84,10 @@ export function uiFieldWikidata(field, context) {
.call(combobox.fetcher(fetchWikidataItems));
combobox.on('accept', function(d) {
_qid = d.id;
change();
if (d) {
_qid = d.id;
change();
}
}).on('cancel', function() {
setLabelForEntity();
});
@@ -94,7 +96,6 @@ export function uiFieldWikidata(field, context) {
.append('button')
.attr('class', 'form-field-button wiki-link')
.attr('title', t('icons.view_on', { domain: 'wikidata.org' }))
.attr('tabindex', -1)
.call(svgIcon('#iD-icon-out-link'))
.on('click', function() {
d3_event.preventDefault();
@@ -118,7 +119,7 @@ export function uiFieldWikidata(field, context) {
enter
.append('span')
.attr('class', 'label')
.text(function(d) { return t('wikidata.' + d); });
.html(function(d) { return t.html('wikidata.' + d); });
enter
.append('input')
@@ -131,7 +132,6 @@ export function uiFieldWikidata(field, context) {
.append('button')
.attr('class', 'form-field-button')
.attr('title', t('icons.copy'))
.attr('tabindex', -1)
.call(svgIcon('#iD-operation-copy'))
.on('click', function() {
d3_event.preventDefault();
+45 -20
View File
@@ -92,8 +92,6 @@ export function uiFieldWikipedia(field, context) {
.call(langCombo)
.merge(_langInput);
utilGetSetValue(_langInput, language()[1]);
_langInput
.on('blur', changeLang)
.on('change', changeLang);
@@ -130,7 +128,6 @@ export function uiFieldWikipedia(field, context) {
link = link.enter()
.append('button')
.attr('class', 'form-field-button wiki-link')
.attr('tabindex', -1)
.attr('title', t('icons.view_on', { domain: 'wikipedia.org' }))
.call(svgIcon('#iD-icon-out-link'))
.merge(link);
@@ -143,14 +140,33 @@ export function uiFieldWikipedia(field, context) {
}
function language() {
function defaultLanguageInfo(skipEnglishFallback) {
const langCode = localizer.languageCode().toLowerCase();
for (let i in _dataWikipedia) {
let d = _dataWikipedia[i];
// default to the language of iD's current locale
if (d[2] === langCode) return d;
}
// fallback to English
return skipEnglishFallback ? ['', '', ''] : ['English', 'English', 'en'];
}
function language(skipEnglishFallback) {
const value = utilGetSetValue(_langInput).toLowerCase();
const locale = localizer.localeCode().toLowerCase();
let localeLanguage;
return _dataWikipedia.find(d => {
if (d[2] === locale) localeLanguage = d;
return d[0].toLowerCase() === value || d[1].toLowerCase() === value || d[2] === value;
}) || localeLanguage || ['English', 'English', 'en'];
for (let i in _dataWikipedia) {
let d = _dataWikipedia[i];
// return the language already set in the UI, if supported
if (d[0].toLowerCase() === value ||
d[1].toLowerCase() === value ||
d[2] === value) return d;
}
// fallback to English
return defaultLanguageInfo(skipEnglishFallback);
}
@@ -168,10 +184,11 @@ export function uiFieldWikipedia(field, context) {
function change(skipWikidata) {
let value = utilGetSetValue(_titleInput);
const m = value.match(/https?:\/\/([-a-z]+)\.wikipedia\.org\/(?:wiki|\1-[-a-z]+)\/([^#]+)(?:#(.+))?/);
const l = m && _dataWikipedia.find(d => m[1] === d[2]);
const langInfo = m && _dataWikipedia.find(d => m[1] === d[2]);
let syncTags = {};
if (l) {
if (langInfo) {
const nativeLangName = langInfo[1];
// Normalize title http://www.mediawiki.org/wiki/API:Query#Title_normalization
value = decodeURIComponent(m[2]).replace(/_/g, ' ');
if (m[3]) {
@@ -186,7 +203,7 @@ export function uiFieldWikipedia(field, context) {
value += '#' + anchor.replace(/_/g, ' ');
}
value = value.slice(0, 1).toUpperCase() + value.slice(1);
utilGetSetValue(_langInput, l[1]);
utilGetSetValue(_langInput, nativeLangName);
utilGetSetValue(_titleInput, value);
}
@@ -250,14 +267,19 @@ export function uiFieldWikipedia(field, context) {
function updateForTags(tags) {
const value = typeof tags[field.key] === 'string' ? tags[field.key] : '';
// Expect tag format of `tagLang:tagArticleTitle`, e.g. `fr:Paris`, with
// optional suffix of `#anchor`
const m = value.match(/([^:]+):([^#]+)(?:#(.+))?/);
const l = m && _dataWikipedia.find(d => m[1] === d[2]);
const tagLang = m && m[1];
const tagArticleTitle = m && m[2];
let anchor = m && m[3];
const tagLangInfo = tagLang && _dataWikipedia.find(d => tagLang === d[2]);
// value in correct format
if (l) {
utilGetSetValue(_langInput, l[1]);
utilGetSetValue(_titleInput, m[2] + (anchor ? ('#' + anchor) : ''));
if (tagLangInfo) {
const nativeLangName = tagLangInfo[1];
utilGetSetValue(_langInput, nativeLangName);
utilGetSetValue(_titleInput, tagArticleTitle + (anchor ? ('#' + anchor) : ''));
if (anchor) {
try {
// Best-effort `anchorencode:` implementation
@@ -266,16 +288,19 @@ export function uiFieldWikipedia(field, context) {
anchor = anchor.replace(/ /g, '_');
}
}
_wikiURL = 'https://' + m[1] + '.wikipedia.org/wiki/' +
m[2].replace(/ /g, '_') + (anchor ? ('#' + anchor) : '');
_wikiURL = 'https://' + tagLang + '.wikipedia.org/wiki/' +
tagArticleTitle.replace(/ /g, '_') + (anchor ? ('#' + anchor) : '');
// unrecognized value format
} else {
utilGetSetValue(_titleInput, value);
if (value && value !== '') {
utilGetSetValue(_langInput, '');
_wikiURL = `https://en.wikipedia.org/wiki/Special:Search?search=${value}`;
const defaultLangInfo = defaultLanguageInfo();
_wikiURL = `https://${defaultLangInfo[2]}.wikipedia.org/w/index.php?fulltext=1&search=${value}`;
} else {
const shownOrDefaultLangInfo = language(true /* skipEnglishFallback */);
utilGetSetValue(_langInput, shownOrDefaultLangInfo[1]);
_wikiURL = '';
}
}
+6 -13
View File
@@ -6,8 +6,7 @@ export function uiFlash(context) {
var _duration = 2000;
var _iconName = '#iD-icon-no';
var _iconClass = 'disabled';
var _text = '';
var _textClass;
var _label = '';
function flash() {
if (_flashTimer) {
@@ -64,8 +63,8 @@ export function uiFlash(context) {
content
.selectAll('.flash-text')
.attr('class', 'flash-text ' + (_textClass || ''))
.text(_text);
.attr('class', 'flash-text')
.html(_label);
_flashTimer = d3_timeout(function() {
@@ -88,15 +87,9 @@ export function uiFlash(context) {
return flash;
};
flash.text = function(_) {
if (!arguments.length) return _text;
_text = _;
return flash;
};
flash.textClass = function(_) {
if (!arguments.length) return _textClass;
_textClass = _;
flash.label = function(_) {
if (!arguments.length) return _label;
_label = _;
return flash;
};

Some files were not shown because too many files have changed in this diff Show More