From 58e31bc45a0585686e66a5b2e6669a249afd64a0 Mon Sep 17 00:00:00 2001 From: Max Grossman Date: Fri, 1 Feb 2019 17:34:40 -0500 Subject: [PATCH 01/16] initial update to draw modes and hover behavior ref #5811 --- modules/behavior/draw.js | 8 ++++++-- modules/behavior/draw_way.js | 6 +++++- modules/behavior/hover.js | 13 +++++++++++-- modules/modes/drag_node.js | 1 - modules/presets/index.js | 33 ++++++++++++++++++++++++++++++++- 5 files changed, 54 insertions(+), 7 deletions(-) diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 3081d2153..dd6b9e0a0 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -13,6 +13,7 @@ import { behaviorTail } from './tail'; import { geoChooseEdge, geoVecLength } from '../geo'; import { utilKeybinding, utilRebind } from '../util'; +import _isEmpty from 'lodash-es/isEmpty'; var _usedTails = {}; var _disableSpace = false; @@ -26,7 +27,7 @@ export function behaviorDraw(context) { var keybinding = utilKeybinding('draw'); - var hover = behaviorHover(context).altDisables(true) + var hover = behaviorHover(context).altDisables(true).ignoreVertex(true) .on('hover', context.ui().sidebar.hover); var tail = behaviorTail(); var edit = behaviorEdit(context); @@ -116,6 +117,9 @@ export function behaviorDraw(context) { _mouseLeave = true; } + function allowsVertex(d) { + return _isEmpty(d.tags) || context.presets().allowsVertex(d, context.graph()); + } // related code // - `mode/drag_node.js` `doMode()` @@ -125,7 +129,7 @@ export function behaviorDraw(context) { var d = datum(); var target = d && d.properties && d.properties.entity; - if (target && target.type === 'node') { // Snap to a node + if (target && target.type === 'node' && allowsVertex(target)) { // Snap to a node dispatch.call('clickNode', this, target, d); return; diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index d980bc26b..93846eb0b 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -17,6 +17,7 @@ import { modeBrowse, modeSelect } from '../modes'; import { osmNode } from '../osm'; import { utilKeybinding } from '../util'; +import _isEmpty from 'lodash-es/isEmpty'; export function behaviorDrawWay(context, wayId, index, mode, startGraph) { var origWay = context.entity(wayId); @@ -65,6 +66,9 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { } } + function allowsVertex(d) { + return _isEmpty(d.tags) || context.presets().allowsVertex(d, context.graph()); + } // related code // - `mode/drag_node.js` `doMode()` @@ -73,7 +77,7 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { function move(datum) { context.surface().classed('nope-disabled', d3_event.altKey); - var targetLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc; + var targetLoc = datum && datum.properties && datum.properties.entity && allowsVertex(datum.properties.entity) && datum.properties.entity.loc; var targetNodes = datum && datum.properties && datum.properties.nodes; var loc = context.map().mouseCoordinates(); diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index 58b9187a9..d13c5d77e 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -8,6 +8,7 @@ import { import { osmEntity, osmNote, krError } from '../osm'; import { utilKeybinding, utilRebind } from '../util'; +import _isEmpty from 'lodash-es/isEmpty'; /* The hover behavior adds the `.hover` class on mouseover to all elements to which @@ -24,6 +25,7 @@ export function behaviorHover(context) { var _newId = null; var _buttonDown; var _altDisables; + var _vertex; var _target; @@ -96,6 +98,9 @@ export function behaviorHover(context) { .on('mouseup.hover', null, true); } + function allowsVertex(d) { + return _isEmpty(d.tags) || context.presets().allowsVertex(d, context.graph()); + } function enter(datum) { if (datum === _target) return; @@ -126,7 +131,6 @@ export function behaviorHover(context) { if (entity.type === 'relation') { entity.members.forEach(function(member) { selector += ', .' + member.id; }); } - } else if (datum && datum.properties && (datum.properties.entity instanceof osmEntity)) { entity = datum.properties.entity; selector = '.' + entity.id; @@ -144,7 +148,7 @@ export function behaviorHover(context) { return; } - var suppressed = _altDisables && d3_event && d3_event.altKey; + var suppressed = (_altDisables && d3_event && d3_event.altKey) || (_vertex && !allowsVertex(entity, context.graph())); _selection.selectAll(selector) .classed(suppressed ? 'hover-suppressed' : 'hover', true); @@ -182,6 +186,11 @@ export function behaviorHover(context) { return behavior; }; + behavior.ignoreVertex = function(val) { + if (!arguments.length) return _vertex; + _vertex = val; + return behavior; + }; return utilRebind(behavior, dispatch, 'on'); } diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 9753f8e1e..cd121d920 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -355,7 +355,6 @@ export function modeDragNode(context) { } } - function end(entity) { if (_isCancelled) return; diff --git a/modules/presets/index.js b/modules/presets/index.js index ff346eacf..12a939470 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -53,7 +53,7 @@ export function presetIndex() { for (var k in entity.tags) { // If any part of an address is present, // allow fallback to "Address" preset - #4353 - if (k.match(/^addr:/) !== null && geometryMatches['addr:*']) { + if (/^addr:/.test(k) && geometryMatches['addr:*']) { address = geometryMatches['addr:*'][0]; } @@ -67,6 +67,7 @@ export function presetIndex() { match = keyMatches[i]; } } + } if (address && (!match || match.isFallback())) { @@ -76,6 +77,36 @@ export function presetIndex() { }); }; + all.allowsVertex = function(entity, resolver) { + return resolver.transient(entity, 'vertexMatch', function() { + var vertexPresets = _index.vertex; + var match; + + for (var k in entity.tags) { + var keyMatches = vertexPresets[k]; + if (!keyMatches) continue; + for (var i = 0; i < keyMatches.length; i++) { + var preset = keyMatches[i]; + if (preset.searchable !== false) { + if (preset.matchScore(entity) > -1) { + match = preset; + break; + } + } + } + + if (!match && /^addr:/.test(k) && vertexPresets['addr:*']) { + match = true; + } + + if (match) break; + + } + + return match; + }); + }; + // Because of the open nature of tagging, iD will never have a complete // list of tags used in OSM, so we want it to have logic like "assume From b7786fd0811306dbd3ec932cae22a3f827e1bca7 Mon Sep 17 00:00:00 2001 From: Max Grossman Date: Fri, 1 Feb 2019 17:58:15 -0500 Subject: [PATCH 02/16] if on address interpolation, match and don't look through keys... ref #5811 --- modules/presets/index.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/modules/presets/index.js b/modules/presets/index.js index 12a939470..3f832b1fa 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -82,25 +82,29 @@ export function presetIndex() { var vertexPresets = _index.vertex; var match; - for (var k in entity.tags) { - var keyMatches = vertexPresets[k]; - if (!keyMatches) continue; - for (var i = 0; i < keyMatches.length; i++) { - var preset = keyMatches[i]; - if (preset.searchable !== false) { - if (preset.matchScore(entity) > -1) { - match = preset; - break; + if (entity.isOnAddressLine(resolver)) { + match = true; + } else { + for (var k in entity.tags) { + var keyMatches = vertexPresets[k]; + if (!keyMatches) continue; + for (var i = 0; i < keyMatches.length; i++) { + var preset = keyMatches[i]; + if (preset.searchable !== false) { + if (preset.matchScore(entity) > -1) { + match = preset; + break; + } } } + + if (!match && /^addr:/.test(k) && vertexPresets['addr:*']) { + match = true; + } + + if (match) break; + } - - if (!match && /^addr:/.test(k) && vertexPresets['addr:*']) { - match = true; - } - - if (match) break; - } return match; From 32c42073c6dfd5ae426e55ce1247a0ac86f63c6a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Feb 2019 13:24:58 -0500 Subject: [PATCH 03/16] Add support for Relation type documentation lookups from wikibase (closes #5860) --- modules/services/osm_wikibase.js | 60 +++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/modules/services/osm_wikibase.js b/modules/services/osm_wikibase.js index f600b6f0f..f207ce5cb 100644 --- a/modules/services/osm_wikibase.js +++ b/modules/services/osm_wikibase.js @@ -66,6 +66,7 @@ export default { 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 @@ -78,8 +79,8 @@ export default { localePick = stmt; } }); - var result = localePick || preferredPick; + var result = localePick || preferredPick; if (result) { var datavalue = result.mainsnak.datavalue; return datavalue.type === 'wikibase-entityid' ? datavalue.value.id : datavalue.value; @@ -96,6 +97,7 @@ export default { */ monolingualClaimToValueObj: function(entity, property) { if (!entity || !entity.claims[property]) return undefined; + return entity.claims[property].reduce(function(acc, obj) { var value = obj.mainsnak.datavalue.value; acc[value.language] = value.text; @@ -105,7 +107,7 @@ export default { toSitelink: function(key, value) { - var result = value ? 'Tag:' + key + '=' + value : 'Key:' + key; + var result = value ? ('Tag:' + key + '=' + value) : 'Key:' + key; return result.replace(/_/g, ' ').trim(); }, @@ -113,35 +115,44 @@ export default { // // Pass params object of the form: // { - // key: 'string', // required - // value: 'string' // optional - // } - // -or- - // { - // rtype: 'rtype' // relation type (e.g. 'multipolygon') + // key: 'string', + // value: 'string', + // rtype: 'string', + // langCode: 'string' // } // getEntity: function(params, callback) { var doRequest = params.debounce ? debouncedRequest : request; - var self = this; + var that = this; var titles = []; var result = {}; - var keySitelink = this.toSitelink(params.key); - var tagSitelink = params.value ? this.toSitelink(params.key, params.value) : false; + var rtypeSitelink = params.rtype ? ('Relation:' + params.rtype).replace(/_/g, ' ').trim() : false; + var keySitelink = params.key ? this.toSitelink(params.key) : false; + var tagSitelink = (params.key && params.value) ? this.toSitelink(params.key, params.value) : false; var localeSitelink; if (params.langCode && _localeIDs[params.langCode] === undefined) { // If this is the first time we are asking about this locale, // fetch corresponding entity (if it exists), and cache it. // If there is no such entry, cache `false` value to avoid re-requesting it. - localeSitelink = ('Locale:' + params.langCode).replace(/_/g, ' ').trim(); + localeSitelink = params.langCode ? ('Locale:' + params.langCode).replace(/_/g, ' ').trim() : false; titles.push(localeSitelink); } - if (_wikibaseCache[keySitelink]) { - result.key = _wikibaseCache[keySitelink]; - } else { - titles.push(keySitelink); + if (rtypeSitelink) { + if (_wikibaseCache[rtypeSitelink]) { + result.rtype = _wikibaseCache[rtypeSitelink]; + } else { + titles.push(rtypeSitelink); + } + } + + if (keySitelink) { + if (_wikibaseCache[keySitelink]) { + result.key = _wikibaseCache[keySitelink]; + } else { + titles.push(keySitelink); + } } if (tagSitelink) { @@ -185,11 +196,15 @@ export default { 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) { + + var title = res.sitelinks.wiki.title; + if (title === rtypeSitelink) { + _wikibaseCache[rtypeSitelink] = res; + result.rtype = res; + } else if (title === keySitelink) { _wikibaseCache[keySitelink] = res; result.key = res; } else if (title === tagSitelink) { @@ -205,7 +220,7 @@ export default { if (localeSitelink) { // If locale ID is not found, store false to prevent repeated queries - self.addLocale(params.langCode, localeID); + that.addLocale(params.langCode, localeID); } callback(null, result); @@ -245,7 +260,7 @@ export default { return; } - var entity = data.tag || data.key; + var entity = data.rtype || data.tag || data.key; if (!entity) { callback('No entity'); return; @@ -282,6 +297,7 @@ export default { // Try to get a wiki page from tag data item first, followed by the corresponding key data item. // If neither tag nor key data item contain a wiki page in the needed language nor English, // get the first found wiki page from either the tag or the key item. + var rtypeWiki = that.monolingualClaimToValueObj(data.rtype, 'P31'); var tagWiki = that.monolingualClaimToValueObj(data.tag, 'P31'); var keyWiki = that.monolingualClaimToValueObj(data.key, 'P31'); @@ -289,7 +305,11 @@ export default { // BUG: in some cases, a more elaborate fallback logic might be needed var langPrefix = langCode.split('-', 2)[0]; + // use the first acceptable wiki page result.wiki = + getWikiInfo(rtypeWiki, langCode, 'inspector.wiki_reference') || + getWikiInfo(rtypeWiki, langPrefix, 'inspector.wiki_reference') || + getWikiInfo(rtypeWiki, 'en', 'inspector.wiki_en_reference') || getWikiInfo(tagWiki, langCode, 'inspector.wiki_reference') || getWikiInfo(tagWiki, langPrefix, 'inspector.wiki_reference') || getWikiInfo(tagWiki, 'en', 'inspector.wiki_en_reference') || From b050c2f4428aa72518638a888383b5dde75e00a8 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Feb 2019 13:34:29 -0500 Subject: [PATCH 04/16] Avoid keeping focus on the documentation lookup button --- modules/ui/tag_reference.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index 0123a9460..84ae00197 100644 --- a/modules/ui/tag_reference.js +++ b/modules/ui/tag_reference.js @@ -143,6 +143,7 @@ export function uiTagReference(tag) { .on('click', function () { d3_event.stopPropagation(); d3_event.preventDefault(); + this.blur(); // avoid keeping focus on the button - #4641 if (_showing) { hide(); } else if (_loaded) { From 2a7fec46fecd1d7c0c0247c66e107a60a5da1c60 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Feb 2019 13:36:39 -0500 Subject: [PATCH 05/16] Fetch somewhat higher resolution images (100x100 was really too small) --- modules/services/osm_wikibase.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/services/osm_wikibase.js b/modules/services/osm_wikibase.js index f207ce5cb..dd3397503 100644 --- a/modules/services/osm_wikibase.js +++ b/modules/services/osm_wikibase.js @@ -288,8 +288,7 @@ export default { if (imageroot && image) { result.imageURL = imageroot + '?' + utilQsString({ title: 'Special:Redirect/file/' + image, - width: 100, - height: 100 + width: 400 }); } } From fbe584c7ec3bffdb416f1c484eff561bc8dbc2da Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Feb 2019 16:27:33 -0500 Subject: [PATCH 06/16] Lookup docs for some presets on wikidata, not wikibase (re: #5823) Now `uiTagReference` can accept a `qid` param for presets where we want this (such as brands) --- modules/presets/preset.js | 7 ++ modules/services/wikidata.js | 65 ++++++++++++++++++- modules/ui/tag_reference.js | 122 +++++++++++++++++++---------------- 3 files changed, 135 insertions(+), 59 deletions(-) diff --git a/modules/presets/preset.js b/modules/presets/preset.js index 1460f4842..61e251b9d 100644 --- a/modules/presets/preset.js +++ b/modules/presets/preset.js @@ -166,6 +166,13 @@ export function presetPreset(id, preset, fields, visible, rawPresets) { var reference = preset.reference || {}; preset.reference = function(geometry) { + // Lookup documentation on Wikidata... + var qid = preset.tags.wikidata || preset.tags['brand:wikidata'] || preset.tags['operator:wikidata']; + if (qid) { + return { qid: qid }; + } + + // Lookup documentation on OSM Wikibase... var key = reference.key || Object.keys(_omit(preset.tags, 'name'))[0]; var value = reference.value || preset.tags[key]; diff --git a/modules/services/wikidata.js b/modules/services/wikidata.js index 95fbef3bd..b4790fb2e 100644 --- a/modules/services/wikidata.js +++ b/modules/services/wikidata.js @@ -66,8 +66,8 @@ export default { format: 'json', formatversion: 2, ids: qid, - props: /*sitelinks|*/'labels|descriptions', - //sitefilter: lang + 'wiki', + props: 'labels|descriptions|claims|sitelinks', + sitefilter: langs.map(function(d) { return d + 'wiki'; }).join('|'), languages: langs.join('|'), languagefallback: 1, origin: '*' @@ -79,6 +79,67 @@ export default { callback(qid, data.entities[qid] || {}); } }); + }, + + + // Pass `params` object of the form: + // { + // qid: 'string' // brand wikidata (e.g. 'Q37158') + // } + // + // Get an result object used to display tag documentation + // { + // title: 'string', + // description: 'string', + // editURL: 'string', + // imageURL: 'string', + // wiki: { title: 'string', text: 'string', url: 'string' } + // } + // + getDocs: function(params, callback) { + this.entityByQID(params.qid, function(err, entity) { + if (err || !entity) { + callback(err); + return; + } + + var description; + if (entity.descriptions && Object.keys(entity.descriptions).length > 0) { + description = entity.descriptions[Object.keys(entity.descriptions)[0]].value; + } + + // prepare result + var result = { + title: entity.id, + description: description, + editURL: 'https://www.wikidata.org/wiki/' + entity.id + }; + + // add image + if (entity.claims) { + var imageroot = 'https://commons.wikimedia.org/w/index.php'; + var props = ['P154','P18']; // logo image, image + var prop, image; + for (var i = 0; i < props.length; i++) { + prop = entity.claims[props[i]]; + if (prop && Object.keys(prop).length > 0) { + image = prop[Object.keys(prop)[0]].mainsnak.datavalue.value; + if (image) { + result.imageURL = imageroot + '?' + utilQsString({ + title: 'Special:Redirect/file/' + image, + width: 400 + }); + break; + } + } + } + } + + // TODO add wiki sitelink + // result.wiki = ? + + callback(null, result); + }); } }; diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index 84ae00197..801ba69aa 100644 --- a/modules/ui/tag_reference.js +++ b/modules/ui/tag_reference.js @@ -8,17 +8,22 @@ import { services } from '../services'; import { svgIcon } from '../svg'; -// Pass `tag` object of the form: +// Pass `which` object of the form: // { // key: 'string', // required // value: 'string' // optional // } // -or- // { -// rtype: 'rtype' // relation type (e.g. 'multipolygon') +// rtype: 'string' // relation type (e.g. 'multipolygon') // } -export function uiTagReference(tag) { - var wikibase = services.osmWikibase; +// -or- +// { +// qid: 'string' // brand wikidata (e.g. 'Q37158') +// } +// +export function uiTagReference(what) { + var wikibase = what.qid ? services.wikidata : services.osmWikibase; var tagReference = {}; var _button = d3_select(null); @@ -33,66 +38,69 @@ export function uiTagReference(tag) { _button .classed('tag-reference-loading', true); - wikibase.getDocs(tag, function(err, docs) { - _body.html(''); + wikibase.getDocs(what, gotDocs); + } - if (!docs || !docs.title) { - _body - .append('p') - .attr('class', 'tag-reference-description') - .text(t('inspector.no_documentation_key')); - done(); - return; - } - if (docs.imageURL) { - _body - .append('img') - .attr('class', 'tag-reference-wiki-image') - .attr('src', docs.imageURL) - .on('load', function() { done(); }) - .on('error', function() { d3_select(this).remove(); done(); }); - } else { - done(); - } + function gotDocs(err, docs) { + _body.html(''); + if (!docs || !docs.title) { _body .append('p') .attr('class', 'tag-reference-description') - .text(docs.description || t('inspector.no_documentation_key')) + .text(t('inspector.no_documentation_key')); + done(); + return; + } + + if (docs.imageURL) { + _body + .append('img') + .attr('class', 'tag-reference-wiki-image') + .attr('src', docs.imageURL) + .on('load', function() { done(); }) + .on('error', function() { d3_select(this).remove(); done(); }); + } else { + done(); + } + + _body + .append('p') + .attr('class', 'tag-reference-description') + .text(docs.description || t('inspector.no_documentation_key')) + .append('a') + .attr('class', 'tag-reference-edit') + .attr('target', '_blank') + .attr('tabindex', -1) + .attr('title', t('inspector.edit_reference')) + .attr('href', docs.editURL) + .call(svgIcon('#iD-icon-edit', 'inline')); + + if (docs.wiki) { + _body + .append('a') + .attr('class', 'tag-reference-link') + .attr('target', '_blank') + .attr('tabindex', -1) + .attr('href', docs.wiki.url) + .call(svgIcon('#iD-icon-out-link', 'inline')) + .append('span') + .text(t(docs.wiki.text)); + } + + // Add link to info about "good changeset comments" - #2923 + if (what.key === 'comment') { + _body .append('a') - .attr('class', 'tag-reference-edit') + .attr('class', 'tag-reference-comment-link') .attr('target', '_blank') .attr('tabindex', -1) - .attr('title', t('inspector.edit_reference')) - .attr('href', docs.editURL) - .call(svgIcon('#iD-icon-edit', 'inline')); - - if (docs.wiki) { - _body - .append('a') - .attr('class', 'tag-reference-link') - .attr('target', '_blank') - .attr('tabindex', -1) - .attr('href', docs.wiki.url) - .call(svgIcon('#iD-icon-out-link', 'inline')) - .append('span') - .text(t(docs.wiki.text)); - } - - // Add link to info about "good changeset comments" - #2923 - if (tag.key === 'comment') { - _body - .append('a') - .attr('class', 'tag-reference-comment-link') - .attr('target', '_blank') - .attr('tabindex', -1) - .call(svgIcon('#iD-icon-out-link', 'inline')) - .attr('href', t('commit.about_changeset_comments_link')) - .append('span') - .text(t('commit.about_changeset_comments')); - } - }); + .call(svgIcon('#iD-icon-out-link', 'inline')) + .attr('href', t('commit.about_changeset_comments_link')) + .append('span') + .text(t('commit.about_changeset_comments')); + } } @@ -156,9 +164,9 @@ export function uiTagReference(tag) { tagReference.body = function(selection) { - var tagid = tag.rtype || (tag.key + '-' + tag.value); + var itemID = what.qid || what.rtype || (what.key + '-' + what.value); _body = selection.selectAll('.tag-reference-body') - .data([tagid], function(d) { return d; }); + .data([itemID], function(d) { return d; }); _body.exit() .remove(); From a2b140e92ba2afed50af1c478ff7bac191a85432 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Feb 2019 16:38:40 -0500 Subject: [PATCH 07/16] Remove unnecessary ternary --- modules/services/osm_wikibase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/services/osm_wikibase.js b/modules/services/osm_wikibase.js index dd3397503..b7b9e8080 100644 --- a/modules/services/osm_wikibase.js +++ b/modules/services/osm_wikibase.js @@ -135,7 +135,7 @@ export default { // 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 = params.langCode ? ('Locale:' + params.langCode).replace(/_/g, ' ').trim() : false; + localeSitelink = ('Locale:' + params.langCode).replace(/_/g, ' ').trim(); titles.push(localeSitelink); } From 6ff381cb28e4b4681c6a79d2939201181382f9b7 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 8 Feb 2019 17:01:54 -0500 Subject: [PATCH 08/16] Drop "from this key" jargon --- data/core.yaml | 2 +- dist/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 0cc322325..702efd306 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -423,7 +423,7 @@ en: role: Role choose: Select feature type results: "{n} results for {search}" - no_documentation_key: There is no documentation available for this key + no_documentation_key: "There is no documentation available." edit_reference: "edit/translate" wiki_reference: View documentation wiki_en_reference: View documentation in English diff --git a/dist/locales/en.json b/dist/locales/en.json index 13463be75..1d2f99e37 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -518,7 +518,7 @@ "role": "Role", "choose": "Select feature type", "results": "{n} results for {search}", - "no_documentation_key": "There is no documentation available for this key", + "no_documentation_key": "There is no documentation available.", "edit_reference": "edit/translate", "wiki_reference": "View documentation", "wiki_en_reference": "View documentation in English", From 48d001dcb9024be670fc268bab1ec6338fed5fd0 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 9 Feb 2019 10:21:13 -0500 Subject: [PATCH 09/16] Don't capitalize first letter of error message in Persian (closes #5877, re: #5679) --- css/80_app.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/css/80_app.css b/css/80_app.css index 4715c07d4..f7028d98b 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2618,6 +2618,9 @@ input.key-trap { .error-details-description-text::first-letter { text-transform: capitalize; } +[dir='rtl'] .error-details-description-text::first-letter { + text-transform: none; /* #5877 */ +} .note-save .new-comment-input, .error-save .new-comment-input { From 24022416cb76e9026360fb2aeef6f74104cefd4b Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sat, 9 Feb 2019 16:28:27 +0000 Subject: [PATCH 10/16] Fix negative number of trips in ImproveOSM issues See https://github.com/openstreetmap/iD/pull/5739#issuecomment-460786055 --- data/core.yaml | 3 ++- dist/locales/en.json | 3 ++- modules/services/improveOSM.js | 7 ++++++- modules/ui/improveOSM_details.js | 5 ++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 702efd306..180b4e8cc 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -684,6 +684,7 @@ en: mr: title: Missing Geometry description: '{num_trips} recorded trips in this area suggest there may be unmapped {geometry_type} here.' + description_alt: 'Data from a 3rd party suggests there may be unmapped {geometry_type} here.' tr: title: Missing Turn Restriction description: '{num_passed} of {num_trips} recorded trips (travelling {travel_direction}) make a turn from {from_way} to {to_way} at {junction}. There may be a missing "{turn_restriction}" restriction.' @@ -1547,4 +1548,4 @@ en: wikidata: identifier: "Identifier" label: "Label" - description: "Description" + description: "Description" \ No newline at end of file diff --git a/dist/locales/en.json b/dist/locales/en.json index 1d2f99e37..bd24533d7 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -828,7 +828,8 @@ }, "mr": { "title": "Missing Geometry", - "description": "{num_trips} recorded trips in this area suggest there may be unmapped {geometry_type} here." + "description": "{num_trips} recorded trips in this area suggest there may be unmapped {geometry_type} here.", + "description_alt": "Data from a 3rd party suggests there may be unmapped {geometry_type} here." }, "tr": { "title": "Missing Turn Restriction", diff --git a/modules/services/improveOSM.js b/modules/services/improveOSM.js index 9d3b51ab7..1d6ad7f0d 100644 --- a/modules/services/improveOSM.js +++ b/modules/services/improveOSM.js @@ -267,6 +267,11 @@ export default { geometry_type: t('QA.improveOSM.geometry_types.' + geoType) }; + // -1 trips indicates data came from a 3rd party + if (feature.numberOfTrips === -1) { + d.desc = t('QA.improveOSM.error_types.mr.description_alt', d.replacements); + } + _erCache.data[d.id] = d; _erCache.rtree.insert(encodeErrorRtree(d)); }); @@ -476,4 +481,4 @@ export default { getClosedIDs: function() { return Object.keys(_erCache.closed).sort(); } -}; +}; \ No newline at end of file diff --git a/modules/ui/improveOSM_details.js b/modules/ui/improveOSM_details.js index efa27ef9b..9f016e4df 100644 --- a/modules/ui/improveOSM_details.js +++ b/modules/ui/improveOSM_details.js @@ -17,6 +17,9 @@ export function uiImproveOsmDetails(context) { var unknown = t('inspector.unknown'); if (!d) return unknown; + + if (d.desc) return d.desc; + var errorType = d.error_key; var et = dataEn.QA.improveOSM.error_types[errorType]; @@ -124,4 +127,4 @@ export function uiImproveOsmDetails(context) { return improveOsmDetails; -} +} \ No newline at end of file From 29ce6cb30c3fd00928efc11f490e1bb000716956 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 9 Feb 2019 14:51:37 -0500 Subject: [PATCH 11/16] Use proper errbacks (re: #5823) --- modules/services/wikidata.js | 32 +++++++++------ modules/ui/fields/wikidata.js | 73 +++++++++++++++++++--------------- modules/ui/fields/wikipedia.js | 4 +- 3 files changed, 64 insertions(+), 45 deletions(-) diff --git a/modules/services/wikidata.js b/modules/services/wikidata.js index b4790fb2e..88a214849 100644 --- a/modules/services/wikidata.js +++ b/modules/services/wikidata.js @@ -5,7 +5,7 @@ import { json as d3_json } from 'd3-request'; import { utilQsString } from '../util'; import { currentLocale } from '../util/locale'; -var endpoint = 'https://www.wikidata.org/w/api.php?'; +var apibase = 'https://www.wikidata.org/w/api.php?'; var _wikidataCache = {}; export default { @@ -21,13 +21,13 @@ export default { // corresponding Wikidata entities. itemsByTitle: function(lang, title, callback) { if (!title) { - callback('', {}); + callback('No title', {}); return; } lang = lang || 'en'; - d3_json(endpoint + utilQsString({ + d3_json(apibase + utilQsString({ action: 'wbgetentities', format: 'json', formatversion: 2, @@ -36,10 +36,13 @@ export default { languages: 'en', // shrink response by filtering to one language origin: '*' }), function(err, data) { - if (err || !data || data.error) { - callback('', {}); + if (data && data.error) { + err = data.error; + } + if (err) { + callback(err, {}); } else { - callback(title, data.entities || {}); + callback(null, data.entities || {}); } }); }, @@ -47,11 +50,11 @@ export default { entityByQID: function(qid, callback) { if (!qid) { - callback('', {}); + callback('No qid', {}); return; } if (_wikidataCache[qid]) { - callback('', _wikidataCache[qid]); + callback(null, _wikidataCache[qid]); return; } @@ -61,7 +64,7 @@ export default { 'en' ]); - d3_json(endpoint + utilQsString({ + d3_json(apibase + utilQsString({ action: 'wbgetentities', format: 'json', formatversion: 2, @@ -72,11 +75,14 @@ export default { languagefallback: 1, origin: '*' }), function(err, data) { - if (err || !data || data.error) { - callback('', {}); + if (data && data.error) { + err = data.error; + } + if (err) { + callback(err, {}); } else { _wikidataCache[qid] = data.entities[qid]; - callback(qid, data.entities[qid] || {}); + callback(null, data.entities[qid] || {}); } }); }, @@ -99,7 +105,7 @@ export default { getDocs: function(params, callback) { this.entityByQID(params.qid, function(err, entity) { if (err || !entity) { - callback(err); + callback(err || 'No entity'); return; } diff --git a/modules/ui/fields/wikidata.js b/modules/ui/fields/wikidata.js index a84e948fb..3c7fe5524 100644 --- a/modules/ui/fields/wikidata.js +++ b/modules/ui/fields/wikidata.js @@ -138,43 +138,54 @@ export function uiFieldWikidata(field) { wiki.tags = function(tags) { var value = tags[field.key] || ''; - var matches = value.match(/^Q[0-9]*$/); - utilGetSetValue(title, value); - // value in correct format - if (matches) { - _wikiURL = 'https://wikidata.org/wiki/' + value; - wikidata.entityByQID(value, function(qid, entity) { - var label = ''; - var description = ''; + if (!/^Q[0-9]*$/.test(value)) { // not a proper QID + unrecognized(); + return; + } - if (entity.labels && Object.keys(entity.labels).length > 0) { - label = entity.labels[Object.keys(entity.labels)[0]].value; - } - if (entity.descriptions && Object.keys(entity.descriptions).length > 0) { - description = entity.descriptions[Object.keys(entity.descriptions)[0]].value; - } + // QID value in correct format + _wikiURL = 'https://wikidata.org/wiki/' + value; + wikidata.entityByQID(value, function(err, entity) { + if (err) { + unrecognized(); + return; + } - d3_select('.preset-wikidata-label') - .style('display', function(){ - return label.length > 0 ? 'flex' : 'none'; - }) - .select('input') - .attr('value', label); + var label = ''; + var description = ''; - d3_select('.preset-wikidata-description') - .style('display', function(){ - return description.length > 0 ? 'flex' : 'none'; - }) - .select('input') - .attr('value', description); - }); + if (entity.labels && Object.keys(entity.labels).length > 0) { + label = entity.labels[Object.keys(entity.labels)[0]].value; + } + if (entity.descriptions && Object.keys(entity.descriptions).length > 0) { + description = entity.descriptions[Object.keys(entity.descriptions)[0]].value; + } + + d3_select('.preset-wikidata-label') + .style('display', function(){ + return label.length > 0 ? 'flex' : 'none'; + }) + .select('input') + .attr('value', label); + + d3_select('.preset-wikidata-description') + .style('display', function(){ + return description.length > 0 ? 'flex' : 'none'; + }) + .select('input') + .attr('value', description); + }); + + + // not a proper QID + function unrecognized() { + d3_select('.preset-wikidata-label') + .style('display', 'none'); + d3_select('.preset-wikidata-description') + .style('display', 'none'); - // unrecognized value format - } else { - d3_select('.preset-wikidata-label').style('display', 'none'); - d3_select('.preset-wikidata-description').style('display', 'none'); if (value && value !== '') { _wikiURL = 'https://wikidata.org/wiki/Special:Search?search=' + value; } else { diff --git a/modules/ui/fields/wikipedia.js b/modules/ui/fields/wikipedia.js index 1b1653107..81ffd7398 100644 --- a/modules/ui/fields/wikipedia.js +++ b/modules/ui/fields/wikipedia.js @@ -200,7 +200,9 @@ export function uiFieldWikipedia(field, context) { var initGraph = context.graph(); var initEntityID = _entity.id; - wikidata.itemsByTitle(language()[2], value, function(title, data) { + wikidata.itemsByTitle(language()[2], value, function(err, data) { + if (err) return; + // If graph has changed, we can't apply this update. if (context.graph() !== initGraph) return; From d055f34be8870941ce46a051db6e82424c984327 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 9 Feb 2019 15:18:32 -0500 Subject: [PATCH 12/16] Add sitelinks to result from wikidata getDocs --- modules/services/wikidata.js | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/modules/services/wikidata.js b/modules/services/wikidata.js index 88a214849..d4df4e6fb 100644 --- a/modules/services/wikidata.js +++ b/modules/services/wikidata.js @@ -109,6 +109,8 @@ export default { return; } + var i; + var description; if (entity.descriptions && Object.keys(entity.descriptions).length > 0) { description = entity.descriptions[Object.keys(entity.descriptions)[0]].value; @@ -126,7 +128,7 @@ export default { var imageroot = 'https://commons.wikimedia.org/w/index.php'; var props = ['P154','P18']; // logo image, image var prop, image; - for (var i = 0; i < props.length; i++) { + for (i = 0; i < props.length; i++) { prop = entity.claims[props[i]]; if (prop && Object.keys(prop).length > 0) { image = prop[Object.keys(prop)[0]].mainsnak.datavalue.value; @@ -141,8 +143,33 @@ export default { } } - // TODO add wiki sitelink - // result.wiki = ? + if (entity.sitelinks) { + // must be one of these that we requested.. + var langs = _uniq([ + currentLocale.toLowerCase(), + currentLocale.split('-', 2)[0].toLowerCase(), + 'en' + ]); + var englishLocale = (currentLocale.split('-', 2)[0].toLowerCase() === 'en'); + + for (i = 0; i < langs.length; i++) { // check each, in order of preference + var w = langs[i] + 'wiki'; + if (entity.sitelinks[w]) { + var title = entity.sitelinks[w].title; + var tKey = 'inspector.wiki_reference'; + if (!englishLocale && langs[i] === 'en') { // user's currentLocale isn't English but + tKey = 'inspector.wiki_en_reference'; // we are sending them to enwiki anyway.. + } + + result.wiki = { + title: title, + text: tKey, + url: 'https://' + langs[i] + '.wikipedia.org/wiki/' + title.replace(/ /g, '_') + }; + break; + } + } + } callback(null, result); }); From 65fc1437dc497bc7a7f99ff9363dbcb47dc6608a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 9 Feb 2019 15:30:57 -0500 Subject: [PATCH 13/16] Fix wiki image float and margins on rtl layout --- css/80_app.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index f7028d98b..bcd8ee8b2 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1112,11 +1112,12 @@ a.hide-toggle { img.tag-reference-wiki-image { float: right; width: 33.3333%; - width: -webkit-calc(33.3333% - 10px); - width: calc(33.3333% - 10px); border-radius: 4px; - max-height: 200px; - margin: 10px 5px 15px 20px; + margin: 10px 5px 15px 10px; +} +[dir='rtl'] img.tag-reference-wiki-image { + float: left; + margin: 10px 10px 15px 5px; } From d64a4c5654fea3c8925e5d3e2f0eb1ba90434573 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 9 Feb 2019 15:48:55 -0500 Subject: [PATCH 14/16] Don't pan map when resizing sidebar on right-to-left layout (closes #5881) --- modules/ui/sidebar.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index a25df48d2..dd1975cb6 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -68,6 +68,7 @@ export function uiSidebar(context) { }) .on('drag', function() { var isRTL = (textDirection === 'rtl'); + var scaleX = isRTL ? 0 : 1; var xMarginProperty = isRTL ? 'margin-right' : 'margin-left'; var x = d3_event.x - dragOffset; @@ -84,7 +85,7 @@ export function uiSidebar(context) { .style(xMarginProperty, '-400px') .style('width', '400px'); - context.ui().onResize([sidebarWidth - d3_event.dx, 0]); + context.ui().onResize([(sidebarWidth - d3_event.dx) * scaleX, 0]); } } else { @@ -94,9 +95,9 @@ export function uiSidebar(context) { .style('width', widthPct + '%'); if (isCollapsed) { - context.ui().onResize([-sidebarWidth, 0]); + context.ui().onResize([-sidebarWidth * scaleX, 0]); } else { - context.ui().onResize([-d3_event.dx, 0]); + context.ui().onResize([-d3_event.dx * scaleX, 0]); } } }) @@ -295,7 +296,9 @@ export function uiSidebar(context) { var isCollapsed = selection.classed('collapsed'); var isCollapsing = !isCollapsed; - var xMarginProperty = textDirection === 'rtl' ? 'margin-right' : 'margin-left'; + var isRTL = (textDirection === 'rtl'); + var scaleX = isRTL ? 0 : 1; + var xMarginProperty = isRTL ? 'margin-right' : 'margin-left'; sidebarWidth = selection.node().getBoundingClientRect().width; @@ -318,7 +321,7 @@ export function uiSidebar(context) { return function(t) { var dx = lastMargin - Math.round(i(t)); lastMargin = lastMargin - dx; - context.ui().onResize(moveMap ? undefined : [dx, 0]); + context.ui().onResize(moveMap ? undefined : [dx * scaleX, 0]); }; }) .on('end', function() { @@ -351,4 +354,4 @@ export function uiSidebar(context) { sidebar.toggle = function() {}; return sidebar; -} \ No newline at end of file +} From 26368522c9c6081e8b7034ed57ba3248130141d1 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 10 Feb 2019 22:08:13 -0500 Subject: [PATCH 15/16] Batch changes from raw tag editor so blur/remove tag don't conflict (closes #5878) --- modules/ui/fields/combo.js | 2 +- modules/ui/raw_tag_editor.js | 39 ++++++++++++++++++++++------------ test/spec/ui/raw_tag_editor.js | 2 +- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/modules/ui/fields/combo.js b/modules/ui/fields/combo.js index d6bc6d043..0028fb16f 100644 --- a/modules/ui/fields/combo.js +++ b/modules/ui/fields/combo.js @@ -297,7 +297,7 @@ export function uiFieldCombo(field, context) { .data([0]); var listClass = 'chiplist'; - + // Use a separate line for each value in the Destinations field // to mimic highway exit signs if (field.id === 'destination_oneway') { diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index e4895bb7e..6ca863fe5 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -20,6 +20,7 @@ export function uiRawTagEditor(context) { var _showBlank = false; var _updatePreference = true; var _expanded = false; + var _pendingChange = null; var _state; var _preset; var _tags; @@ -211,7 +212,7 @@ export function uiRawTagEditor(context) { .property('disabled', isReadOnly); items.selectAll('button.remove') - .on('click', removeTag); + .on('mousedown', removeTag); // 'click' fires too late - #5878 @@ -335,11 +336,11 @@ export function uiRawTagEditor(context) { } } - var t = {}; + _pendingChange = _pendingChange || {}; if (kOld) { - t[kOld] = undefined; + _pendingChange[kOld] = undefined; } - t[kNew] = vNew; + _pendingChange[kNew] = vNew; d.key = kNew; // update datum to avoid exit/enter on tag update d.value = vNew; @@ -347,15 +348,16 @@ export function uiRawTagEditor(context) { this.value = kNew; utilGetSetValue(inputVal, vNew); - dispatch.call('change', this, t); + scheduleChange(); } function valueChange(d) { if (isReadOnly(d)) return; - var t = {}; - t[d.key] = this.value; - dispatch.call('change', this, t); + + _pendingChange = _pendingChange || {}; + _pendingChange[d.key] = this.value; + scheduleChange(); } @@ -366,23 +368,32 @@ export function uiRawTagEditor(context) { _showBlank = false; content(wrap); } else { - var t = {}; - t[d.key] = undefined; - dispatch.call('change', this, t); + _pendingChange = _pendingChange || {}; + _pendingChange[d.key] = undefined; + scheduleChange(); } } function addTag() { - // Wrapped in a setTimeout in case it's being called from a blur - // handler. Without the setTimeout, the call to `content` would - // wipe out the pending value change. + // Delay render in case this click is blurring an edited combo. + // Without the setTimeout, the `content` render would wipe out the pending tag change. window.setTimeout(function() { _showBlank = true; content(wrap); list.selectAll('li:last-child input.key').node().focus(); + }, 20); + } + + + function scheduleChange() { + // Delay change in case this change is blurring an edited combo. - #5878 + window.setTimeout(function() { + dispatch.call('change', this, _pendingChange); + _pendingChange = null; }, 10); } + } diff --git a/test/spec/ui/raw_tag_editor.js b/test/spec/ui/raw_tag_editor.js index c48116719..ad699d506 100644 --- a/test/spec/ui/raw_tag_editor.js +++ b/test/spec/ui/raw_tag_editor.js @@ -53,7 +53,7 @@ describe('iD.uiRawTagEditor', function() { expect(tags).to.eql({highway: undefined}); done(); }); - iD.utilTriggerEvent(element.selectAll('button.remove'), 'click'); + iD.utilTriggerEvent(element.selectAll('button.remove'), 'mousedown'); }); it('adds tags when pressing the TAB key on last input.value', function (done) { From ba44c800769a61daa6779ca95dd3e33941e2fd3c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 10 Feb 2019 23:29:23 -0500 Subject: [PATCH 16/16] Force visible any entities related to a selected error (closes #5880) --- modules/modes/select_error.js | 3 +- modules/renderer/features.js | 43 +++++++----- modules/ui/improveOSM_details.js | 8 ++- modules/ui/keepRight_details.js | 6 ++ test/spec/renderer/features.js | 117 +++++++++++++++++-------------- 5 files changed, 106 insertions(+), 71 deletions(-) diff --git a/modules/modes/select_error.js b/modules/modes/select_error.js index de12b4dd1..3350fcf2a 100644 --- a/modules/modes/select_error.js +++ b/modules/modes/select_error.js @@ -148,8 +148,9 @@ export function modeSelectError(context, selectedErrorID, selectedErrorService) .hide(); context.selectedErrorID(null); + context.features().forceVisible([]); }; return mode; -} \ No newline at end of file +} diff --git a/modules/renderer/features.js b/modules/renderer/features.js index e8cb1008d..4894794a0 100644 --- a/modules/renderer/features.js +++ b/modules/renderer/features.js @@ -8,10 +8,7 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { osmEntity } from '../osm'; import { utilRebind } from '../util/rebind'; -import { - utilQsString, - utilStringQs -} from '../util'; +import { utilQsString, utilStringQs } from '../util'; export function rendererFeatures(context) { @@ -58,13 +55,14 @@ export function rendererFeatures(context) { 'obliterated': true }; - var dispatch = d3_dispatch('change', 'redraw'), - _cullFactor = 1, - _cache = {}, - _features = {}, - _stats = {}, - _keys = [], - _hidden = []; + var dispatch = d3_dispatch('change', 'redraw'); + var _cullFactor = 1; + var _cache = {}; + var _features = {}; + var _stats = {}; + var _keys = []; + var _hidden = []; + var _forceVisible = {}; function update() { @@ -277,10 +275,10 @@ export function rendererFeatures(context) { features.gatherStats = function(d, resolver, dimensions) { - var needsRedraw = false, - type = _groupBy(d, function(ent) { return ent.type; }), - entities = [].concat(type.relation || [], type.way || [], type.node || []), - currHidden, geometry, matches, i, j; + var needsRedraw = false; + var type = _groupBy(d, function(ent) { return ent.type; }); + var entities = [].concat(type.relation || [], type.way || [], type.node || []); + var currHidden, geometry, matches, i, j; for (i = 0; i < _keys.length; i++) { _features[_keys[i]].count = 0; @@ -346,8 +344,8 @@ export function rendererFeatures(context) { } if (!_cache[ent].matches) { - var matches = {}, - hasMatch = false; + var matches = {}; + var hasMatch = false; for (var i = 0; i < _keys.length; i++) { if (_keys[i] === 'others') { @@ -462,6 +460,7 @@ export function rendererFeatures(context) { features.isHidden = function(entity, resolver, geometry) { if (!_hidden.length) return false; if (!entity.version) return false; + if (_forceVisible[entity.id]) return false; var fn = (geometry === 'vertex' ? features.isHiddenChild : features.isHiddenFeature); return fn(entity, resolver, geometry); @@ -482,6 +481,16 @@ export function rendererFeatures(context) { }; + features.forceVisible = function(entityIDs) { + if (!arguments.length) return Object.keys(_forceVisible); + _forceVisible = {}; + for (var i = 0; i < entityIDs.length; i++) { + _forceVisible[entityIDs[i]] = true; + } + return features; + }; + + features.init = function() { var storage = context.storage('disabled-features'); if (storage) { diff --git a/modules/ui/improveOSM_details.js b/modules/ui/improveOSM_details.js index 9f016e4df..e1bc288ab 100644 --- a/modules/ui/improveOSM_details.js +++ b/modules/ui/improveOSM_details.js @@ -64,6 +64,7 @@ export function uiImproveOsmDetails(context) { .html(errorDetail); // If there are entity links in the error message.. + var relatedEntities = []; descriptionEnter.selectAll('.error_entity_link, .error_object_link') .each(function() { var link = d3_select(this); @@ -73,6 +74,8 @@ export function uiImproveOsmDetails(context) { : this.textContent; var entity = context.hasEntity(entityID); + relatedEntities.push(entityID); + // Add click handler link .on('mouseover', function() { @@ -116,6 +119,9 @@ export function uiImproveOsmDetails(context) { } } }); + + // Don't hide entities related to this error - #5880 + context.features().forceVisible(relatedEntities); } @@ -127,4 +133,4 @@ export function uiImproveOsmDetails(context) { return improveOsmDetails; -} \ No newline at end of file +} diff --git a/modules/ui/keepRight_details.js b/modules/ui/keepRight_details.js index 96b6536b6..e28d620a3 100644 --- a/modules/ui/keepRight_details.js +++ b/modules/ui/keepRight_details.js @@ -66,6 +66,7 @@ export function uiKeepRightDetails(context) { .html(errorDetail); // If there are entity links in the error message.. + var relatedEntities = []; descriptionEnter.selectAll('.error_entity_link, .error_object_link') .each(function() { var link = d3_select(this); @@ -75,6 +76,8 @@ export function uiKeepRightDetails(context) { : this.textContent; var entity = context.hasEntity(entityID); + relatedEntities.push(entityID); + // Add click handler link .on('mouseover', function() { @@ -118,6 +121,9 @@ export function uiKeepRightDetails(context) { } } }); + + // Don't hide entities related to this error - #5880 + context.features().forceVisible(relatedEntities); } diff --git a/test/spec/renderer/features.js b/test/spec/renderer/features.js index f8488d32a..234a60e71 100644 --- a/test/spec/renderer/features.js +++ b/test/spec/renderer/features.js @@ -1,6 +1,6 @@ describe('iD.Features', function() { - var dimensions = [1000, 1000], - context, features; + var dimensions = [1000, 1000]; + var context, features; function _values(obj) { var result = []; @@ -64,18 +64,18 @@ describe('iD.Features', function() { describe('#gatherStats', function() { it('counts features', function() { var graph = iD.coreGraph([ - iD.osmNode({id: 'point_bar', tags: {amenity: 'bar'}, version: 1}), - iD.osmNode({id: 'point_dock', tags: {waterway: 'dock'}, version: 1}), - iD.osmNode({id: 'point_rail_station', tags: {railway: 'station'}, version: 1}), - iD.osmNode({id: 'point_generator', tags: {power: 'generator'}, version: 1}), - iD.osmNode({id: 'point_old_rail_station', tags: {railway: 'station', disused: 'yes'}, version: 1}), - iD.osmWay({id: 'motorway', tags: {highway: 'motorway'}, version: 1}), - iD.osmWay({id: 'building_yes', tags: {area: 'yes', amenity: 'school', building: 'yes'}, version: 1}), - iD.osmWay({id: 'boundary', tags: {boundary: 'administrative'}, version: 1}), - iD.osmWay({id: 'fence', tags: {barrier: 'fence'}, version: 1}) - ]), - all = _values(graph.base().entities), - stats; + iD.osmNode({id: 'point_bar', tags: {amenity: 'bar'}, version: 1}), + iD.osmNode({id: 'point_dock', tags: {waterway: 'dock'}, version: 1}), + iD.osmNode({id: 'point_rail_station', tags: {railway: 'station'}, version: 1}), + iD.osmNode({id: 'point_generator', tags: {power: 'generator'}, version: 1}), + iD.osmNode({id: 'point_old_rail_station', tags: {railway: 'station', disused: 'yes'}, version: 1}), + iD.osmWay({id: 'motorway', tags: {highway: 'motorway'}, version: 1}), + iD.osmWay({id: 'building_yes', tags: {area: 'yes', amenity: 'school', building: 'yes'}, version: 1}), + iD.osmWay({id: 'boundary', tags: {boundary: 'administrative'}, version: 1}), + iD.osmWay({id: 'fence', tags: {barrier: 'fence'}, version: 1}) + ]); + var all = _values(graph.base().entities); + var stats; features.gatherStats(all, graph, dimensions); stats = features.stats(); @@ -205,8 +205,8 @@ describe('iD.Features', function() { version: 1 }) - ]), - all = _values(graph.base().entities); + ]); + var all = _values(graph.base().entities); function doMatch(ids) { @@ -435,12 +435,12 @@ describe('iD.Features', function() { describe('hiding', function() { it('hides child vertices on a hidden way', function() { - var a = iD.osmNode({id: 'a', version: 1}), - b = iD.osmNode({id: 'b', version: 1}), - w = iD.osmWay({id: 'w', nodes: [a.id, b.id], tags: {highway: 'path'}, version: 1}), - graph = iD.coreGraph([a, b, w]), - geometry = a.geometry(graph), - all = _values(graph.base().entities); + var a = iD.osmNode({id: 'a', version: 1}); + var b = iD.osmNode({id: 'b', version: 1}); + var w = iD.osmWay({id: 'w', nodes: [a.id, b.id], tags: {highway: 'path'}, version: 1}); + var graph = iD.coreGraph([a, b, w]); + var geometry = a.geometry(graph); + var all = _values(graph.base().entities); features.disable('paths'); features.gatherStats(all, graph, dimensions); @@ -452,23 +452,23 @@ describe('iD.Features', function() { }); it('hides uninteresting (e.g. untagged or "other") member ways on a hidden multipolygon relation', function() { - var outer = iD.osmWay({id: 'outer', tags: {area: 'yes', natural: 'wood'}, version: 1}), - inner1 = iD.osmWay({id: 'inner1', tags: {barrier: 'fence'}, version: 1}), - inner2 = iD.osmWay({id: 'inner2', version: 1}), - inner3 = iD.osmWay({id: 'inner3', tags: {highway: 'residential'}, version: 1}), - r = iD.osmRelation({ - id: 'r', - tags: {type: 'multipolygon'}, - members: [ - {id: outer.id, role: 'outer', type: 'way'}, - {id: inner1.id, role: 'inner', type: 'way'}, - {id: inner2.id, role: 'inner', type: 'way'}, - {id: inner3.id, role: 'inner', type: 'way'} - ], - version: 1 - }), - graph = iD.coreGraph([outer, inner1, inner2, inner3, r]), - all = _values(graph.base().entities); + var outer = iD.osmWay({id: 'outer', tags: {area: 'yes', natural: 'wood'}, version: 1}); + var inner1 = iD.osmWay({id: 'inner1', tags: {barrier: 'fence'}, version: 1}); + var inner2 = iD.osmWay({id: 'inner2', version: 1}); + var inner3 = iD.osmWay({id: 'inner3', tags: {highway: 'residential'}, version: 1}); + var r = iD.osmRelation({ + id: 'r', + tags: {type: 'multipolygon'}, + members: [ + {id: outer.id, role: 'outer', type: 'way'}, + {id: inner1.id, role: 'inner', type: 'way'}, + {id: inner2.id, role: 'inner', type: 'way'}, + {id: inner3.id, role: 'inner', type: 'way'} + ], + version: 1 + }); + var graph = iD.coreGraph([outer, inner1, inner2, inner3, r]); + var all = _values(graph.base().entities); features.disable('landuse'); features.gatherStats(all, graph, dimensions); @@ -480,12 +480,12 @@ describe('iD.Features', function() { }); it('hides only versioned entities', function() { - var a = iD.osmNode({id: 'a', version: 1}), - b = iD.osmNode({id: 'b'}), - graph = iD.coreGraph([a, b]), - ageo = a.geometry(graph), - bgeo = b.geometry(graph), - all = _values(graph.base().entities); + var a = iD.osmNode({id: 'a', version: 1}); + var b = iD.osmNode({id: 'b'}); + var graph = iD.coreGraph([a, b]); + var ageo = a.geometry(graph); + var bgeo = b.geometry(graph); + var all = _values(graph.base().entities); features.disable('points'); features.gatherStats(all, graph, dimensions); @@ -494,10 +494,23 @@ describe('iD.Features', function() { expect(features.isHidden(b, graph, bgeo)).to.be.false; }); + it('#forceVisible', function() { + var a = iD.osmNode({id: 'a', version: 1}); + var graph = iD.coreGraph([a]); + var ageo = a.geometry(graph); + var all = _values(graph.base().entities); + + features.disable('points'); + features.gatherStats(all, graph, dimensions); + features.forceVisible(['a']); + + expect(features.isHidden(a, graph, ageo)).to.be.false; + }); + it('auto-hides features', function() { - var graph = iD.coreGraph([]), - maxPoints = 200, - all, hidden, autoHidden, i, msg; + var graph = iD.coreGraph([]); + var maxPoints = 200; + var all, hidden, autoHidden, i, msg; for (i = 0; i < maxPoints; i++) { graph.rebase([iD.osmNode({version: 1})], [graph]); @@ -525,10 +538,10 @@ describe('iD.Features', function() { }); it('doubles auto-hide threshold when doubling viewport size', function() { - var graph = iD.coreGraph([]), - maxPoints = 400, - dimensions = [2000, 1000], - all, hidden, autoHidden, i, msg; + var graph = iD.coreGraph([]); + var maxPoints = 400; + var dimensions = [2000, 1000]; + var all, hidden, autoHidden, i, msg; for (i = 0; i < maxPoints; i++) { graph.rebase([iD.osmNode({version: 1})], [graph]);