diff --git a/data/keepRight.json b/data/keepRight.json index a5ef35c71..52b47b638 100644 --- a/data/keepRight.json +++ b/data/keepRight.json @@ -1,4 +1,35 @@ { + "localizeStrings": { + "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" + }, "errorTypes": { "20": { "title": "multiple nodes on the same spot", diff --git a/modules/services/keepRight.js b/modules/services/keepRight.js index 3eb853e02..f25bd1494 100644 --- a/modules/services/keepRight.js +++ b/modules/services/keepRight.js @@ -13,7 +13,7 @@ import { krError } from '../osm'; import { t } from '../util/locale'; import { utilRebind, utilTiler, utilQsString } from '../util'; -import { errorTypes } from '../../data/keepRight.json'; +import { errorTypes, localizeStrings } from '../../data/keepRight.json'; var tiler = utilTiler(); @@ -22,37 +22,6 @@ 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 @@ -119,7 +88,7 @@ function tokenReplacements(d) { // some descriptions are just fixed text if (!errorTemplate.regex) return; - // regex pattern should match description with variable details captured as groups + // regex pattern should match description with variable details captured var errorRegex = new RegExp(errorTemplate.regex, 'i'); var errorMatch = errorRegex.exec(d.description); if (!errorMatch) { @@ -132,33 +101,62 @@ function tokenReplacements(d) { } for (var i = 1; i < errorMatch.length; i++) { // skip first - var group = errorMatch[i]; + var capture = 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]); + 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 if (localizeStrings[capture]) { // some replacement strings can be localized + capture = t('QA.keepRight.error_parts.' + localizeStrings[capture]); } - replacements['var' + i] = group; + replacements['var' + i] = capture; } return replacements; } -function parseError(group, idType) { +function parseError(capture, idType) { + + switch (idType) { + // simple case just needs a linking span + 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 linkEntity(d) { - return '' + d + ''; + return '' + d + ''; } // arbitrary node list of form: #ID, #ID, #ID... - function parseError211(capture) { + function parse211(capture) { var newList = []; var items = capture.split(', '); @@ -172,7 +170,7 @@ function parseError(group, idType) { } // arbitrary way list of form: #ID(layer),#ID(layer),#ID(layer)... - function parseError231(capture) { + function parse231(capture) { var newList = []; // unfortunately 'layer' can itself contain commas, so we split on '),' var items = capture.split('),'); @@ -190,7 +188,7 @@ function parseError(group, idType) { } // arbitrary node/relation list of form: from node #ID,to relation #ID,to node #ID... - function parseError294(capture) { + function parse294(capture) { var newList = []; var items = capture.split(','); @@ -220,7 +218,7 @@ function parseError(group, idType) { } // may or may not include the string "(including the name 'name')" - function parseError370(capture) { + function parse370(capture) { if (!capture) return ''; var match = capture.match(/\(including the name (\'.+\')\)/); @@ -231,7 +229,7 @@ function parseError(group, idType) { } // arbitrary node list of form: #ID,#ID,#ID... - function parseWarning20(capture) { + function parse20(capture) { var newList = []; var items = capture.split(','); @@ -243,32 +241,6 @@ function parseError(group, idType) { 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; } @@ -338,7 +310,7 @@ export default { var coincident = false; do { // first time, move marker up. after that, move marker right. - var delta = coincident ? [0.00002, 0] : [0, 0.00002]; + var delta = coincident ? [0.00001, 0] : [0, 0.00001]; loc = geoVecAdd(loc, delta); var bbox = geoExtent(loc).bbox(); coincident = _krCache.rtree.search(bbox).length; diff --git a/modules/ui/keepRight_details.js b/modules/ui/keepRight_details.js index d73de470f..38047a9ad 100644 --- a/modules/ui/keepRight_details.js +++ b/modules/ui/keepRight_details.js @@ -1,7 +1,12 @@ -import { event as d3_event } from 'd3-selection'; +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 } from '../util'; export function uiKeepRightDetails(context) { @@ -46,28 +51,60 @@ export function uiKeepRightDetails(context) { // description - var description = detailsEnter + var descriptionEnter = detailsEnter .append('div') .attr('class', 'kr_error-details-description'); - description + descriptionEnter .append('h4') .text(function() { return t('QA.keepRight.detail_description'); }); - description + descriptionEnter .append('div') .attr('class', 'kr_error-details-description-text') .html(errorDetail); - description.selectAll('.kr_error_description-id') - .on('click', function() { clickLink(context, this.text); }); + // If there are entity links in the error message.. + descriptionEnter.selectAll('.kr_error_entity_link') + .each(function() { + var link = d3_select(this); + var entityID = this.innerText; + 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); + context.enter(modeSelect(context, [entityID])); + }); - function clickLink(context, entityID) { - d3_event.preventDefault(); - context.layers().layer('osm').enabled(true); - context.zoomToEntity(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) { + var preset = context.presets().match(entity, context.graph()); + name = preset && !preset.isFallback() && preset.name(); // fallback to preset name + } + + if (name) { + this.innerText = name; + } + } + }); } diff --git a/modules/util/util.js b/modules/util/util.js index 605378ece..cdc928da2 100644 --- a/modules/util/util.js +++ b/modules/util/util.js @@ -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; @@ -145,11 +147,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])); @@ -158,36 +161,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; } @@ -195,10 +203,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 + ')' : '')); } @@ -233,11 +240,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; } @@ -255,9 +263,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) { @@ -272,8 +280,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; }