mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-18 14:45:12 +02:00
Fix merge conflicts
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,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.
|
||||
//
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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,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]);
|
||||
|
||||
@@ -149,7 +149,6 @@ export function behaviorDrag() {
|
||||
|
||||
|
||||
function behavior(selection) {
|
||||
_pointerId = null;
|
||||
var matchesSelector = utilPrefixDOMProperty('matchesSelector');
|
||||
var delegate = pointerdown;
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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)"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 = () => [];
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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().
|
||||
*/
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()];
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user