Merge branch 'keep-right_QA'

This commit is contained in:
Bryan Housel
2019-01-04 21:37:19 -05:00
47 changed files with 2735 additions and 183 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));
}
+7
View File
@@ -263,6 +263,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) {
+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';
-1
View File
@@ -1,4 +1,3 @@
import { geoBounds as d3_geoBounds } from 'd3-geo';
import {
+126
View File
@@ -0,0 +1,126 @@
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import {
behaviorBreathe,
behaviorHover,
behaviorLasso,
behaviorSelect
} from '../behavior';
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.enter = function() {
// 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));
}
var error = checkSelectedID();
if (!error) return;
behaviors.forEach(context.install);
keybinding.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);
};
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;
}
+1
View File
@@ -72,6 +72,7 @@ export function modeSelectNote(context, selectedNoteID) {
} else {
selection
.classed('selected', true);
context.selectedNoteID(selectedNoteID);
}
}
+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)}
}
});
+3 -2
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));
}
+3
View File
@@ -1,3 +1,4 @@
import serviceKeepRight from './keepRight';
import serviceMapillary from './mapillary';
import serviceMapRules from './maprules';
import serviceNominatim from './nominatim';
@@ -13,6 +14,7 @@ import serviceWikipedia from './wikipedia';
export var services = {
geocoder: serviceNominatim,
keepRight: serviceKeepRight,
mapillary: serviceMapillary,
openstreetcam: serviceOpenstreetcam,
osm: serviceOsm,
@@ -26,6 +28,7 @@ export var services = {
};
export {
serviceKeepRight,
serviceMapillary,
serviceMapRules,
serviceNominatim,
+454
View File
@@ -0,0 +1,454 @@
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 } from '../../data/keepRight.json';
var tiler = utilTiler();
var dispatch = d3_dispatch('loaded');
var _krCache;
var _krZoom = 14;
var _krUrlRoot = 'https://www.keepright.at/';
var _krLocalize = {
node: 'node',
way: 'way',
relation: 'relation',
highway: 'highway',
railway: 'railway',
waterway: 'waterway',
cycleway: 'cycleway',
footpath: 'footpath',
'cycleway/footpath': 'cycleway_footpath',
riverbank: 'riverbank',
bridge: 'bridge',
tunnel: 'tunnel',
place_of_worship: 'place_of_worship',
pub: 'pub',
restaurant: 'restaurant',
school: 'school',
university: 'university',
hospital: 'hospital',
library: 'library',
theatre: 'theatre',
courthouse: 'courthouse',
bank: 'bank',
cinema: 'cinema',
pharmacy: 'pharmacy',
cafe: 'cafe',
fast_food: 'fast_food',
fuel: 'fuel',
from: 'from',
to: 'to'
};
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 as groups
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 group = errorMatch[i];
var idType;
idType = 'IDs' in errorTemplate ? errorTemplate.IDs[i-1] : '';
if (idType && group) { // link IDs if present in the group
group = parseError(group, idType);
} else if (htmlRegex.test(group)) { // escape any html in non-IDs
group = '\\' + group + '\\';
} else if (_krLocalize[group]) { // some replacement strings can be localized
group = t('QA.keepRight.error_parts.' + _krLocalize[group]);
}
replacements['var' + i] = group;
}
return replacements;
}
function parseError(group, idType) {
function linkEntity(d) {
return '<span><a class="kr_error_description-id">' + d + '</a></span>';
}
// arbitrary node list of form: #ID, #ID, #ID...
function parseError211(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 parseError231(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 parseError294(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 parseError370(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 parseWarning20(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(', ');
}
switch (idType) {
// simple case just needs a linking span
case 'n':
case 'w':
case 'r':
group = linkEntity(idType + group);
break;
// some errors have more complex ID lists/variance
case '211':
group = parseError211(group);
break;
case '231':
group = parseError231(group);
break;
case '294':
group = parseError294(group);
break;
case '370':
group = parseError370(group);
break;
case '20':
group = parseWarning20(group);
}
return group;
}
export default {
init: function() {
if (!_krCache) {
this.reset();
}
this.event = utilRebind(this, dispatch, 'on');
},
reset: function() {
if (_krCache) {
_forEach(_krCache.inflight, abortRequest);
}
_krCache = { loaded: {}, inflight: {}, keepRight: {}, 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;
// - 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.00002, 0] : [0, 0.00002];
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,
object_id: props.object_id,
object_type: props.object_type,
schema: props.schema,
title: props.title
});
d.replacements = tokenReplacements(d);
_krCache.keepRight[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' || d.state === 'ignore_t') {
that.removeError(d);
} 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.keepRight[id];
},
// replace a single error in the cache
replaceError: function(error) {
if (!(error instanceof krError) || !error.id) return;
_krCache.keepRight[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.keepRight[error.id];
updateRtree(encodeErrorRtree(error), false); // false = remove
},
errorURL: function(error) {
return _krUrlRoot + 'report_map.php?schema=' + error.schema + '&error=' + error.id;
}
};
+5
View File
@@ -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) {
+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';
+241
View File
@@ -0,0 +1,241 @@
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 : 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; });
+9
View File
@@ -180,6 +180,13 @@ export function uiHelp(context) {
'using',
'tracing',
'upload'
]],
['qa', [
'intro',
'tools_h',
'tools',
'issues_h',
'issues'
]]
];
@@ -227,6 +234,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 = {
+4
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';
@@ -64,4 +67,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';
+6 -6
View File
@@ -92,9 +92,9 @@ export function uiInspector(context) {
}
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;
};
+80
View File
@@ -0,0 +1,80 @@
import { event as d3_event } from 'd3-selection';
import { dataEn } from '../../data';
import { t } from '../util/locale';
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];
if (et && et.description) {
return t('QA.keepRight.errorTypes.' + errorType + '.description', d.replacements);
} else if (pt && pt.description) {
return t('QA.keepRight.errorTypes.' + parentErrorType + '.description', d.replacements);
} else {
return unknown;
}
}
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 description = detailsEnter
.append('div')
.attr('class', 'kr_error-details-description');
description
.append('h4')
.text(function() { return t('QA.keepRight.detail_description'); });
description
.append('div')
.attr('class', 'kr_error-details-description-text')
.html(errorDetail);
description.selectAll('.kr_error_description-id')
.on('click', function() { clickLink(context, this.text); });
function clickLink(context, entityID) {
d3_event.preventDefault();
context.layers().layer('osm').enabled(true);
context.zoomToEntity(entityID);
}
}
keepRightDetails.error = function(val) {
if (!arguments.length) return _error;
_error = val;
return keepRightDetails;
};
return keepRightDetails;
}
+221
View File
@@ -0,0 +1,221 @@
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, 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 _error;
function keepRightEditor(selection) {
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('.error-editor')
.data([0]);
editor.enter()
.append('div')
.attr('class', 'modal-section keepRight-editor')
.merge(editor)
.call(keepRightHeader.error(_error))
.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;
}
+78 -2
View File
@@ -29,6 +29,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) {
@@ -37,6 +38,7 @@ export function uiMapData(context) {
function autoHiddenFeature(d) {
if (d.type === 'kr_error') return context.errors().autoHidden(d);
return context.features().autoHidden(d);
}
@@ -47,6 +49,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;
}
@@ -206,6 +224,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) {
@@ -429,7 +499,8 @@ export function uiMapData(context) {
var tip = t(name + '.' + d + '.tooltip'),
key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null);
if (name === 'feature' && autoHiddenFeature(d)) {
if ((name === 'feature' || name === 'keepRight') && autoHiddenFeature(d)) {
var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden');
tip += '<div>' + msg + '</div>';
}
@@ -460,7 +531,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));
});
}
@@ -501,6 +572,7 @@ export function uiMapData(context) {
function update() {
_dataLayerContainer
.call(drawOsmItems)
.call(drawQAItems)
.call(drawPhotoItems)
.call(drawCustomDataItems)
.call(drawVectorItems); // Beta - Detroit mapping challenge
@@ -510,6 +582,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);
}
@@ -609,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;
};
+3 -3
View File
@@ -155,7 +155,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 +425,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;
};
+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();
}
}
+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) {
+1 -1
View File
@@ -4,6 +4,7 @@ export { utilCleanTags } from './clean_tags';
export { utilDisplayName } from './util';
export { utilDisplayNameForPath } from './util';
export { utilDisplayType } from './util';
export { utilEntityRoot } from './util';
export { utilEditDistance } from './util';
export { utilEntitySelector } from './util';
export { utilEntityOrMemberSelector } from './util';
@@ -27,7 +28,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 -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;
+9
View File
@@ -121,6 +121,15 @@ export function utilDisplayType(id) {
}
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('=');