Merge branch 'master' into validation

# Conflicts:
#	data/core.yaml
#	dist/locales/en.json
#	modules/ui/commit_warnings.js
#	modules/ui/entity_editor.js
#	modules/util/index.js
#	modules/util/util.js
#	modules/validations/index.js
#	modules/validations/many_deletions.js
#	modules/validations/missing_tag.js
This commit is contained in:
Quincy Morgan
2019-01-14 10:13:56 -05:00
119 changed files with 4618 additions and 635 deletions
+2 -2
View File
@@ -160,8 +160,8 @@ export function behaviorDrag() {
for (; target && target !== root; target = target.parentNode) {
var datum = target.__data__;
var entity = datum instanceof osmNote ?
datum : datum && datum.properties && datum.properties.entity;
var entity = datum instanceof osmNote ? datum
: datum && datum.properties && datum.properties.entity;
if (entity && target[matchesSelector](_selector)) {
return dragstart.call(target, entity);
+5 -1
View File
@@ -5,7 +5,7 @@ import {
select as d3_select
} from 'd3-selection';
import { osmEntity, osmNote } from '../osm';
import { osmEntity, osmNote, krError } from '../osm';
import { utilKeybinding, utilRebind } from '../util';
@@ -112,6 +112,10 @@ export function behaviorHover(context) {
entity = datum;
selector = '.data' + datum.__featurehash__;
} else if (datum instanceof krError) {
entity = datum;
selector = '.kr_error-' + datum.id;
} else if (datum instanceof osmNote) {
entity = datum;
selector = '.note-' + datum.id;
+10 -3
View File
@@ -12,12 +12,14 @@ import {
modeBrowse,
modeSelect,
modeSelectData,
modeSelectNote
modeSelectNote,
modeSelectError
} from '../modes';
import {
osmEntity,
osmNote
osmNote,
krError
} from '../osm';
@@ -130,6 +132,7 @@ export function behaviorSelect(context) {
if (datum instanceof osmEntity) { // clicked an entity..
var selectedIDs = context.selectedIDs();
context.selectedNoteID(null);
context.selectedErrorID(null);
if (!isMultiselect) {
if (selectedIDs.length > 1 && (!suppressMenu && !isShowAlways)) {
@@ -167,9 +170,13 @@ export function behaviorSelect(context) {
context
.selectedNoteID(datum.id)
.enter(modeSelectNote(context, datum.id));
} else if (datum instanceof krError & !isMultiselect) { // clicked a krError error
context
.selectedErrorID(datum.id)
.enter(modeSelectError(context, datum.id));
} else { // clicked nothing..
context.selectedNoteID(null);
context.selectedErrorID(null);
if (!isMultiselect && mode.id !== 'browse') {
context.enter(modeBrowse(context));
}
+10 -1
View File
@@ -146,7 +146,9 @@ export function coreContext() {
this.loadEntity(entityID, function(err, result) {
if (err) return;
var entity = _find(result.data, function(e) { return e.id === entityID; });
if (entity) { map.zoomTo(entity); }
if (entity) {
map.zoomTo(entity);
}
});
}
@@ -264,6 +266,13 @@ export function coreContext() {
return context;
};
var _selectedErrorID;
context.selectedErrorID = function(errorID) {
if (!arguments.length) return _selectedErrorID;
_selectedErrorID = errorID;
return context;
};
/* Behaviors */
context.install = function(behavior) {
+15 -6
View File
@@ -52,9 +52,6 @@ export function coreHistory(context) {
annotation = actions.pop();
}
_stack[_index].transform = context.projection.transform();
_stack[_index].selectedIDs = context.selectedIDs();
var graph = _stack[_index].graph;
for (var i = 0; i < actions.length; i++) {
graph = actions[i](graph, t);
@@ -63,7 +60,9 @@ export function coreHistory(context) {
return {
graph: graph,
annotation: annotation,
imageryUsed: _imageryUsed
imageryUsed: _imageryUsed,
transform: context.projection.transform(),
selectedIDs: context.selectedIDs()
};
}
@@ -410,7 +409,8 @@ export function coreHistory(context) {
var base = _stack[0];
var s = _stack.map(function(i) {
var modified = [], deleted = [];
var modified = [];
var deleted = [];
_forEach(i.graph.entities, function(entity, id) {
if (entity) {
@@ -440,6 +440,8 @@ export function coreHistory(context) {
if (deleted.length) x.deleted = deleted;
if (i.imageryUsed) x.imageryUsed = i.imageryUsed;
if (i.annotation) x.annotation = i.annotation;
if (i.transform) x.transform = i.transform;
if (i.selectedIDs) x.selectedIDs = i.selectedIDs;
return x;
});
@@ -537,7 +539,9 @@ export function coreHistory(context) {
return {
graph: coreGraph(_stack[0].graph).load(entities),
annotation: d.annotation,
imageryUsed: d.imageryUsed
imageryUsed: d.imageryUsed,
transform: d.transform,
selectedIDs: d.selectedIDs
};
});
@@ -555,6 +559,11 @@ export function coreHistory(context) {
});
}
var transform = _stack[_index].transform;
if (transform) {
context.map().transformEase(transform, 0); // 0 = immediate, no easing
}
if (loadComplete) {
dispatch.call('change');
}
+23 -14
View File
@@ -20,13 +20,14 @@ export function modeDragNote(context) {
var _nudgeInterval;
var _lastLoc;
var _note; // most current note.. dragged note may have stale datum.
function startNudge(note, nudge) {
function startNudge(nudge) {
if (_nudgeInterval) window.clearInterval(_nudgeInterval);
_nudgeInterval = window.setInterval(function() {
context.pan(nudge);
doMove(note, nudge);
doMove(nudge);
}, 50);
}
@@ -45,58 +46,66 @@ export function modeDragNote(context) {
function start(note) {
context.surface().selectAll('.note-' + note.id)
_note = note;
var osm = services.osm;
if (osm) {
// Get latest note from cache.. The marker may have a stale datum bound to it
// and dragging it around can sometimes delete the users note comment.
_note = osm.getNote(_note.id);
}
context.surface().selectAll('.note-' + _note.id)
.classed('active', true);
context.perform(actionNoop());
context.enter(mode);
context.selectedNoteID(note.id);
context.selectedNoteID(_note.id);
}
function move(note) {
function move() {
d3_event.sourceEvent.stopPropagation();
_lastLoc = context.projection.invert(d3_event.point);
doMove(note);
doMove();
var nudge = geoViewportEdge(d3_event.point, context.map().dimensions());
if (nudge) {
startNudge(note, nudge);
startNudge(nudge);
} else {
stopNudge();
}
}
function doMove(note, nudge) {
function doMove(nudge) {
nudge = nudge || [0, 0];
var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);
var currMouse = geoVecSubtract(currPoint, nudge);
var loc = context.projection.invert(currMouse);
note = note.move(loc);
_note = _note.move(loc);
var osm = services.osm;
if (osm) {
osm.replaceNote(note); // update note cache
osm.replaceNote(_note); // update note cache
}
context.replace(actionNoop()); // trigger redraw
}
function end(note) {
function end() {
context.replace(actionNoop()); // trigger redraw
context
.selectedNoteID(note.id)
.enter(modeSelectNote(context, note.id));
.selectedNoteID(_note.id)
.enter(modeSelectNote(context, _note.id));
}
var drag = behaviorDrag()
.selector('.layer-notes .new')
.selector('.layer-touch.markers .target.note.new')
.surface(d3_select('#map').node())
.origin(origin)
.on('start', start)
+1
View File
@@ -12,4 +12,5 @@ export { modeRotate } from './rotate';
export { modeSave } from './save';
export { modeSelect } from './select';
export { modeSelectData } from './select_data';
export { modeSelectError} from './select_error';
export { modeSelectNote } from './select_note';
+93 -85
View File
@@ -192,6 +192,14 @@ export function modeSelect(context, selectedIDs) {
};
mode.zoomToSelected = function() {
var entity = singular();
if (entity) {
context.map().zoomTo(entity);
}
};
mode.reselect = function() {
if (!checkSelectedIDs()) return;
@@ -229,6 +237,91 @@ export function modeSelect(context, selectedIDs) {
mode.enter = function() {
if (!checkSelectedIDs()) return;
var operations = _without(_values(Operations), Operations.operationDelete)
.map(function(o) { return o(selectedIDs, context); })
.filter(function(o) { return o.available(); });
// deprecation warning - Radial Menu to be removed in iD v3
var isRadialMenu = context.storage('edit-menu-style') === 'radial';
if (isRadialMenu) {
operations = operations.slice(0,7);
operations.unshift(Operations.operationDelete(selectedIDs, context));
} else {
operations.push(Operations.operationDelete(selectedIDs, context));
}
operations.forEach(function(operation) {
if (operation.behavior) {
behaviors.push(operation.behavior);
}
});
behaviors.forEach(context.install);
keybinding
.on(t('inspector.zoom_to.key'), mode.zoomToSelected)
.on(['[', 'pgup'], previousVertex)
.on([']', 'pgdown'], nextVertex)
.on(['{', uiCmd('⌘['), 'home'], firstVertex)
.on(['}', uiCmd('⌘]'), 'end'], lastVertex)
.on(['\\', 'pause'], nextParent)
.on('⎋', esc, true)
.on('space', toggleMenu);
d3_select(document)
.call(keybinding);
// deprecation warning - Radial Menu to be removed in iD v3
editMenu = isRadialMenu
? uiRadialMenu(context, operations)
: uiEditMenu(context, operations);
context.ui().sidebar
.select(singular() ? singular().id : null, newFeature);
context.history()
.on('undone.select', update)
.on('redone.select', update);
context.map()
.on('move.select', closeMenu)
.on('drawn.select', selectElements);
context.surface()
.on('dblclick.select', dblclick);
selectElements();
if (selectedIDs.length > 1) {
var entities = uiSelectionList(context, selectedIDs);
context.ui().sidebar.show(entities);
}
if (follow) {
var extent = geoExtent();
var graph = context.graph();
selectedIDs.forEach(function(id) {
var entity = context.entity(id);
extent._extend(entity.extent(graph));
});
var loc = extent.center();
context.map().centerEase(loc);
} else if (singular() && singular().type === 'way') {
context.map().pan([0,0]); // full redraw, to adjust z-sorting #2914
}
timeout = window.setTimeout(function() {
positionMenu();
if (!suppressMenu) {
showMenu();
}
}, 270); /* after any centerEase completes */
function update() {
closeMenu();
@@ -419,91 +512,6 @@ export function modeSelect(context, selectedIDs) {
.classed('related', true);
}
}
if (!checkSelectedIDs()) return;
var operations = _without(_values(Operations), Operations.operationDelete)
.map(function(o) { return o(selectedIDs, context); })
.filter(function(o) { return o.available(); });
// deprecation warning - Radial Menu to be removed in iD v3
var isRadialMenu = context.storage('edit-menu-style') === 'radial';
if (isRadialMenu) {
operations = operations.slice(0,7);
operations.unshift(Operations.operationDelete(selectedIDs, context));
} else {
operations.push(Operations.operationDelete(selectedIDs, context));
}
operations.forEach(function(operation) {
if (operation.behavior) {
behaviors.push(operation.behavior);
}
});
behaviors.forEach(context.install);
keybinding
.on(['[', 'pgup'], previousVertex)
.on([']', 'pgdown'], nextVertex)
.on(['{', uiCmd('⌘['), 'home'], firstVertex)
.on(['}', uiCmd('⌘]'), 'end'], lastVertex)
.on(['\\', 'pause'], nextParent)
.on('⎋', esc, true)
.on('space', toggleMenu);
d3_select(document)
.call(keybinding);
// deprecation warning - Radial Menu to be removed in iD v3
editMenu = isRadialMenu
? uiRadialMenu(context, operations)
: uiEditMenu(context, operations);
context.ui().sidebar
.select(singular() ? singular().id : null, newFeature);
context.history()
.on('undone.select', update)
.on('redone.select', update);
context.map()
.on('move.select', closeMenu)
.on('drawn.select', selectElements);
context.surface()
.on('dblclick.select', dblclick);
selectElements();
if (selectedIDs.length > 1) {
var entities = uiSelectionList(context, selectedIDs);
context.ui().sidebar.show(entities);
}
if (follow) {
var extent = geoExtent();
var graph = context.graph();
selectedIDs.forEach(function(id) {
var entity = context.entity(id);
extent._extend(entity.extent(graph));
});
var loc = extent.center();
context.map().centerEase(loc);
} else if (singular() && singular().type === 'way') {
context.map().pan([0,0]); // full redraw, to adjust z-sorting #2914
}
timeout = window.setTimeout(function() {
positionMenu();
if (!suppressMenu) {
showMenu();
}
}, 270); /* after any centerEase completes */
};
+12 -2
View File
@@ -1,4 +1,3 @@
import { geoBounds as d3_geoBounds } from 'd3-geo';
import {
@@ -13,6 +12,8 @@ import {
behaviorSelect
} from '../behavior';
import { t } from '../util/locale';
import { geoExtent } from '../geo';
import { modeBrowse, modeDragNode, modeDragNote } from '../modes';
import { uiDataEditor } from '../ui';
@@ -61,9 +62,18 @@ export function modeSelectData(context, selectedDatum) {
}
mode.zoomToSelected = function() {
var extent = geoExtent(d3_geoBounds(selectedDatum));
context.map().centerZoom(extent.center(), context.map().trimmedExtentZoom(extent));
};
mode.enter = function() {
behaviors.forEach(context.install);
keybinding.on('⎋', esc, true);
keybinding
.on(t('inspector.zoom_to.key'), mode.zoomToSelected)
.on('⎋', esc, true);
d3_select(document)
.call(keybinding);
+138
View File
@@ -0,0 +1,138 @@
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import {
behaviorBreathe,
behaviorHover,
behaviorLasso,
behaviorSelect
} from '../behavior';
import { t } from '../util/locale';
import { services } from '../services';
import { modeBrowse, modeDragNode, modeDragNote } from '../modes';
import { uiKeepRightEditor } from '../ui';
import { utilKeybinding } from '../util';
export function modeSelectError(context, selectedErrorID) {
var mode = {
id: 'select-error',
button: 'browse'
};
var keepRight = services.keepRight;
var keybinding = utilKeybinding('select-error');
var keepRightEditor = uiKeepRightEditor(context)
.on('change', function() {
context.map().pan([0,0]); // trigger a redraw
var error = checkSelectedID();
if (!error) return;
context.ui().sidebar
.show(keepRightEditor.error(error));
});
var behaviors = [
behaviorBreathe(context),
behaviorHover(context),
behaviorSelect(context),
behaviorLasso(context),
modeDragNode(context).behavior,
modeDragNote(context).behavior
];
function checkSelectedID() {
if (!keepRight) return;
var error = keepRight.getError(selectedErrorID);
if (!error) {
context.enter(modeBrowse(context));
}
return error;
}
mode.zoomToSelected = function() {
if (!keepRight) return;
var error = keepRight.getError(selectedErrorID);
if (error) {
context.map().centerZoom(error.loc, 20);
}
};
mode.enter = function() {
var error = checkSelectedID();
if (!error) return;
behaviors.forEach(context.install);
keybinding
.on(t('inspector.zoom_to.key'), mode.zoomToSelected)
.on('⎋', esc, true);
d3_select(document)
.call(keybinding);
selectError();
var sidebar = context.ui().sidebar;
sidebar.show(keepRightEditor.error(error));
context.map()
.on('drawn.select-error', selectError);
// class the error as selected, or return to browse mode if the error is gone
function selectError(drawn) {
if (!checkSelectedID()) return;
var selection = context.surface()
.selectAll('.kr_error-' + selectedErrorID);
if (selection.empty()) {
// Return to browse mode if selected DOM elements have
// disappeared because the user moved them out of view..
var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;
if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) {
context.enter(modeBrowse(context));
}
} else {
selection
.classed('selected', true);
context.selectedErrorID(selectedErrorID);
}
}
function esc() {
if (d3_select('.combobox').size()) return;
context.enter(modeBrowse(context));
}
};
mode.exit = function() {
behaviors.forEach(context.uninstall);
d3_select(document)
.call(keybinding.unbind);
context.surface()
.selectAll('.kr_error.selected')
.classed('selected hover', false);
context.map()
.on('drawn.select-error', null);
context.ui().sidebar
.hide();
context.selectedErrorID(null);
};
return mode;
}
+18 -3
View File
@@ -10,6 +10,8 @@ import {
behaviorSelect
} from '../behavior';
import { t } from '../util/locale';
import { modeBrowse, modeDragNode, modeDragNote } from '../modes';
import { services } from '../services';
import { uiNoteEditor } from '../ui';
@@ -72,6 +74,7 @@ export function modeSelectNote(context, selectedNoteID) {
} else {
selection
.classed('selected', true);
context.selectedNoteID(selectedNoteID);
}
}
@@ -83,9 +86,18 @@ export function modeSelectNote(context, selectedNoteID) {
}
mode.newFeature = function(_) {
mode.zoomToSelected = function() {
if (!osm) return;
var note = osm.getNote(selectedNoteID);
if (note) {
context.map().centerZoom(note.loc, 20);
}
};
mode.newFeature = function(val) {
if (!arguments.length) return newFeature;
newFeature = _;
newFeature = val;
return mode;
};
@@ -95,7 +107,10 @@ export function modeSelectNote(context, selectedNoteID) {
if (!note) return;
behaviors.forEach(context.install);
keybinding.on('⎋', esc, true);
keybinding
.on(t('inspector.zoom_to.key'), mode.zoomToSelected)
.on('⎋', esc, true);
d3_select(document)
.call(keybinding);
+1
View File
@@ -1,5 +1,6 @@
export { osmChangeset } from './changeset';
export { osmEntity } from './entity';
export { krError } from './keepRight';
export { osmNode } from './node';
export { osmNote } from './note';
export { osmRelation } from './relation';
+49
View File
@@ -0,0 +1,49 @@
import _extend from 'lodash-es/extend';
export function krError() {
if (!(this instanceof krError)) {
return (new krError()).initialize(arguments);
} else if (arguments.length) {
this.initialize(arguments);
}
}
krError.id = function() {
return krError.id.next--;
};
krError.id.next = -1;
_extend(krError.prototype, {
type: 'krError',
initialize: function(sources) {
for (var i = 0; i < sources.length; ++i) {
var source = sources[i];
for (var prop in source) {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
if (source[prop] === undefined) {
delete this[prop];
} else {
this[prop] = source[prop];
}
}
}
}
if (!this.id) {
this.id = krError.id() + ''; // as string
}
return this;
},
update: function(attrs) {
return krError(this, attrs); // {v: 1 + (this.v || 0)}
}
});
+4 -3
View File
@@ -351,10 +351,11 @@ export function rendererMap(context) {
function editOff() {
context.features().resetStats();
surface.selectAll('.layer-osm *').remove();
surface.selectAll('.layer-touch *').remove();
surface.selectAll('.layer-touch:not(.markers) *').remove();
var mode = context.mode();
if (mode && mode.id !== 'save' && mode.id !== 'select-note' && mode.id !== 'select-data') {
if (mode && mode.id !== 'save' && mode.id !== 'select-note' &&
mode.id !== 'select-data' && mode.id !== 'select-error') {
context.enter(modeBrowse(context));
}
@@ -857,7 +858,7 @@ export function rendererMap(context) {
if (!isFinite(extent.area())) return;
var z2 = map.trimmedExtentZoom(extent);
zoomLimits = zoomLimits || [context.minEditableZoom(), 19];
zoomLimits = zoomLimits || [context.minEditableZoom(), 20];
map.centerZoom(extent.center(), Math.min(Math.max(z2, zoomLimits[0]), zoomLimits[1]));
};
+6
View File
@@ -1,8 +1,10 @@
import serviceKeepRight from './keepRight';
import serviceMapillary from './mapillary';
import serviceMapRules from './maprules';
import serviceNominatim from './nominatim';
import serviceOpenstreetcam from './openstreetcam';
import serviceOsm from './osm';
import serviceOsmWikibase from './osm_wikibase';
import serviceStreetside from './streetside';
import serviceTaginfo from './taginfo';
import serviceVectorTile from './vector_tile';
@@ -12,9 +14,11 @@ import serviceWikipedia from './wikipedia';
export var services = {
geocoder: serviceNominatim,
keepRight: serviceKeepRight,
mapillary: serviceMapillary,
openstreetcam: serviceOpenstreetcam,
osm: serviceOsm,
osmWikibase: serviceOsmWikibase,
maprules: serviceMapRules,
streetside: serviceStreetside,
taginfo: serviceTaginfo,
@@ -24,11 +28,13 @@ export var services = {
};
export {
serviceKeepRight,
serviceMapillary,
serviceMapRules,
serviceNominatim,
serviceOpenstreetcam,
serviceOsm,
serviceOsmWikibase,
serviceStreetside,
serviceTaginfo,
serviceVectorTile,
+498
View File
@@ -0,0 +1,498 @@
import _extend from 'lodash-es/extend';
import _find from 'lodash-es/find';
import _forEach from 'lodash-es/forEach';
import rbush from 'rbush';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { json as d3_json } from 'd3-request';
import { request as d3_request } from 'd3-request';
import { geoExtent, geoVecAdd } from '../geo';
import { krError } from '../osm';
import { t } from '../util/locale';
import { utilRebind, utilTiler, utilQsString } from '../util';
import { errorTypes, localizeStrings } from '../../data/keepRight.json';
var tiler = utilTiler();
var dispatch = d3_dispatch('loaded');
var _krCache;
var _krZoom = 14;
var _krUrlRoot = 'https://www.keepright.at/';
var _krRuleset = [
// no 20 - multiple node on same spot - these are mostly boundaries overlapping roads
30, 40, 50, 60, 70, 90, 100, 110, 120, 130, 150, 160, 170, 180,
190, 191, 192, 193, 194, 195, 196, 197, 198,
200, 201, 202, 203, 204, 205, 206, 207, 208, 210, 220,
230, 231, 232, 270, 280, 281, 282, 283, 284, 285,
290, 291, 292, 293, 294, 295, 296, 297, 298, 300, 310, 311, 312, 313,
320, 350, 360, 370, 380, 390, 400, 401, 402, 410, 411, 412, 413
];
function abortRequest(i) {
if (i) {
i.abort();
}
}
function abortUnwantedRequests(cache, tiles) {
_forEach(cache.inflight, function(v, k) {
var wanted = _find(tiles, function(tile) {
return k === tile.id;
});
if (!wanted) {
abortRequest(v);
delete cache.inflight[k];
}
});
}
function encodeErrorRtree(d) {
return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
}
// replace or remove error from rtree
function updateRtree(item, replace) {
_krCache.rtree.remove(item, function isEql(a, b) {
return a.data.id === b.data.id;
});
if (replace) {
_krCache.rtree.insert(item);
}
}
function tokenReplacements(d) {
if (!(d instanceof krError)) return;
var htmlRegex = new RegExp(/<\/[a-z][\s\S]*>/);
var replacements = {};
var errorTemplate = errorTypes[d.which_type];
if (!errorTemplate) {
/* eslint-disable no-console */
console.log('No Template: ', d.which_type);
console.log(' ', d.description);
/* eslint-enable no-console */
return;
}
// some descriptions are just fixed text
if (!errorTemplate.regex) return;
// regex pattern should match description with variable details captured
var errorRegex = new RegExp(errorTemplate.regex, 'i');
var errorMatch = errorRegex.exec(d.description);
if (!errorMatch) {
/* eslint-disable no-console */
console.log('Unmatched: ', d.which_type);
console.log(' ', d.description);
console.log(' ', errorRegex);
/* eslint-enable no-console */
return;
}
for (var i = 1; i < errorMatch.length; i++) { // skip first
var capture = errorMatch[i];
var idType;
idType = 'IDs' in errorTemplate ? errorTemplate.IDs[i-1] : '';
if (idType && capture) { // link IDs if present in the capture
capture = parseError(capture, idType);
} else if (htmlRegex.test(capture)) { // escape any html in non-IDs
capture = '\\' + capture + '\\';
} else {
var compare = capture.toLowerCase();
if (localizeStrings[compare]) { // some replacement strings can be localized
capture = t('QA.keepRight.error_parts.' + localizeStrings[compare]);
}
}
replacements['var' + i] = capture;
}
return replacements;
}
function parseError(capture, idType) {
var compare = capture.toLowerCase();
if (localizeStrings[compare]) { // some replacement strings can be localized
capture = t('QA.keepRight.error_parts.' + localizeStrings[compare]);
}
switch (idType) {
// link a string like "this node"
case 'this':
capture = linkErrorObject(capture);
break;
case 'url':
capture = linkURL(capture);
break;
// link an entity ID
case 'n':
case 'w':
case 'r':
capture = linkEntity(idType + capture);
break;
// some errors have more complex ID lists/variance
case '20':
capture = parse20(capture);
break;
case '211':
capture = parse211(capture);
break;
case '231':
capture = parse231(capture);
break;
case '294':
capture = parse294(capture);
break;
case '370':
capture = parse370(capture);
break;
}
return capture;
function linkErrorObject(d) {
return '<a class="kr_error_object_link">' + d + '</a>';
}
function linkEntity(d) {
return '<a class="kr_error_entity_link">' + d + '</a>';
}
function linkURL(d) {
return '<a class="kr_external_link" target="_blank" href="' + d + '">' + d + '</a>';
}
// arbitrary node list of form: #ID, #ID, #ID...
function parse211(capture) {
var newList = [];
var items = capture.split(', ');
items.forEach(function(item) {
// ID has # at the front
var id = linkEntity('n' + item.slice(1));
newList.push(id);
});
return newList.join(', ');
}
// arbitrary way list of form: #ID(layer),#ID(layer),#ID(layer)...
function parse231(capture) {
var newList = [];
// unfortunately 'layer' can itself contain commas, so we split on '),'
var items = capture.split('),');
items.forEach(function(item) {
var match = item.match(/\#(\d+)\((.+)\)?/);
if (match !== null && match.length > 2) {
newList.push(linkEntity('w' + match[1]) + ' ' +
t('QA.keepRight.errorTypes.231.layer', { layer: match[2] })
);
}
});
return newList.join(', ');
}
// arbitrary node/relation list of form: from node #ID,to relation #ID,to node #ID...
function parse294(capture) {
var newList = [];
var items = capture.split(',');
items.forEach(function(item) {
var role;
var idType;
var id;
// item of form "from/to node/relation #ID"
item = item.split(' ');
// to/from role is more clear in quotes
role = '"' + item[0] + '"';
// first letter of node/relation provides the type
idType = item[1].slice(0,1);
// ID has # at the front
id = item[2].slice(1);
id = linkEntity(idType + id);
item = [role, item[1], id].join(' ');
newList.push(item);
});
return newList.join(', ');
}
// may or may not include the string "(including the name 'name')"
function parse370(capture) {
if (!capture) return '';
var match = capture.match(/\(including the name (\'.+\')\)/);
if (match !== null && match.length) {
return t('QA.keepRight.errorTypes.370.including_the_name', { name: match[1] });
}
return '';
}
// arbitrary node list of form: #ID,#ID,#ID...
function parse20(capture) {
var newList = [];
var items = capture.split(',');
items.forEach(function(item) {
// ID has # at the front
var id = linkEntity('n' + item.slice(1));
newList.push(id);
});
return newList.join(', ');
}
}
export default {
init: function() {
if (!_krCache) {
this.reset();
}
this.event = utilRebind(this, dispatch, 'on');
},
reset: function() {
if (_krCache) {
_forEach(_krCache.inflight, abortRequest);
}
_krCache = {
data: {},
loaded: {},
inflight: {},
closed: {},
rtree: rbush()
};
},
// KeepRight API: http://osm.mueschelsoft.de/keepright/interfacing.php
loadErrors: function(projection) {
var options = { format: 'geojson' };
var rules = _krRuleset.join();
// determine the needed tiles to cover the view
var tiles = tiler
.zoomExtent([_krZoom, _krZoom])
.getTiles(projection);
// abort inflight requests that are no longer needed
abortUnwantedRequests(_krCache, tiles);
// issue new requests..
tiles.forEach(function(tile) {
if (_krCache.loaded[tile.id] || _krCache.inflight[tile.id]) return;
var rect = tile.extent.rectangle();
var params = _extend({}, options, { left: rect[0], bottom: rect[3], right: rect[2], top: rect[1] });
var url = _krUrlRoot + 'export.php?' + utilQsString(params) + '&ch=' + rules;
_krCache.inflight[tile.id] = d3_json(url,
function(err, data) {
delete _krCache.inflight[tile.id];
if (err) return;
_krCache.loaded[tile.id] = true;
if (!data.features || !data.features.length) return;
data.features.forEach(function(feature) {
var loc = feature.geometry.coordinates;
var props = feature.properties;
// if there is a parent, save its error type e.g.:
// Error 191 = "highway-highway"
// Error 190 = "intersections without junctions" (parent)
var errorType = props.error_type;
var errorTemplate = errorTypes[errorType];
var parentErrorType = (Math.floor(errorType / 10) * 10).toString();
// try to handle error type directly, fallback to parent error type.
var whichType = errorTemplate ? errorType : parentErrorType;
var whichTemplate = errorTypes[whichType];
// Rewrite a few of the errors at this point..
// This is done to make them easier to linkify and translate.
switch (whichType) {
case '170':
props.description = 'This feature has a FIXME tag: ' + props.description;
break;
case '292':
case '293':
props.description = props.description.replace('A turn-', 'This turn-');
break;
case '294':
case '295':
case '296':
case '297':
case '298':
props.description = 'This turn-restriction~' + props.description;
break;
case '300':
props.description = 'This highway is missing a maxspeed tag';
break;
case '411':
case '412':
case '413':
props.description = 'This feature~' + props.description;
break;
}
// - move markers slightly so it doesn't obscure the geometry,
// - then move markers away from other coincident markers
var coincident = false;
do {
// first time, move marker up. after that, move marker right.
var delta = coincident ? [0.00001, 0] : [0, 0.00001];
loc = geoVecAdd(loc, delta);
var bbox = geoExtent(loc).bbox();
coincident = _krCache.rtree.search(bbox).length;
} while (coincident);
var d = new krError({
loc: loc,
id: props.error_id,
comment: props.comment || null,
description: props.description || '',
error_id: props.error_id,
which_type: whichType,
error_type: errorType,
parent_error_type: parentErrorType,
severity: whichTemplate.severity || 'error',
object_id: props.object_id,
object_type: props.object_type,
schema: props.schema,
title: props.title
});
d.replacements = tokenReplacements(d);
_krCache.data[d.id] = d;
_krCache.rtree.insert(encodeErrorRtree(d));
});
dispatch.call('loaded');
}
);
});
},
postKeepRightUpdate: function(d, callback) {
if (_krCache.inflight[d.id]) {
return callback({ message: 'Error update already inflight', status: -2 }, d);
}
var that = this;
var params = { schema: d.schema, id: d.error_id };
if (d.state) {
params.st = d.state;
}
if (d.newComment !== undefined) {
params.co = d.newComment;
}
// NOTE: This throws a CORS err, but it seems successful.
// We don't care too much about the response, so this is fine.
var url = _krUrlRoot + 'comment.php?' + utilQsString(params);
_krCache.inflight[d.id] = d3_request(url)
.post(function(err) {
delete _krCache.inflight[d.id];
if (d.state === 'ignore') { // ignore permanently (false positive)
that.removeError(d);
} else if (d.state === 'ignore_t') { // ignore temporarily (error fixed)
that.removeError(d);
_krCache.closed[d.schema + ':' + d.error_id] = true;
} else {
d = that.replaceError(d.update({
comment: d.newComment,
newComment: undefined,
state: undefined
}));
}
return callback(err, d);
});
},
// get all cached errors covering the viewport
getErrors: function(projection) {
var viewport = projection.clipExtent();
var min = [viewport[0][0], viewport[1][1]];
var max = [viewport[1][0], viewport[0][1]];
var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
return _krCache.rtree.search(bbox).map(function(d) {
return d.data;
});
},
// get a single error from the cache
getError: function(id) {
return _krCache.data[id];
},
// replace a single error in the cache
replaceError: function(error) {
if (!(error instanceof krError) || !error.id) return;
_krCache.data[error.id] = error;
updateRtree(encodeErrorRtree(error), true); // true = replace
return error;
},
// remove a single error from the cache
removeError: function(error) {
if (!(error instanceof krError) || !error.id) return;
delete _krCache.data[error.id];
updateRtree(encodeErrorRtree(error), false); // false = remove
},
errorURL: function(error) {
return _krUrlRoot + 'report_map.php?schema=' + error.schema + '&error=' + error.id;
},
// Get an array of errors closed during this session.
// Used to populate `closed:keepright` changeset tag
getClosedIDs: function() {
return Object.keys(_krCache.closed).sort();
}
};
+21 -2
View File
@@ -47,7 +47,7 @@ var oauth = osmAuth({
var _blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*'];
var _tileCache = { loaded: {}, inflight: {}, seen: {} };
var _noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, rtree: rbush() };
var _noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: rbush() };
var _userCache = { toLoad: {}, user: {} };
var _changeset = {};
@@ -385,7 +385,7 @@ export default {
if (_changeset.inflight) abortRequest(_changeset.inflight);
_tileCache = { loaded: {}, inflight: {}, seen: {} };
_noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, rtree: rbush() };
_noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: rbush() };
_userCache = { toLoad: {}, user: {} };
_changeset = {};
@@ -432,6 +432,11 @@ export default {
},
noteReportURL: function(note) {
return urlroot + '/reports/new?reportable_type=Note&reportable_id=' + note.id;
},
// Generic method to load data from the OSM API
// Can handle either auth or unauth calls.
loadFromAPI: function(path, callback, options) {
@@ -951,6 +956,13 @@ export default {
// we get the updated note back, remove from caches and reparse..
this.removeNote(note);
// update closed note cache - used to populate `closed:note` changeset tag
if (action === 'close') {
_noteCache.closed[note.id] = true;
} else if (action === 'reopen') {
delete _noteCache.closed[note.id];
}
var options = { skipSeen: false };
return parseXML(xml, function(err, results) {
if (err) {
@@ -1113,6 +1125,13 @@ export default {
_noteCache.note[note.id] = note;
updateRtree(encodeNoteRtree(note), true); // true = replace
return note;
},
// Get an array of note IDs closed during this session.
// Used to populate `closed:note` changeset tag
getClosedIDs: function() {
return Object.keys(_noteCache.closed).sort();
}
};
+201
View File
@@ -0,0 +1,201 @@
import _debounce from 'lodash-es/debounce';
import _forEach from 'lodash-es/forEach';
import { json as d3_json } from 'd3-request';
import { utilQsString } from '../util';
var apibase = 'https://wiki.openstreetmap.org/w/api.php';
var _inflight = {};
var _wikibaseCache = {};
var _localeIDs = { en: false };
var debouncedRequest = _debounce(request, 500, { leading: false });
function request(url, callback) {
if (_inflight[url]) return;
_inflight[url] = d3_json(url, function (err, data) {
delete _inflight[url];
callback(err, data);
});
}
/**
* 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() {
_inflight = {};
_wikibaseCache = {};
_localeIDs = {};
},
reset: function() {
_forEach(_inflight, function(req) { req.abort(); });
_inflight = {};
},
/**
* Get the best value for the property, or undefined if not found
* @param entity object from wikibase
* @param property string e.g. 'P4' for image
* @param langCode string e.g. 'fr' for French
*/
claimToValue: function(entity, property, langCode) {
if (!entity.claims[property]) return undefined;
var locale = _localeIDs[langCode];
var preferredPick, localePick;
_forEach(entity.claims[property], function(stmt) {
// If exists, use value limited to the needed language (has a qualifier P26 = locale)
// Or if not found, use the first value with the "preferred" rank
if (!preferredPick && stmt.rank === 'preferred') {
preferredPick = stmt;
}
if (locale && stmt.qualifiers && stmt.qualifiers.P26 &&
stmt.qualifiers.P26[0].datavalue.value.id === locale
) {
localePick = stmt;
}
});
var result = localePick || preferredPick;
if (result) {
var datavalue = result.mainsnak.datavalue;
return datavalue.type === 'wikibase-entityid' ? datavalue.value.id : datavalue.value;
} else {
return undefined;
}
},
toSitelink: function(key, value) {
var result = value ? 'Tag:' + key + '=' + value : 'Key:' + key;
return result.replace(/_/g, ' ').trim();
},
getEntity: function(params, callback) {
var doRequest = params.debounce ? debouncedRequest : request;
var self = this;
var titles = [];
var result = {};
var keySitelink = this.toSitelink(params.key);
var tagSitelink = 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 (_wikibaseCache[keySitelink]) {
result.key = _wikibaseCache[keySitelink];
} else {
titles.push(keySitelink);
}
if (tagSitelink) {
if (_wikibaseCache[tagSitelink]) {
result.tag = _wikibaseCache[tagSitelink];
} else {
titles.push(tagSitelink);
}
}
if (!titles.length) {
// Nothing to do, we already had everything in the cache
return callback(null, result);
}
// Requesting just the user language code
// If backend recognizes the code, it will perform proper fallbacks,
// and the result will contain the requested code. If not, all values are returned:
// {"zh-tw":{"value":"...","language":"zh-tw","source-language":"zh-hant"}
// {"pt-br":{"value":"...","language":"pt","for-language":"pt-br"}}
var obj = {
action: 'wbgetentities',
sites: 'wiki',
titles: titles.join('|'),
languages: params.langCode,
languagefallback: 1,
origin: '*',
format: 'json',
// There is an MW Wikibase API bug https://phabricator.wikimedia.org/T212069
// We shouldn't use v1 until it gets fixed, but should switch to it afterwards
// formatversion: 2,
};
var url = apibase + '?' + utilQsString(obj);
doRequest(url, function(err, d) {
if (err) {
callback(err);
} else if (!d.success || d.error) {
callback(d.error.messages.map(function(v) { return v.html['*']; }).join('<br>'));
} else {
var localeID = false;
_forEach(d.entities, function(res) {
if (res.missing !== '') {
var title = res.sitelinks.wiki.title;
// Simplify access to the localized values
res.description = localizedToString(res.descriptions, params.langCode);
res.label = localizedToString(res.labels, params.langCode);
if (title === keySitelink) {
_wikibaseCache[keySitelink] = res;
result.key = res;
} else if (title === tagSitelink) {
_wikibaseCache[tagSitelink] = res;
result.tag = res;
} else if (title === localeSitelink) {
localeID = res.id;
} else {
console.log('Unexpected title ' + title); // eslint-disable-line no-console
}
}
});
if (localeSitelink) {
// If locale ID is not found, store false to prevent repeated queries
self.addLocale(params.langCode, localeID);
}
callback(null, result);
}
});
},
addLocale: function(langCode, qid) {
// Makes it easier to unit test
_localeIDs[langCode] = qid;
},
apibase: function(val) {
if (!arguments.length) return apibase;
apibase = val;
return this;
}
};
+2 -2
View File
@@ -2,9 +2,9 @@ import { select as d3_select } from 'd3-selection';
import { svgPointTransform } from './helpers';
import { geoMetersToLat } from '../geo';
import _throttle from 'lodash-es/throttle';
export function svgGeolocate(projection, context, dispatch) {
export function svgGeolocate(projection) {
var layer = d3_select(null);
var _position;
+1
View File
@@ -2,6 +2,7 @@ export { svgAreas } from './areas.js';
export { svgData } from './data.js';
export { svgDebug } from './debug.js';
export { svgDefs } from './defs.js';
export { svgKeepRight } from './keepRight';
export { svgIcon } from './icon.js';
export { svgGeolocate } from './geolocate';
export { svgLabels } from './labels.js';
+245
View File
@@ -0,0 +1,245 @@
import _throttle from 'lodash-es/throttle';
import { select as d3_select } from 'd3-selection';
import { modeBrowse } from '../modes';
import { svgPointTransform } from './index';
import { services } from '../services';
var _keepRightEnabled = false;
var _keepRightService;
export function svgKeepRight(projection, context, dispatch) {
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var touchLayer = d3_select(null);
var drawLayer = d3_select(null);
var _keepRightVisible = false;
function markerPath(selection, klass) {
selection
.attr('class', klass)
.attr('transform', 'translate(-4, -24)')
.attr('d', 'M11.6,6.2H7.1l1.4-5.1C8.6,0.6,8.1,0,7.5,0H2.2C1.7,0,1.3,0.3,1.3,0.8L0,10.2c-0.1,0.6,0.4,1.1,0.9,1.1h4.6l-1.8,7.6C3.6,19.4,4.1,20,4.7,20c0.3,0,0.6-0.2,0.8-0.5l6.9-11.9C12.7,7,12.3,6.2,11.6,6.2z');
}
// Loosely-coupled keepRight service for fetching errors.
function getService() {
if (services.keepRight && !_keepRightService) {
_keepRightService = services.keepRight;
_keepRightService.on('loaded', throttledRedraw);
} else if (!services.keepRight && _keepRightService) {
_keepRightService = null;
}
return _keepRightService;
}
// Show the errors
function editOn() {
if (!_keepRightVisible) {
_keepRightVisible = true;
drawLayer
.style('display', 'block');
}
}
// Immediately remove the errors and their touch targets
function editOff() {
if (_keepRightVisible) {
_keepRightVisible = false;
drawLayer
.style('display', 'none');
drawLayer.selectAll('.kr_error')
.remove();
touchLayer.selectAll('.kr_error')
.remove();
}
}
// Enable the layer. This shows the errors and transitions them to visible.
function layerOn() {
editOn();
drawLayer
.style('opacity', 0)
.transition()
.duration(250)
.style('opacity', 1)
.on('end interrupt', function () {
dispatch.call('change');
});
}
// Disable the layer. This transitions the layer invisible and then hides the errors.
function layerOff() {
throttledRedraw.cancel();
drawLayer.interrupt();
touchLayer.selectAll('.kr_error')
.remove();
drawLayer
.transition()
.duration(250)
.style('opacity', 0)
.on('end interrupt', function () {
editOff();
dispatch.call('change');
});
}
// Update the error markers
function updateMarkers() {
if (!_keepRightVisible || !_keepRightEnabled) return;
var service = getService();
var selectedID = context.selectedErrorID();
var data = (service ? service.getErrors(projection) : []);
var getTransform = svgPointTransform(projection);
// Draw markers..
var markers = drawLayer.selectAll('.kr_error')
.data(data, function(d) { return d.id; });
// exit
markers.exit()
.remove();
// enter
var markersEnter = markers.enter()
.append('g')
.attr('class', function(d) {
return 'kr_error kr_error-' + d.id + ' kr_error_type_' + d.parent_error_type; }
);
markersEnter
.append('ellipse')
.attr('cx', 0.5)
.attr('cy', 1)
.attr('rx', 6.5)
.attr('ry', 3)
.attr('class', 'stroke');
markersEnter
.append('path')
.call(markerPath, 'shadow');
markersEnter
.append('use')
.attr('class', 'kr_error-fill')
.attr('width', '20px')
.attr('height', '20px')
.attr('x', '-8px')
.attr('y', '-22px')
.attr('xlink:href', '#iD-icon-bolt');
// update
markers
.merge(markersEnter)
.sort(sortY)
.classed('selected', function(d) { return d.id === selectedID; })
.attr('transform', getTransform);
// Draw targets..
if (touchLayer.empty()) return;
var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
var targets = touchLayer.selectAll('.kr_error')
.data(data, function(d) { return d.id; });
// exit
targets.exit()
.remove();
// enter/update
targets.enter()
.append('rect')
.attr('width', '20px')
.attr('height', '20px')
.attr('x', '-8px')
.attr('y', '-22px')
.merge(targets)
.sort(sortY)
.attr('class', function(d) {
return 'kr_error target kr_error-' + d.id + ' ' + fillClass;
})
.attr('transform', getTransform);
function sortY(a, b) {
return (a.id === selectedID) ? 1
: (b.id === selectedID) ? -1
: (a.severity === 'error' && b.severity !== 'error') ? 1
: (b.severity === 'error' && a.severity !== 'error') ? -1
: b.loc[1] - a.loc[1];
}
}
// Draw the keepRight layer and schedule loading errors and updating markers.
function drawKeepRight(selection) {
var service = getService();
var surface = context.surface();
if (surface && !surface.empty()) {
touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');
}
drawLayer = selection.selectAll('.layer-keepRight')
.data(service ? [0] : []);
drawLayer.exit()
.remove();
drawLayer = drawLayer.enter()
.append('g')
.attr('class', 'layer-keepRight')
.style('display', _keepRightEnabled ? 'block' : 'none')
.merge(drawLayer);
if (_keepRightEnabled) {
if (service && ~~context.map().zoom() >= minZoom) {
editOn();
service.loadErrors(projection);
updateMarkers();
} else {
editOff();
}
}
}
// Toggles the layer on and off
drawKeepRight.enabled = function(val) {
if (!arguments.length) return _keepRightEnabled;
_keepRightEnabled = val;
if (_keepRightEnabled) {
layerOn();
} else {
layerOff();
if (context.selectedErrorID()) {
context.enter(modeBrowse(context));
}
}
dispatch.call('change');
return this;
};
drawKeepRight.supported = function() {
return !!getService();
};
return drawKeepRight;
}
+2
View File
@@ -10,6 +10,7 @@ import { select as d3_select } from 'd3-selection';
import { svgData } from './data';
import { svgDebug } from './debug';
import { svgGeolocate } from './geolocate';
import { svgKeepRight } from './keepRight';
import { svgStreetside } from './streetside';
import { svgMapillaryImages } from './mapillary_images';
import { svgMapillarySigns } from './mapillary_signs';
@@ -28,6 +29,7 @@ export function svgLayers(projection, context) {
{ id: 'osm', layer: svgOsm(projection, context, dispatch) },
{ id: 'notes', layer: svgNotes(projection, context, dispatch) },
{ id: 'data', layer: svgData(projection, context, dispatch) },
{ id: 'keepRight', layer: svgKeepRight(projection, context, dispatch) },
{ id: 'streetside', layer: svgStreetside(projection, context, dispatch)},
{ id: 'mapillary-images', layer: svgMapillaryImages(projection, context, dispatch) },
{ id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },
+123 -68
View File
@@ -8,12 +8,18 @@ import { svgPointTransform } from './index';
import { services } from '../services';
var _notesEnabled = false;
var _osmService;
export function svgNotes(projection, context, dispatch) {
if (!dispatch) { dispatch = d3_dispatch('change'); }
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var layer = d3_select(null);
var _notes;
var touchLayer = d3_select(null);
var drawLayer = d3_select(null);
var _notesVisible = false;
function markerPath(selection, klass) {
selection
@@ -22,40 +28,49 @@ export function svgNotes(projection, context, dispatch) {
.attr('d', 'm17.5,0l-15,0c-1.37,0 -2.5,1.12 -2.5,2.5l0,11.25c0,1.37 1.12,2.5 2.5,2.5l3.75,0l0,3.28c0,0.38 0.43,0.6 0.75,0.37l4.87,-3.65l5.62,0c1.37,0 2.5,-1.12 2.5,-2.5l0,-11.25c0,-1.37 -1.12,-2.5 -2.5,-2.5z');
}
function init() {
if (svgNotes.initialized) return; // run once
svgNotes.enabled = false;
svgNotes.initialized = true;
}
function editOn() {
layer.style('display', 'block');
}
function editOff() {
layer.selectAll('.note').remove();
layer.style('display', 'none');
}
// Loosely-coupled osm service for fetching notes.
function getService() {
if (services.osm && !_notes) {
_notes = services.osm;
_notes.on('loadedNotes', throttledRedraw);
} else if (!services.osm && _notes) {
_notes = null;
if (services.osm && !_osmService) {
_osmService = services.osm;
_osmService.on('loadedNotes', throttledRedraw);
} else if (!services.osm && _osmService) {
_osmService = null;
}
return _notes;
return _osmService;
}
function showLayer() {
// Show the notes
function editOn() {
if (!_notesVisible) {
_notesVisible = true;
drawLayer
.style('display', 'block');
}
}
// Immediately remove the notes and their touch targets
function editOff() {
if (_notesVisible) {
_notesVisible = false;
drawLayer
.style('display', 'none');
drawLayer.selectAll('.note')
.remove();
touchLayer.selectAll('.note')
.remove();
}
}
// Enable the layer. This shows the notes and transitions them to visible.
function layerOn() {
editOn();
layer
.classed('disabled', false)
drawLayer
.style('opacity', 0)
.transition()
.duration(250)
@@ -66,30 +81,35 @@ export function svgNotes(projection, context, dispatch) {
}
function hideLayer() {
editOff();
// Disable the layer. This transitions the layer invisible and then hides the notes.
function layerOff() {
throttledRedraw.cancel();
layer.interrupt();
drawLayer.interrupt();
touchLayer.selectAll('.note')
.remove();
layer
drawLayer
.transition()
.duration(250)
.style('opacity', 0)
.on('end interrupt', function () {
layer.classed('disabled', true);
editOff();
dispatch.call('change');
});
}
function update() {
// Update the note markers
function updateMarkers() {
if (!_notesVisible || !_notesEnabled) return;
var service = getService();
var selectedID = context.selectedNoteID();
var data = (service ? service.notes(projection) : []);
var transform = svgPointTransform(projection);
var notes = layer.selectAll('.note')
var getTransform = svgPointTransform(projection);
// Draw markers..
var notes = drawLayer.selectAll('.note')
.data(data, function(d) { return d.status + d.id; });
// exit
@@ -139,55 +159,90 @@ export function svgNotes(projection, context, dispatch) {
// update
notes
.merge(notesEnter)
.sort(function(a, b) {
return (a.id === selectedID) ? 1
: (b.id === selectedID) ? -1
: b.loc[1] - a.loc[1]; // sort Y
.sort(sortY)
.classed('selected', function(d) {
var mode = context.mode();
var isMoving = mode && mode.id === 'drag-note'; // no shadows when dragging
return !isMoving && d.id === selectedID;
})
.classed('selected', function(d) { return d.id === selectedID; })
.attr('transform', transform);
.attr('transform', getTransform);
// Draw targets..
if (touchLayer.empty()) return;
var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
var targets = touchLayer.selectAll('.note')
.data(data, function(d) { return d.id; });
// exit
targets.exit()
.remove();
// enter/update
targets.enter()
.append('rect')
.attr('width', '20px')
.attr('height', '20px')
.attr('x', '-8px')
.attr('y', '-22px')
.merge(targets)
.sort(sortY)
.attr('class', function(d) {
var newClass = (d.id < 0 ? 'new' : '');
return 'note target note-' + d.id + ' ' + fillClass + newClass;
})
.attr('transform', getTransform);
function sortY(a, b) {
return (a.id === selectedID) ? 1 : (b.id === selectedID) ? -1 : b.loc[1] - a.loc[1];
}
}
// Draw the notes layer and schedule loading notes and updating markers.
function drawNotes(selection) {
var enabled = svgNotes.enabled;
var service = getService();
layer = selection.selectAll('.layer-notes')
.data(service ? [0] : []);
layer.exit()
.remove();
layer.enter()
.append('g')
.attr('class', 'layer-notes')
.style('display', enabled ? 'block' : 'none')
.merge(layer);
function dimensions() {
return [window.innerWidth, window.innerHeight];
var surface = context.surface();
if (surface && !surface.empty()) {
touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');
}
if (enabled) {
drawLayer = selection.selectAll('.layer-notes')
.data(service ? [0] : []);
drawLayer.exit()
.remove();
drawLayer = drawLayer.enter()
.append('g')
.attr('class', 'layer-notes')
.style('display', _notesEnabled ? 'block' : 'none')
.merge(drawLayer);
if (_notesEnabled) {
if (service && ~~context.map().zoom() >= minZoom) {
editOn();
service.loadNotes(projection, dimensions());
update();
service.loadNotes(projection);
updateMarkers();
} else {
editOff();
}
}
}
drawNotes.enabled = function(val) {
if (!arguments.length) return svgNotes.enabled;
svgNotes.enabled = val;
if (svgNotes.enabled) {
showLayer();
// Toggles the layer on and off
drawNotes.enabled = function(val) {
if (!arguments.length) return _notesEnabled;
_notesEnabled = val;
if (_notesEnabled) {
layerOn();
} else {
hideLayer();
layerOff();
if (context.selectedNoteID()) {
context.enter(modeBrowse(context));
}
@@ -197,6 +252,6 @@ export function svgNotes(projection, context, dispatch) {
return this;
};
init();
return drawNotes;
}
+1 -1
View File
@@ -2,7 +2,7 @@ export function svgTouch() {
function drawTouch(selection) {
selection.selectAll('.layer-touch')
.data(['areas', 'lines', 'points', 'turns', 'notes'])
.data(['areas', 'lines', 'points', 'turns', 'markers'])
.enter()
.append('g')
.attr('class', function(d) { return 'layer-touch ' + d; });
+5 -8
View File
@@ -56,15 +56,12 @@ export function uiCombobox(context, klass) {
var parent = this.parentNode;
var sibling = this.nextSibling;
var caret = d3_select(parent).selectAll('.combobox-caret')
d3_select(parent).selectAll('.combobox-caret')
.filter(function(d) { return d === input.node(); })
.data([input.node()]);
caret = caret.enter()
.data([input.node()])
.enter()
.insert('div', function() { return sibling; })
.attr('class', 'combobox-caret')
.merge(caret);
.attr('class', 'combobox-caret');
}
@@ -341,7 +338,7 @@ export function uiCombobox(context, klass) {
// Dispatches an 'accept' event if an option has been chosen.
// Then hides the combobox.
function accept(d) {
d = d || _choice;
d = d || _choice || value();
if (d) {
utilGetSetValue(input, d.value);
utilTriggerEvent(input, 'change');
+21 -8
View File
@@ -8,6 +8,7 @@ import { select as d3_select } from 'd3-selection';
import { t } from '../util/locale';
import { osmChangeset } from '../osm';
import { services } from '../services';
import { uiChangesetEditor } from './changeset_editor';
import { uiCommitChanges } from './commit_changes';
import { uiCommitWarnings } from './commit_warnings';
@@ -63,6 +64,8 @@ export function uiCommit(context) {
}
var tags;
// Initialize changeset if one does not exist yet.
// Also pull values from local storage.
if (!_changeset) {
var detected = utilDetect();
tags = {
@@ -81,13 +84,9 @@ export function uiCommit(context) {
tags.hashtags = hashtags;
}
// iD 2.8.1 could write a literal 'undefined' here.. see #5021
// (old source values expire after 2 days, so 'undefined' checks can go away in v2.9)
var source = context.storage('source');
if (source && source !== 'undefined') {
if (source) {
tags.source = source;
} else if (source === 'undefined') {
context.storage('source', null);
}
_changeset = new osmChangeset({ tags: tags });
@@ -95,8 +94,22 @@ export function uiCommit(context) {
tags = _clone(_changeset.tags);
// assign tags for imagery used
var imageryUsed = context.history().imageryUsed().join(';').substr(0, 255);
tags.imagery_used = imageryUsed || 'None';
// assign tags for closed issues and notes
var osmClosed = osm.getClosedIDs();
if (osmClosed.length) {
tags['closed:note'] = osmClosed.join(';').substr(0, 255);
}
if (services.keepRight) {
var krClosed = services.keepRight.getClosedIDs();
if (krClosed.length) {
tags['closed:keepright'] = krClosed.join(';').substr(0, 255);
}
}
_changeset = _changeset.update({ tags: tags });
var header = selection.selectAll('.header')
@@ -109,17 +122,17 @@ export function uiCommit(context) {
headerTitle
.append('div')
.attr('class', 'header-block header-block-outer');
headerTitle
.append('div')
.attr('class', 'header-block')
.append('h3')
.text(t('commit.title'));
headerTitle
.append('div')
.attr('class', 'header-block header-block-outer header-block-close')
.append('button')
.append('button')
.attr('class', 'close')
.on('click', function() { context.enter(modeBrowse(context)); })
.call(svgIcon('#iD-icon-close'));
+3 -1
View File
@@ -26,7 +26,9 @@ export function uiCommitWarnings(context) {
}, {});
_forEach(issues, function(instances, severity) {
instances = _uniqBy(instances, function(val) { return val.id + '_' + val.message.replace(/\s+/g,''); });
instances = _uniqBy(instances, function(val) {
return val.entity || (val.id + '_' + val.message.replace(/\s+/g,''));
});
var section = severity + '-section';
var instanceItem = severity + '-item';
+20 -3
View File
@@ -4,17 +4,33 @@ import { svgIcon } from '../svg';
import {
uiDataHeader,
uiRawTagEditor
uiQuickLinks,
uiRawTagEditor,
uiTooltipHtml
} from './index';
export function uiDataEditor(context) {
var dataHeader = uiDataHeader();
var quickLinks = uiQuickLinks();
var rawTagEditor = uiRawTagEditor(context);
var _datum;
function dataEditor(selection) {
// quick links
var choices = [{
id: 'zoom_to',
label: 'inspector.zoom_to.title',
tooltip: function() {
return uiTooltipHtml(t('inspector.zoom_to.tooltip_data'), t('inspector.zoom_to.key'));
},
click: function zoomTo() {
context.mode().zoomToSelected();
}
}];
var header = selection.selectAll('.header')
.data([0]);
@@ -50,14 +66,15 @@ export function uiDataEditor(context) {
.append('div')
.attr('class', 'modal-section data-editor')
.merge(editor)
.call(dataHeader.datum(_datum));
.call(dataHeader.datum(_datum))
.call(quickLinks.choices(choices));
var rte = body.selectAll('.raw-tag-editor')
.data([0]);
rte.enter()
.append('div')
.attr('class', 'inspector-border raw-tag-editor inspector-inner data-editor')
.attr('class', 'raw-tag-editor inspector-inner data-editor')
.merge(rte)
.call(rawTagEditor
.expanded(true)
+1 -1
View File
@@ -104,7 +104,7 @@ export function uiEditMenu(context, operations) {
.attr('transform', function () { return 'translate(' + [2 * p, 5] + ')'; })
.attr('xlink:href', function (d) { return '#iD-operation-' + d.id; });
tooltip = d3_select(document.body)
tooltip = d3_select('#id-container')
.append('div')
.attr('class', 'tooltip-inner edit-menu-tooltip');
+52 -28
View File
@@ -15,12 +15,14 @@ import { actionChangeTags } from '../actions';
import { modeBrowse } from '../modes';
import { svgIcon } from '../svg';
import { uiPresetIcon } from './preset_icon';
import { uiQuickLinks } from './quick_links';
import { uiRawMemberEditor } from './raw_member_editor';
import { uiRawMembershipEditor } from './raw_membership_editor';
import { uiRawTagEditor } from './raw_tag_editor';
import { uiTagReference } from './tag_reference';
import { uiPresetEditor } from './preset_editor';
import { uiEntityIssues } from './entity_issues';
import { uiTooltipHtml } from './tooltipHtml';
import { utilCleanTags, utilRebind } from '../util';
@@ -35,6 +37,7 @@ export function uiEntityEditor(context) {
var _tagReference;
var entityIssues = uiEntityIssues(context);
var quickLinks = uiQuickLinks();
var presetEditor = uiPresetEditor(context).on('change', changeTags);
var rawTagEditor = uiRawTagEditor(context).on('change', changeTags);
var rawMemberEditor = uiRawMemberEditor(context);
@@ -49,28 +52,28 @@ export function uiEntityEditor(context) {
.data([0]);
// Enter
var enter = header.enter()
var headerEnter = header.enter()
.append('div')
.attr('class', 'header fillL cf');
enter
headerEnter
.append('button')
.attr('class', 'fl preset-reset preset-choose')
.call(svgIcon((textDirection === 'rtl') ? '#iD-icon-forward' : '#iD-icon-backward'));
enter
headerEnter
.append('button')
.attr('class', 'fr preset-close')
.on('click', function() { context.enter(modeBrowse(context)); })
.call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close'));
enter
headerEnter
.append('h3')
.text(t('inspector.edit'));
// Update
header = header
.merge(enter);
.merge(headerEnter);
header.selectAll('.preset-reset')
.on('click', function() {
@@ -83,11 +86,11 @@ export function uiEntityEditor(context) {
.data([0]);
// Enter
enter = body.enter()
var bodyEnter = body.enter()
.append('div')
.attr('class', 'inspector-body');
enter
bodyEnter
.append('div')
.attr('class', 'preset-list-item inspector-inner')
.append('div')
@@ -100,27 +103,31 @@ export function uiEntityEditor(context) {
.append('div')
.attr('class', 'label-inner');
enter
bodyEnter
.append('div')
.attr('class', 'inspector-border entity-issues');
.attr('class', 'preset-quick-links');
enter
bodyEnter
.append('div')
.attr('class', 'inspector-border preset-editor');
.attr('class', 'entity-issues');
enter
bodyEnter
.append('div')
.attr('class', 'inspector-border raw-tag-editor inspector-inner');
.attr('class', 'preset-editor');
enter
bodyEnter
.append('div')
.attr('class', 'inspector-border raw-member-editor inspector-inner');
.attr('class', 'raw-tag-editor inspector-inner');
enter
bodyEnter
.append('div')
.attr('class', 'raw-member-editor inspector-inner');
bodyEnter
.append('div')
.attr('class', 'raw-membership-editor inspector-inner');
enter
bodyEnter
.append('input')
.attr('type', 'text')
.attr('class', 'key-trap');
@@ -128,8 +135,9 @@ export function uiEntityEditor(context) {
// Update
body = body
.merge(enter);
.merge(bodyEnter);
// update header
if (_tagReference) {
body.selectAll('.preset-list-button-wrap')
.call(_tagReference.button);
@@ -149,7 +157,6 @@ export function uiEntityEditor(context) {
.preset(_activePreset)
);
var label = body.select('.label-inner');
var nameparts = label.selectAll('.namepart')
.data(_activePreset.name().split(' - '), function(d) { return d; });
@@ -168,6 +175,23 @@ export function uiEntityEditor(context) {
.entityID(_entityID)
);
// update quick links
var choices = [{
id: 'zoom_to',
label: 'inspector.zoom_to.title',
tooltip: function() {
return uiTooltipHtml(t('inspector.zoom_to.tooltip_feature'), t('inspector.zoom_to.key'));
},
click: function zoomTo() {
context.mode().zoomToSelected();
}
}];
body.select('.preset-quick-links')
.call(quickLinks.choices(choices));
// update editor sections
body.select('.preset-editor')
.call(presetEditor
.preset(_activePreset)
@@ -274,25 +298,25 @@ export function uiEntityEditor(context) {
}
entityEditor.modified = function(_) {
entityEditor.modified = function(val) {
if (!arguments.length) return _modified;
_modified = _;
_modified = val;
d3_selectAll('button.preset-close use')
.attr('xlink:href', (_modified ? '#iD-icon-apply' : '#iD-icon-close'));
return entityEditor;
};
entityEditor.state = function(_) {
entityEditor.state = function(val) {
if (!arguments.length) return _state;
_state = _;
_state = val;
return entityEditor;
};
entityEditor.entityID = function(_) {
entityEditor.entityID = function(val) {
if (!arguments.length) return _entityID;
_entityID = _;
_entityID = val;
_base = context.graph();
_coalesceChanges = false;
@@ -310,10 +334,10 @@ export function uiEntityEditor(context) {
};
entityEditor.preset = function(_) {
entityEditor.preset = function(val) {
if (!arguments.length) return _activePreset;
if (_ !== _activePreset) {
_activePreset = _;
if (val !== _activePreset) {
_activePreset = val;
_tagReference = uiTagReference(_activePreset.reference(context.geometry(_entityID)), context)
.showing(false);
}
+17 -17
View File
@@ -231,38 +231,38 @@ export function uiField(context, presetField, entity, options) {
}
};
// A shown field has a visible UI, a non-shown field is in the 'Add field' dropdown
field.isShown = function() {
return _show || isPresent();
};
// An allowed field can appear in the UI or in the 'Add field' dropdown.
// A non-allowed field is hidden from the user altogether
field.isAllowed = function() {
if (!entity || isPresent()) return true; // a field with a value should always display
if (isPresent()) {
// always allow a field with a value to display
return true;
}
var latest = context.hasEntity(entity.id); // check the most current copy of the entity
if (!latest) return true;
var prerequisiteTag = field.prerequisiteTag;
if (prerequisiteTag && prerequisiteTag.key && field.entityID && context.hasEntity(field.entityID)) {
var value = context.entity(field.entityID).tags[prerequisiteTag.key];
if (value) {
if (prerequisiteTag.valueNot) {
return prerequisiteTag.valueNot !== value;
}
if (prerequisiteTag.value) {
return prerequisiteTag.value === value;
}
return true;
} else {
return false;
var require = field.prerequisiteTag;
if (require && require.key) {
var value = latest.tags[require.key];
if (!value) return false;
if (require.valueNot) {
return require.valueNot !== value;
}
if (require.value) {
return require.value === value;
}
return true;
}
return true;
};
field.focus = function() {
if (field.impl) {
field.impl.focus();
+2 -2
View File
@@ -4,7 +4,7 @@ import {
} from 'd3-selection';
import marked from 'marked';
import { t } from '../util/locale';
import { t, textDirection } from '../util/locale';
import { svgIcon } from '../svg';
import { icon } from './intro/helper';
@@ -197,7 +197,7 @@ export function uiFieldHelp(context, fieldName) {
titleEnter
.append('h2')
.attr('class', 'fl')
.attr('class', ((textDirection === 'rtl') ? 'fr' : 'fl'))
.text(t('help.field.' + fieldName + '.title'));
titleEnter
+12 -18
View File
@@ -7,25 +7,13 @@ import { utilGetSetValue, utilNoAuto } from '../util';
export function uiFormFields(context) {
var moreCombo = uiCombobox(context, 'more-fields').minItems(1);
var _fieldsArr = [];
var _state = '';
var _fieldsArr;
var _klass = '';
function formFields(selection, klass) {
render(selection, klass);
}
formFields.tagsChanged = function() {};
function render(selection, klass) {
formFields.tagsChanged = function() {
render(selection, klass);
};
function formFields(selection) {
var allowedFields = _fieldsArr.filter(function(field) { return field.isAllowed(); });
var shown = allowedFields.filter(function(field) { return field.isShown(); });
var notShown = allowedFields.filter(function(field) { return !field.isShown(); });
@@ -34,7 +22,7 @@ export function uiFormFields(context) {
container = container.enter()
.append('div')
.attr('class', 'form-fields-container ' + (klass || ''))
.attr('class', 'form-fields-container ' + (_klass || ''))
.merge(container);
@@ -111,7 +99,7 @@ export function uiFormFields(context) {
.on('accept', function (d) {
var field = d.field;
field.show();
render(selection);
selection.call(formFields); // rerender
if (field.type !== 'semiCombo' && field.type !== 'multiCombo') {
field.focus();
}
@@ -122,7 +110,7 @@ export function uiFormFields(context) {
formFields.fieldsArr = function(val) {
if (!arguments.length) return _fieldsArr;
_fieldsArr = val;
_fieldsArr = val || [];
return formFields;
};
@@ -132,6 +120,12 @@ export function uiFormFields(context) {
return formFields;
};
formFields.klass = function(val) {
if (!arguments.length) return _klass;
_klass = val;
return formFields;
};
return formFields;
}
+9
View File
@@ -181,6 +181,13 @@ export function uiHelp(context) {
'using',
'tracing',
'upload'
]],
['qa', [
'intro',
'tools_h',
'tools',
'issues_h',
'issues'
]]
];
@@ -228,6 +235,8 @@ export function uiHelp(context) {
'help.imagery.offsets_h': 3,
'help.streetlevel.using_h': 3,
'help.gps.using_h': 3,
'help.qa.tools_h': 3,
'help.qa.issues_h': 3
};
var replacements = {
+5
View File
@@ -30,6 +30,9 @@ export { uiGeolocate } from './geolocate';
export { uiHelp } from './help';
export { uiInfo } from './info';
export { uiInspector } from './inspector';
export { uiKeepRightDetails } from './keepRight_details';
export { uiKeepRightEditor } from './keepRight_editor';
export { uiKeepRightHeader } from './keepRight_header';
export { uiLasso } from './lasso';
export { uiLoading } from './loading';
export { uiMapData } from './map_data';
@@ -44,6 +47,7 @@ export { uiNoteReport } from './note_report';
export { uiPresetEditor } from './preset_editor';
export { uiPresetIcon } from './preset_icon';
export { uiPresetList } from './preset_list';
export { uiQuickLinks } from './quick_links';
export { uiRadialMenu } from './radial_menu';
export { uiRawMemberEditor } from './raw_member_editor';
export { uiRawMembershipEditor } from './raw_membership_editor';
@@ -64,4 +68,5 @@ export { uiTooltipHtml } from './tooltipHtml';
export { uiUndoRedo } from './undo_redo';
export { uiVersion } from './version';
export { uiViewOnOSM } from './view_on_osm';
export { uiViewOnKeepRight } from './view_on_keepRight';
export { uiZoom } from './zoom';
+1 -1
View File
@@ -306,7 +306,7 @@ export function uiInit(context) {
var panPixels = 80;
context.keybinding()
.on('⌫', function() { d3_event.preventDefault(); })
.on(t('sidebar.key'), ui.sidebar.toggle)
.on([t('sidebar.key'), '`', '²'], ui.sidebar.toggle) // #5663 - common QWERTY, AZERTY
.on('←', pan([panPixels, 0]))
.on('↑', pan([0, panPixels]))
.on('→', pan([-panPixels, 0]))
+6 -6
View File
@@ -92,9 +92,9 @@ export function uiInspector(context) {
inspector.showList = function() {};
inspector.setPreset = function() {};
inspector.state = function(_) {
inspector.state = function(val) {
if (!arguments.length) return _state;
_state = _;
_state = val;
entityEditor.state(_state);
// remove any old field help overlay that might have gotten attached to the inspector
@@ -104,16 +104,16 @@ export function uiInspector(context) {
};
inspector.entityID = function(_) {
inspector.entityID = function(val) {
if (!arguments.length) return _entityID;
_entityID = _;
_entityID = val;
return inspector;
};
inspector.newFeature = function(_) {
inspector.newFeature = function(val) {
if (!arguments.length) return _newFeature;
_newFeature = _;
_newFeature = val;
return inspector;
};
+132
View File
@@ -0,0 +1,132 @@
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import { dataEn } from '../../data';
import { modeSelect } from '../modes';
import { t } from '../util/locale';
import { utilDisplayName, utilEntityOrMemberSelector, utilEntityRoot } from '../util';
export function uiKeepRightDetails(context) {
var _error;
function errorDetail(d) {
var unknown = t('inspector.unknown');
if (!d) return unknown;
var errorType = d.error_type;
var parentErrorType = d.parent_error_type;
var et = dataEn.QA.keepRight.errorTypes[errorType];
var pt = dataEn.QA.keepRight.errorTypes[parentErrorType];
var detail;
if (et && et.description) {
detail = t('QA.keepRight.errorTypes.' + errorType + '.description', d.replacements);
} else if (pt && pt.description) {
detail = t('QA.keepRight.errorTypes.' + parentErrorType + '.description', d.replacements);
} else {
detail = unknown;
}
return detail;
}
function keepRightDetails(selection) {
var details = selection.selectAll('.kr_error-details')
.data(
(_error ? [_error] : []),
function(d) { return d.id + '-' + (d.status || 0); }
);
details.exit()
.remove();
var detailsEnter = details.enter()
.append('div')
.attr('class', 'kr_error-details kr_error-details-container');
// description
var descriptionEnter = detailsEnter
.append('div')
.attr('class', 'kr_error-details-description');
descriptionEnter
.append('h4')
.text(function() { return t('QA.keepRight.detail_description'); });
descriptionEnter
.append('div')
.attr('class', 'kr_error-details-description-text')
.html(errorDetail);
// If there are entity links in the error message..
descriptionEnter.selectAll('.kr_error_entity_link, .kr_error_object_link')
.each(function() {
var link = d3_select(this);
var isObjectLink = link.classed('kr_error_object_link');
var entityID = isObjectLink ?
(utilEntityRoot(_error.object_type) + _error.object_id)
: this.textContent;
var entity = context.hasEntity(entityID);
// Add click handler
link
.on('mouseover', function() {
context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph()))
.classed('hover', true);
})
.on('mouseout', function() {
context.surface().selectAll('.hover')
.classed('hover', false);
})
.on('click', function() {
d3_event.preventDefault();
var osmlayer = context.layers().layer('osm');
if (!osmlayer.enabled()) {
osmlayer.enabled(true);
}
context.map().centerZoom(_error.loc, 20);
if (entity) {
context.enter(modeSelect(context, [entityID]));
} else {
context.loadEntity(entityID, function() {
context.enter(modeSelect(context, [entityID]));
});
}
});
// Replace with friendly name if possible
// (The entity may not yet be loaded into the graph)
if (entity) {
var name = utilDisplayName(entity); // try to use common name
if (!name && !isObjectLink) {
var preset = context.presets().match(entity, context.graph());
name = preset && !preset.isFallback() && preset.name(); // fallback to preset name
}
if (name) {
this.innerText = name;
}
}
});
}
keepRightDetails.error = function(val) {
if (!arguments.length) return _error;
_error = val;
return keepRightDetails;
};
return keepRightDetails;
}
+244
View File
@@ -0,0 +1,244 @@
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import { t } from '../util/locale';
import { services } from '../services';
import { modeBrowse } from '../modes';
import { svgIcon } from '../svg';
import {
uiKeepRightDetails,
uiKeepRightHeader,
uiQuickLinks,
uiTooltipHtml,
uiViewOnKeepRight
} from './index';
import { utilNoAuto, utilRebind } from '../util';
export function uiKeepRightEditor(context) {
var dispatch = d3_dispatch('change');
var keepRightDetails = uiKeepRightDetails(context);
var keepRightHeader = uiKeepRightHeader(context);
var quickLinks = uiQuickLinks();
var _error;
function keepRightEditor(selection) {
// quick links
var choices = [{
id: 'zoom_to',
label: 'inspector.zoom_to.title',
tooltip: function() {
return uiTooltipHtml(t('inspector.zoom_to.tooltip_issue'), t('inspector.zoom_to.key'));
},
click: function zoomTo() {
context.mode().zoomToSelected();
}
}];
var header = selection.selectAll('.header')
.data([0]);
var headerEnter = header.enter()
.append('div')
.attr('class', 'header fillL');
headerEnter
.append('button')
.attr('class', 'fr keepRight-editor-close')
.on('click', function() {
context.enter(modeBrowse(context));
})
.call(svgIcon('#iD-icon-close'));
headerEnter
.append('h3')
.text(t('QA.keepRight.title'));
var body = selection.selectAll('.body')
.data([0]);
body = body.enter()
.append('div')
.attr('class', 'body')
.merge(body);
var editor = body.selectAll('.keepRight-editor')
.data([0]);
editor.enter()
.append('div')
.attr('class', 'modal-section keepRight-editor')
.merge(editor)
.call(keepRightHeader.error(_error))
.call(quickLinks.choices(choices))
.call(keepRightDetails.error(_error))
.call(keepRightSaveSection);
var footer = selection.selectAll('.footer')
.data([0]);
footer.enter()
.append('div')
.attr('class', 'footer')
.merge(footer)
.call(uiViewOnKeepRight(context).what(_error));
}
function keepRightSaveSection(selection) {
var isSelected = (_error && _error.id === context.selectedErrorID());
var isShown = (_error && (isSelected || _error.newComment || _error.comment));
var saveSection = selection.selectAll('.error-save')
.data(
(isShown ? [_error] : []),
function(d) { return d.id + '-' + (d.status || 0); }
);
// exit
saveSection.exit()
.remove();
// enter
var saveSectionEnter = saveSection.enter()
.append('div')
.attr('class', 'keepRight-save save-section cf');
saveSectionEnter
.append('h4')
.attr('class', '.error-save-header')
.text(t('QA.keepRight.comment'));
saveSectionEnter
.append('textarea')
.attr('class', 'new-comment-input')
.attr('placeholder', t('QA.keepRight.comment_placeholder'))
.attr('maxlength', 1000)
.property('value', function(d) { return d.newComment || d.comment; })
.call(utilNoAuto)
.on('input', changeInput)
.on('blur', changeInput);
// update
saveSection = saveSectionEnter
.merge(saveSection)
.call(keepRightSaveButtons);
function changeInput() {
var input = d3_select(this);
var val = input.property('value').trim();
if (val === _error.comment) {
val = undefined;
}
// store the unsaved comment with the error itself
_error = _error.update({ newComment: val });
var keepRight = services.keepRight;
if (keepRight) {
keepRight.replaceError(_error); // update keepright cache
}
saveSection
.call(keepRightSaveButtons);
}
}
function keepRightSaveButtons(selection) {
var isSelected = (_error && _error.id === context.selectedErrorID());
var buttonSection = selection.selectAll('.buttons')
.data((isSelected ? [_error] : []), function(d) { return d.status + d.id; });
// exit
buttonSection.exit()
.remove();
// enter
var buttonEnter = buttonSection.enter()
.append('div')
.attr('class', 'buttons');
buttonEnter
.append('button')
.attr('class', 'button comment-button action')
.text(t('QA.keepRight.save_comment'));
buttonEnter
.append('button')
.attr('class', 'button close-button action');
buttonEnter
.append('button')
.attr('class', 'button ignore-button action');
// update
buttonSection = buttonSection
.merge(buttonEnter);
buttonSection.select('.comment-button') // select and propagate data
.attr('disabled', function(d) {
return d.newComment === undefined ? true : null;
})
.on('click.comment', function(d) {
this.blur(); // avoid keeping focus on the button - #4641
var keepRight = services.keepRight;
if (keepRight) {
keepRight.postKeepRightUpdate(d, function(err, error) {
dispatch.call('change', error);
});
}
});
buttonSection.select('.close-button') // select and propagate data
.text(function(d) {
var andComment = (d.newComment !== undefined ? '_comment' : '');
return t('QA.keepRight.close' + andComment);
})
.on('click.close', function(d) {
this.blur(); // avoid keeping focus on the button - #4641
var keepRight = services.keepRight;
if (keepRight) {
d.state = 'ignore_t'; // ignore temporarily (error fixed)
keepRight.postKeepRightUpdate(d, function(err, error) {
dispatch.call('change', error);
});
}
});
buttonSection.select('.ignore-button') // select and propagate data
.text(function(d) {
var andComment = (d.newComment !== undefined ? '_comment' : '');
return t('QA.keepRight.ignore' + andComment);
})
.on('click.ignore', function(d) {
this.blur(); // avoid keeping focus on the button - #4641
var keepRight = services.keepRight;
if (keepRight) {
d.state = 'ignore'; // ignore permanently (false positive)
keepRight.postKeepRightUpdate(d, function(err, error) {
dispatch.call('change', error);
});
}
});
}
keepRightEditor.error = function(val) {
if (!arguments.length) return _error;
_error = val;
return keepRightEditor;
};
return utilRebind(keepRightEditor, dispatch, 'on');
}
+71
View File
@@ -0,0 +1,71 @@
import { dataEn } from '../../data';
import { svgIcon } from '../svg';
import { t } from '../util/locale';
export function uiKeepRightHeader() {
var _error;
function errorTitle(d) {
var unknown = t('inspector.unknown');
if (!d) return unknown;
var errorType = d.error_type;
var parentErrorType = d.parent_error_type;
var et = dataEn.QA.keepRight.errorTypes[errorType];
var pt = dataEn.QA.keepRight.errorTypes[parentErrorType];
if (et && et.title) {
return t('QA.keepRight.errorTypes.' + errorType + '.title');
} else if (pt && pt.title) {
return t('QA.keepRight.errorTypes.' + parentErrorType + '.title');
} else {
return unknown;
}
}
function keepRightHeader(selection) {
var header = selection.selectAll('.kr_error-header')
.data(
(_error ? [_error] : []),
function(d) { return d.id + '-' + (d.status || 0); }
);
header.exit()
.remove();
var headerEnter = header.enter()
.append('div')
.attr('class', 'kr_error-header');
var iconEnter = headerEnter
.append('div')
.attr('class', 'kr_error-header-icon')
.classed('new', function(d) { return d.id < 0; });
iconEnter
.append('div')
.attr('class', function(d) {
return 'preset-icon-28 kr_error kr_error-' + d.id + ' kr_error_type_' + d.parent_error_type;
})
.call(svgIcon('#iD-icon-bolt', 'kr_error-fill'));
headerEnter
.append('div')
.attr('class', 'kr_error-header-label')
.text(errorTitle);
}
keepRightHeader.error = function(val) {
if (!arguments.length) return _error;
_error = val;
return keepRightHeader;
};
return keepRightHeader;
}
+79 -5
View File
@@ -30,6 +30,7 @@ export function uiMapData(context) {
var _dataLayerContainer = d3_select(null);
var _fillList = d3_select(null);
var _featureList = d3_select(null);
var _QAList = d3_select(null);
function showsFeature(d) {
@@ -38,6 +39,7 @@ export function uiMapData(context) {
function autoHiddenFeature(d) {
if (d.type === 'kr_error') return context.errors().autoHidden(d);
return context.features().autoHidden(d);
}
@@ -48,6 +50,22 @@ export function uiMapData(context) {
}
function showsQA(d) {
var QAKeys = [d];
var QALayers = layers.all().filter(function(obj) { return QAKeys.indexOf(obj.id) !== -1; });
var data = QALayers.filter(function(obj) { return obj.layer.supported(); });
function layerSupported(d) {
return d.layer && d.layer.supported();
}
function layerEnabled(d) {
return layerSupported(d) && d.layer.enabled();
}
return layerEnabled(data[0]);
}
function showsFill(d) {
return _fillSelected === d;
}
@@ -207,6 +225,58 @@ export function uiMapData(context) {
}
function drawQAItems(selection) {
var qaKeys = ['keepRight'];
var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; });
var ul = selection
.selectAll('.layer-list-qa')
.data([0]);
ul = ul.enter()
.append('ul')
.attr('class', 'layer-list layer-list-qa')
.merge(ul);
var li = ul.selectAll('.list-item')
.data(qaLayers);
li.exit()
.remove();
var liEnter = li.enter()
.append('li')
.attr('class', function(d) { return 'list-item list-item-' + d.id; });
var labelEnter = liEnter
.append('label')
.each(function(d) {
d3_select(this)
.call(tooltip()
.title(t('map_data.layers.' + d.id + '.tooltip'))
.placement('bottom')
);
});
labelEnter
.append('input')
.attr('type', 'checkbox')
.on('change', function(d) { toggleLayer(d.id); });
labelEnter
.append('span')
.text(function(d) { return t('map_data.layers.' + d.id + '.title'); });
// Update
li
.merge(liEnter)
.classed('active', function (d) { return d.layer.enabled(); })
.selectAll('input')
.property('checked', function (d) { return d.layer.enabled(); });
}
// Beta feature - sample vector layers to support Detroit Mapping Challenge
// https://github.com/osmus/detroit-mapping-challenge
function drawVectorItems(selection) {
@@ -427,10 +497,9 @@ export function uiMapData(context) {
.call(tooltip()
.html(true)
.title(function(d) {
var tip = t(name + '.' + d + '.tooltip'),
key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null);
if (name === 'feature' && autoHiddenFeature(d)) {
var tip = t(name + '.' + d + '.tooltip');
var key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null);
if ((name === 'feature' || name === 'keepRight') && autoHiddenFeature(d)) {
var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden');
tip += '<div>' + msg + '</div>';
}
@@ -461,7 +530,7 @@ export function uiMapData(context) {
.selectAll('input')
.property('checked', active)
.property('indeterminate', function(d) {
return (name === 'feature' && autoHiddenFeature(d));
return ((name === 'feature' || name === 'keepRight') && autoHiddenFeature(d));
});
}
@@ -502,6 +571,7 @@ export function uiMapData(context) {
function update() {
_dataLayerContainer
.call(drawOsmItems)
.call(drawQAItems)
.call(drawPhotoItems)
.call(drawCustomDataItems)
.call(drawVectorItems); // Beta - Detroit mapping challenge
@@ -511,6 +581,9 @@ export function uiMapData(context) {
_featureList
.call(drawListItems, features, 'checkbox', 'feature', clickFeature, showsFeature);
_QAList
.call(drawListItems, ['keep-right'], 'checkbox', 'QA', function(d) { toggleLayer(d); }, showsQA);
}
@@ -611,6 +684,7 @@ export function uiMapData(context) {
.append('div')
.attr('class', 'pane-content');
// data layers
content
.append('div')
+2 -2
View File
@@ -110,9 +110,9 @@ export function uiNoteComments() {
}
noteComments.note = function(_) {
noteComments.note = function(val) {
if (!arguments.length) return _note;
_note = _;
_note = val;
return noteComments;
};
+20 -3
View File
@@ -16,6 +16,8 @@ import {
uiNoteComments,
uiNoteHeader,
uiNoteReport,
uiQuickLinks,
uiTooltipHtml,
uiViewOnOSM,
} from './index';
@@ -27,6 +29,7 @@ import {
export function uiNoteEditor(context) {
var dispatch = d3_dispatch('change');
var quickLinks = uiQuickLinks();
var noteComments = uiNoteComments();
var noteHeader = uiNoteHeader();
@@ -37,6 +40,19 @@ export function uiNoteEditor(context) {
function noteEditor(selection) {
// quick links
var choices = [{
id: 'zoom_to',
label: 'inspector.zoom_to.title',
tooltip: function() {
return uiTooltipHtml(t('inspector.zoom_to.tooltip_note'), t('inspector.zoom_to.key'));
},
click: function zoomTo() {
context.mode().zoomToSelected();
}
}];
var header = selection.selectAll('.header')
.data([0]);
@@ -73,6 +89,7 @@ export function uiNoteEditor(context) {
.attr('class', 'modal-section note-editor')
.merge(editor)
.call(noteHeader.note(_note))
.call(quickLinks.choices(choices))
.call(noteComments.note(_note))
.call(noteSaveSection);
@@ -155,7 +172,7 @@ export function uiNoteEditor(context) {
noteSaveEnter
.append('textarea')
.attr('id', 'new-comment-input')
.attr('class', 'new-comment-input')
.attr('placeholder', t('note.inputPlaceholder'))
.attr('maxlength', 1000)
.property('value', function(d) { return d.newComment; })
@@ -425,9 +442,9 @@ export function uiNoteEditor(context) {
}
noteEditor.note = function(_) {
noteEditor.note = function(val) {
if (!arguments.length) return _note;
_note = _;
_note = val;
return noteEditor;
};
+2 -2
View File
@@ -49,9 +49,9 @@ export function uiNoteHeader() {
}
noteHeader.note = function(_) {
noteHeader.note = function(val) {
if (!arguments.length) return _note;
_note = _;
_note = val;
return noteHeader;
};
+10 -13
View File
@@ -1,23 +1,20 @@
import { t } from '../util/locale';
import { osmNote } from '../osm';
import { services } from '../services';
import { svgIcon } from '../svg';
import {
osmNote
} from '../osm';
export function uiNoteReport() {
var _note;
var url = 'https://www.openstreetmap.org/reports/new?reportable_id=';
function noteReport(selection) {
var url;
if (services.osm && (_note instanceof osmNote) && (!_note.isNew())) {
url = services.osm.noteReportURL(_note);
}
if (!(_note instanceof osmNote)) return;
url += _note.id + '&reportable_type=Note';
var data = ((!_note || _note.isNew()) ? [] : [_note]);
var link = selection.selectAll('.note-report')
.data(data, function(d) { return d.id; });
.data(url ? [url] : []);
// exit
link.exit()
@@ -28,7 +25,7 @@ export function uiNoteReport() {
.append('a')
.attr('class', 'note-report')
.attr('target', '_blank')
.attr('href', url)
.attr('href', function(d) { return d; })
.call(svgIcon('#iD-icon-out-link', 'inline'));
linkEnter
@@ -37,9 +34,9 @@ export function uiNoteReport() {
}
noteReport.note = function(_) {
noteReport.note = function(val) {
if (!arguments.length) return _note;
_note = _;
_note = val;
return noteReport;
};
+3 -3
View File
@@ -87,8 +87,9 @@ export function uiPresetEditor(context) {
selection
.call(formFields
.fieldsArr(_fieldsArr)
.state(_state),
'inspector-inner fillL3');
.state(_state)
.klass('inspector-inner fillL3')
);
selection.selectAll('.wrap-form-field input')
@@ -120,7 +121,6 @@ export function uiPresetEditor(context) {
presetEditor.tags = function(val) {
if (!arguments.length) return _tags;
_tags = val;
formFields.tagsChanged();
// Don't reset _fieldsArr here.
return presetEditor;
};
+62
View File
@@ -0,0 +1,62 @@
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import { t } from '../util/locale';
import { tooltip } from '../util/tooltip';
export function uiQuickLinks() {
var _choices = [];
function quickLinks(selection) {
var container = selection.selectAll('.quick-links')
.data([0]);
container = container.enter()
.append('div')
.attr('class', 'quick-links')
.merge(container);
var items = container.selectAll('.quick-link')
.data(_choices, function(d) { return d.id; });
items.exit()
.remove();
items.enter()
.append('a')
.attr('class', function(d) { return 'quick-link quick-link-' + d.id; })
.attr('href', '#')
.text(function(d) { return t(d.label); })
.each(function(d) {
if (typeof d.tooltip !== 'function') return;
d3_select(this)
.call(tooltip().html(true).title(d.tooltip).placement('bottom'));
})
.on('click', function(d) {
if (typeof d.click !== 'function') return;
d3_event.preventDefault();
d.click(d);
});
}
// val should be an array of choices like:
// [{
// id: 'link-id',
// label: 'translation.key',
// tooltip: function(d),
// click: function(d)
// }, ..]
quickLinks.choices = function(val) {
if (!arguments.length) return _choices;
_choices = val;
return quickLinks;
};
return quickLinks;
}
+11 -7
View File
@@ -1,3 +1,5 @@
import _uniq from 'lodash-es/uniq';
import {
select as d3_select,
selectAll as d3_selectAll
@@ -19,7 +21,7 @@ export function uiShortcuts(context) {
context.keybinding()
.on(t('shortcuts.toggle.key'), function () {
.on([t('shortcuts.toggle.key'), '?'], function () {
if (d3_selectAll('.modal-shortcuts').size()) { // already showing
if (_modalSelection) {
_modalSelection.close();
@@ -179,7 +181,12 @@ export function uiShortcuts(context) {
arr = ['F11'];
}
return arr.map(function(s) {
// replace translations
arr = arr.map(function(s) {
return uiCmd.display(s.indexOf('.') !== -1 ? t(s) : s);
});
return _uniq(arr).map(function(s) {
return {
shortcut: s,
separator: d.separator
@@ -191,17 +198,14 @@ export function uiShortcuts(context) {
var selection = d3_select(this);
var click = d.shortcut.toLowerCase().match(/(.*).click/);
if (click && click[1]) {
if (click && click[1]) { // replace "left_click", "right_click" with mouse icon
selection
.call(svgIcon('#iD-walkthrough-mouse', 'mouseclick', click[1]));
} else {
selection
.append('kbd')
.attr('class', 'shortcut')
.text(function (d) {
var key = d.shortcut;
return key.indexOf('.') !== -1 ? uiCmd.display(t(key)) : uiCmd.display(key);
});
.text(function (d) { return d.shortcut; });
}
if (i < nodes.length - 1) {
+30 -13
View File
@@ -9,18 +9,9 @@ import {
selectAll as d3_selectAll
} from 'd3-selection';
import {
osmEntity,
osmNote
} from '../osm';
import {
uiDataEditor,
uiFeatureList,
uiInspector,
uiNoteEditor
} from './index';
import { osmEntity, osmNote, krError } from '../osm';
import { services } from '../services';
import { uiDataEditor, uiFeatureList, uiInspector, uiNoteEditor, uiKeepRightEditor } from './index';
import { textDirection } from '../util/locale';
@@ -28,9 +19,11 @@ export function uiSidebar(context) {
var inspector = uiInspector(context);
var dataEditor = uiDataEditor(context);
var noteEditor = uiNoteEditor(context);
var keepRightEditor = uiKeepRightEditor(context);
var _current;
var _wasData = false;
var _wasNote = false;
var _wasKRError = false;
function sidebar(selection) {
@@ -127,12 +120,34 @@ export function uiSidebar(context) {
if (context.mode().id === 'drag-note') return;
_wasNote = true;
var osm = services.osm;
if (osm) {
datum = osm.getNote(datum.id); // marker may contain stale data - get latest
}
sidebar
.show(noteEditor.note(datum));
selection.selectAll('.sidebar-component')
.classed('inspector-hover', true);
} else if (datum instanceof krError) {
_wasKRError = true;
var keepRight = services.keepRight;
if (keepRight) {
datum = keepRight.getError(datum.id); // marker may contain stale data - get latest
}
d3_selectAll('.kr_error')
.classed('hover', function(d) { return d.id === datum.id; });
sidebar
.show(keepRightEditor.error(datum));
selection.selectAll('.sidebar-component')
.classed('inspector-hover', true);
} else if (!_current && (datum instanceof osmEntity)) {
featureListWrap
.classed('inspector-hidden', true);
@@ -158,10 +173,12 @@ export function uiSidebar(context) {
inspector
.state('hide');
} else if (_wasData || _wasNote) {
} else if (_wasData || _wasNote || _wasKRError) {
_wasNote = false;
_wasData = false;
_wasKRError = false;
d3_selectAll('.note').classed('hover', false);
d3_selectAll('.kr_error').classed('hover', false);
sidebar.hide();
}
}
+49 -41
View File
@@ -1,6 +1,3 @@
import _find from 'lodash-es/find';
import _omit from 'lodash-es/omit';
import {
event as d3_event,
select as d3_select
@@ -10,10 +7,11 @@ import { t } from '../util/locale';
import { utilDetect } from '../util/detect';
import { services } from '../services';
import { svgIcon } from '../svg';
import { utilQsString } from '../util';
export function uiTagReference(tag) {
var taginfo = services.taginfo;
var wikibase = services.osmWikibase;
var tagReference = {};
var _button = d3_select(null);
@@ -21,42 +19,49 @@ export function uiTagReference(tag) {
var _loaded;
var _showing;
/**
* @returns {{itemTitle: String, description: String, image: String|null}|null}
**/
function findLocal(data) {
var locale = utilDetect().locale.toLowerCase();
var localized;
var entity = data.tag || data.key;
if (!entity) return null;
if (locale !== 'pt-br') { // see #3776, prefer 'pt' over 'pt-br'
localized = _find(data, function(d) {
return d.lang.toLowerCase() === locale;
});
if (localized) return localized;
var result = {
title: entity.title,
description: entity.description,
};
if (entity.claims) {
var langCode = utilDetect().locale.toLowerCase();
var url;
var image = wikibase.claimToValue(entity, 'P4', langCode);
if (image) {
url = 'https://commons.wikimedia.org/w/index.php';
} else {
image = wikibase.claimToValue(entity, 'P28', langCode);
if (image) {
url = 'https://wiki.openstreetmap.org/w/index.php';
}
}
if (image) {
result.image = {
url: url,
title: 'Special:Redirect/file/' + image
};
}
}
// try the non-regional version of a language, like
// 'en' if the language is 'en-US'
if (locale.indexOf('-') !== -1) {
var first = locale.split('-')[0];
localized = _find(data, function(d) {
return d.lang.toLowerCase() === first;
});
if (localized) return localized;
}
// finally fall back to english
return _find(data, function(d) {
return d.lang.toLowerCase() === 'en';
});
return result;
}
function load(param) {
if (!taginfo) return;
if (!wikibase) return;
_button
.classed('tag-reference-loading', true);
taginfo.docs(param, function show(err, data) {
wikibase.getEntity(param, function show(err, data) {
var docs;
if (!err && data) {
docs = findLocal(data);
@@ -65,23 +70,25 @@ export function uiTagReference(tag) {
_body.html('');
if (!docs || !docs.title) {
if (param.hasOwnProperty('value')) {
load(_omit(param, 'value')); // retry with key only
} else {
_body
.append('p')
.attr('class', 'tag-reference-description')
.text(t('inspector.no_documentation_key'));
done();
}
_body
.append('p')
.attr('class', 'tag-reference-description')
.text(t('inspector.no_documentation_key'));
done();
return;
}
if (docs.image && docs.image.thumb_url_prefix) {
if (docs.image) {
var imageUrl = docs.image.url + '?' + utilQsString({
title: docs.image.title,
width: 100,
height: 100,
});
_body
.append('img')
.attr('class', 'tag-reference-wiki-image')
.attr('src', docs.image.thumb_url_prefix + '100' + docs.image.thumb_url_suffix)
.attr('src', imageUrl)
.on('load', function() { done(); })
.on('error', function() { d3_select(this).remove(); done(); });
} else {
@@ -91,7 +98,7 @@ export function uiTagReference(tag) {
_body
.append('p')
.attr('class', 'tag-reference-description')
.text(docs.description || t('inspector.documentation_redirect'));
.text(docs.description || t('inspector.no_documentation_key'));
_body
.append('a')
@@ -101,7 +108,7 @@ export function uiTagReference(tag) {
.attr('href', 'https://wiki.openstreetmap.org/wiki/' + docs.title)
.call(svgIcon('#iD-icon-out-link', 'inline'))
.append('span')
.text(t('inspector.reference'));
.text(t('inspector.edit_reference'));
// Add link to info about "good changeset comments" - #2923
if (param.key === 'comment') {
@@ -171,6 +178,7 @@ export function uiTagReference(tag) {
} else if (_loaded) {
done();
} else {
tag.langCode = utilDetect().locale.toLowerCase();
load(tag);
}
});
+45
View File
@@ -0,0 +1,45 @@
import { t } from '../util/locale';
import { services } from '../services';
import { svgIcon } from '../svg';
import { krError } from '../osm';
export function uiViewOnKeepRight() {
var _error; // a keepright error
function viewOnKeepRight(selection) {
var url;
if (services.keepRight && (_error instanceof krError)) {
url = services.keepRight.errorURL(_error);
}
var link = selection.selectAll('.view-on-keepRight')
.data(url ? [url] : []);
// exit
link.exit()
.remove();
// enter
var linkEnter = link.enter()
.append('a')
.attr('class', 'view-on-keepRight')
.attr('target', '_blank')
.attr('href', function(d) { return d; })
.call(svgIcon('#iD-icon-out-link', 'inline'));
linkEnter
.append('span')
.text(t('inspector.view_on_keepRight'));
}
viewOnKeepRight.what = function(val) {
if (!arguments.length) return _error;
_error = val;
return viewOnKeepRight;
};
return viewOnKeepRight;
}
+1 -4
View File
@@ -1,9 +1,6 @@
import { t } from '../util/locale';
import { osmEntity, osmNote } from '../osm';
import { svgIcon } from '../svg';
import {
osmEntity,
osmNote
} from '../osm';
export function uiViewOnOSM(context) {
+22 -16
View File
@@ -9,7 +9,8 @@ export function utilDetect(force) {
detected = {};
var ua = navigator.userAgent,
m = null;
m = null,
q = utilStringQs(window.location.hash.substring(1));
m = ua.match(/(edge)\/?\s*(\.?\d+(\.\d+)*)/i); // Edge
if (m !== null) {
@@ -59,24 +60,30 @@ export function utilDetect(force) {
// Added due to incomplete svg style support. See #715
detected.opera = (detected.browser.toLowerCase() === 'opera' && parseFloat(detected.version) < 15 );
detected.locale = (navigator.language || navigator.userLanguage || 'en-US');
detected.language = detected.locale.split('-')[0];
// Set locale based on url param (format 'en-US') or browser lang (default)
if (q.hasOwnProperty('locale')) {
detected.locale = q.locale;
detected.language = q.locale.split('-')[0];
} else {
detected.locale = (navigator.language || navigator.userLanguage || 'en-US');
detected.language = detected.locale.split('-')[0];
// Search `navigator.languages` for a better locale.. Prefer the first language,
// unless the second language is a culture-specific version of the first one, see #3842
if (navigator.languages && navigator.languages.length > 0) {
var code0 = navigator.languages[0],
parts0 = code0.split('-');
// Search `navigator.languages` for a better locale. Prefer the first language,
// unless the second language is a culture-specific version of the first one, see #3842
if (navigator.languages && navigator.languages.length > 0) {
var code0 = navigator.languages[0],
parts0 = code0.split('-');
detected.locale = code0;
detected.language = parts0[0];
detected.locale = code0;
detected.language = parts0[0];
if (navigator.languages.length > 1 && parts0.length === 1) {
var code1 = navigator.languages[1],
parts1 = code1.split('-');
if (navigator.languages.length > 1 && parts0.length === 1) {
var code1 = navigator.languages[1],
parts1 = code1.split('-');
if (parts1[0] === parts0[0]) {
detected.locale = code1;
if (parts1[0] === parts0[0]) {
detected.locale = code1;
}
}
}
}
@@ -90,7 +97,6 @@ export function utilDetect(force) {
}
// detect text direction
var q = utilStringQs(window.location.hash.substring(1));
var lang = dataLocales[detected.locale];
if ((lang && lang.rtl) || (q.rtl === 'true')) {
detected.textDirection = 'rtl';
+1 -1
View File
@@ -5,6 +5,7 @@ export { utilDisplayName } from './util';
export { utilDisplayNameForPath } from './util';
export { utilDisplayType } from './util';
export { utilDisplayLabel } from './util';
export { utilEntityRoot } from './util';
export { utilEditDistance } from './util';
export { utilEntitySelector } from './util';
export { utilEntityOrMemberSelector } from './util';
@@ -28,7 +29,6 @@ export { utilRebind } from './rebind';
export { utilSetTransform } from './util';
export { utilSessionMutex } from './session_mutex';
export { utilStringQs } from './util';
// export { utilSuggestNames } from './suggest_names';
export { utilTagText } from './util';
export { utilTiler } from './tiler';
export { utilTriggerEvent } from './trigger_event';
+3 -2
View File
@@ -1,4 +1,5 @@
import _isFunction from 'lodash-es/isFunction';
import _uniq from 'lodash-es/uniq';
import {
event as d3_event,
@@ -125,7 +126,7 @@ export function utilKeybinding(namespace) {
// Remove one or more keycode bindings.
keybinding.off = function(codes, capture) {
var arr = [].concat(codes);
var arr = _uniq([].concat(codes));
for (var i = 0; i < arr.length; i++) {
var id = arr[i] + (capture ? '-capture' : '-bubble');
@@ -141,7 +142,7 @@ export function utilKeybinding(namespace) {
return keybinding.off(codes, capture);
}
var arr = [].concat(codes);
var arr = _uniq([].concat(codes));
for (var i = 0; i < arr.length; i++) {
var id = arr[i] + (capture ? '-capture' : '-bubble');
+3 -1
View File
@@ -42,7 +42,9 @@ export function t(s, o, loc) {
if (rep !== undefined) {
if (o) {
for (var k in o) {
rep = rep.replace('{' + k + '}', o[k]);
var variable = '{' + k + '}';
var re = new RegExp(variable, 'g'); // check globally for variables
rep = rep.replace(re, o[k]);
}
}
return rep;
+50 -32
View File
@@ -37,6 +37,7 @@ export function utilEntityOrMemberSelector(ids, graph) {
export function utilEntityOrDeepMemberSelector(ids, graph) {
var seen = {};
var allIDs = [];
function addEntityAndMembersIfNotYetSeen(id) {
// avoid infinite recursion for circular relations by skipping seen entities
if (seen[id]) return;
@@ -53,6 +54,7 @@ export function utilEntityOrDeepMemberSelector(ids, graph) {
}
}
}
ids.forEach(function(id) {
addEntityAndMembersIfNotYetSeen(id);
});
@@ -85,9 +87,9 @@ export function utilGetAllNodes(ids, graph) {
export function utilDisplayName(entity) {
var localizedNameKey = 'name:' + utilDetect().locale.toLowerCase().split('-')[0],
name = entity.tags[localizedNameKey] || entity.tags.name || '',
network = entity.tags.cycle_network || entity.tags.network;
var localizedNameKey = 'name:' + utilDetect().locale.toLowerCase().split('-')[0];
var name = entity.tags[localizedNameKey] || entity.tags.name || '';
var network = entity.tags.cycle_network || entity.tags.network;
if (!name && entity.tags.ref) {
name = entity.tags.ref;
@@ -137,6 +139,15 @@ export function utilDisplayLabel(entity, context) {
}
export function utilEntityRoot(entityType) {
return {
node: 'n',
way: 'w',
relation: 'r'
}[entityType];
}
export function utilStringQs(str) {
return str.split('&').reduce(function(obj, pair){
var parts = pair.split('=');
@@ -152,11 +163,12 @@ export function utilStringQs(str) {
export function utilQsString(obj, noencode) {
// encode everything except special characters used in certain hash parameters:
// "/" in map states, ":", ",", {" and "}" in background
function softEncode(s) {
// encode everything except special characters used in certain hash parameters:
// "/" in map states, ":", ",", {" and "}" in background
return encodeURIComponent(s).replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent);
return encodeURIComponent(s).replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent);
}
return Object.keys(obj).sort().map(function(key) {
return encodeURIComponent(key) + '=' + (
noencode ? softEncode(obj[key]) : encodeURIComponent(obj[key]));
@@ -165,36 +177,41 @@ export function utilQsString(obj, noencode) {
export function utilPrefixDOMProperty(property) {
var prefixes = ['webkit', 'ms', 'moz', 'o'],
i = -1,
n = prefixes.length,
s = document.body;
var prefixes = ['webkit', 'ms', 'moz', 'o'];
var i = -1;
var n = prefixes.length;
var s = document.body;
if (property in s)
return property;
property = property.substr(0, 1).toUpperCase() + property.substr(1);
while (++i < n)
if (prefixes[i] + property in s)
while (++i < n) {
if (prefixes[i] + property in s) {
return prefixes[i] + property;
}
}
return false;
}
export function utilPrefixCSSProperty(property) {
var prefixes = ['webkit', 'ms', 'Moz', 'O'],
i = -1,
n = prefixes.length,
s = document.body.style;
var prefixes = ['webkit', 'ms', 'Moz', 'O'];
var i = -1;
var n = prefixes.length;
var s = document.body.style;
if (property.toLowerCase() in s)
if (property.toLowerCase() in s) {
return property.toLowerCase();
}
while (++i < n)
if (prefixes[i] + property in s)
while (++i < n) {
if (prefixes[i] + property in s) {
return '-' + prefixes[i].toLowerCase() + property.replace(/([A-Z])/g, '-$1').toLowerCase();
}
}
return false;
}
@@ -202,10 +219,9 @@ export function utilPrefixCSSProperty(property) {
var transformProperty;
export function utilSetTransform(el, x, y, scale) {
var prop = transformProperty = transformProperty || utilPrefixCSSProperty('Transform'),
translate = utilDetect().opera ?
'translate(' + x + 'px,' + y + 'px)' :
'translate3d(' + x + 'px,' + y + 'px,0)';
var prop = transformProperty = transformProperty || utilPrefixCSSProperty('Transform');
var translate = utilDetect().opera ? 'translate(' + x + 'px,' + y + 'px)'
: 'translate3d(' + x + 'px,' + y + 'px,0)';
return el.style(prop, translate + (scale ? ' scale(' + scale + ')' : ''));
}
@@ -240,11 +256,12 @@ export function utilEditDistance(a, b) {
// 1. Only works on HTML elements, not SVG
// 2. Does not cause style recalculation
export function utilFastMouse(container) {
var rect = container.getBoundingClientRect(),
rectLeft = rect.left,
rectTop = rect.top,
clientLeft = +container.clientLeft,
clientTop = +container.clientTop;
var rect = container.getBoundingClientRect();
var rectLeft = rect.left;
var rectTop = rect.top;
var clientLeft = +container.clientLeft;
var clientTop = +container.clientTop;
if (textDirection === 'rtl') {
rectLeft = 0;
}
@@ -262,9 +279,9 @@ export var utilGetPrototypeOf = Object.getPrototypeOf || function(obj) { return
export function utilAsyncMap(inputs, func, callback) {
var remaining = inputs.length,
results = [],
errors = [];
var remaining = inputs.length;
var results = [];
var errors = [];
inputs.forEach(function(d, i) {
func(d, function done(err, data) {
@@ -279,8 +296,9 @@ export function utilAsyncMap(inputs, func, callback) {
// wraps an index to an interval [0..length-1]
export function utilWrap(index, length) {
if (index < 0)
if (index < 0) {
index += Math.ceil(-index/length)*length;
}
return index % length;
}
+2 -2
View File
@@ -14,8 +14,8 @@ export function validationDeprecatedTag() {
var validation = function(changes) {
var issues = [];
for (var i = 0; i < changes.created.length; i++) {
var change = changes.created[i],
deprecatedTags = change.deprecatedTags();
var change = changes.created[i];
var deprecatedTags = change.deprecatedTags();
if (!_isEmpty(deprecatedTags)) {
var tags = utilTagText({ tags: deprecatedTags });
+51
View File
@@ -0,0 +1,51 @@
import { t } from '../util/locale';
import { discardNames } from '../../node_modules/name-suggestion-index/config/filters.json';
export function validationGenericName() {
function isGenericName(entity) {
var name = entity.tags.name;
if (!name) return false;
var i, re;
// test if the name is just the tag value (e.g. "park")
var keys = ['amenity', 'leisure', 'shop', 'man_made', 'tourism'];
for (i = 0; i < keys.length; i++) {
var val = entity.tags[keys[i]];
if (val && val.replace(/\_/g, ' ').toLowerCase() === name.toLowerCase()) {
return name;
}
}
// test if the name is a generic name (e.g. "pizzaria")
for (i = 0; i < discardNames.length; i++) {
re = new RegExp(discardNames[i], 'i');
if (re.test(name)) {
return name;
}
}
return false;
}
return function validation(changes) {
var warnings = [];
for (var i = 0; i < changes.created.length; i++) {
var change = changes.created[i];
var generic = isGenericName(change);
if (generic) {
warnings.push({
id: 'generic_name',
message: t('validations.generic_name'),
tooltip: t('validations.generic_name_tooltip', { name: generic }),
entity: change
});
}
}
return warnings;
};
}
+1
View File
@@ -3,6 +3,7 @@ export { validationDisconnectedHighway } from './disconnected_highway';
export { validationHighwayCrossingOtherWays } from './crossing_ways';
export { validationHighwayAlmostJunction } from './highway_almost_junction';
export { ValidationIssueType, ValidationIssueSeverity } from './validation_issue';
export { validationGenericName } from './generic_name.js';
export { validationManyDeletions } from './many_deletions';
export { validationMapCSSChecks } from './mapcss_checks';
export { validationMissingTag } from './missing_tag';
+5 -5
View File
@@ -10,13 +10,13 @@ export function validationManyDeletions() {
var validation = function(changes, graph) {
var issues = [];
var nodes=0, ways=0, areas=0, relations=0;
var nodes = 0, ways = 0, areas = 0, relations = 0;
changes.deleted.forEach(function(c) {
if (c.type === 'node') {nodes++;}
else if (c.type === 'way' && c.geometry(graph) === 'line') {ways++;}
else if (c.type === 'way' && c.geometry(graph) === 'area') {areas++;}
else if (c.type === 'relation') {relations++;}
if (c.type === 'node') { nodes++; }
else if (c.type === 'way' && c.geometry(graph) === 'line') { ways++; }
else if (c.type === 'way' && c.geometry(graph) === 'area') { areas++; }
else if (c.type === 'relation') { relations++; }
});
if (changes.deleted.length > threshold) {
issues.push(new validationIssue({
+2
View File
@@ -21,5 +21,7 @@ export function validationMapCSSChecks() {
return issues;
};
return validation;
}
+4 -4
View File
@@ -20,12 +20,12 @@ export function validationMissingTag(context) {
}
var validation = function(changes, graph) {
var types = ['point', 'line', 'area', 'relation'],
issues = [];
var types = ['point', 'line', 'area', 'relation'];
var issues = [];
for (var i = 0; i < changes.created.length; i++) {
var change = changes.created[i],
geometry = change.geometry(graph);
var change = changes.created[i];
var geometry = change.geometry(graph);
if (types.indexOf(geometry) !== -1 && !hasTags(change, graph)) {
var entityLabel = utilDisplayLabel(change, context);
+3 -3
View File
@@ -32,9 +32,9 @@ export function validationTagSuggestsArea() {
var validation = function(changes, graph) {
var issues = [];
for (var i = 0; i < changes.created.length; i++) {
var change = changes.created[i],
geometry = change.geometry(graph),
suggestion = (geometry === 'line' ? tagSuggestsArea(change.tags) : undefined);
var change = changes.created[i];
var geometry = change.geometry(graph);
var suggestion = (geometry === 'line' ? tagSuggestsArea(change.tags) : undefined);
if (suggestion) {
issues.push(new validationIssue({