diff --git a/css/80_app.css b/css/80_app.css index d73dfa5b4..fb13d9f60 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1114,11 +1114,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; } @@ -2630,6 +2631,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 { diff --git a/data/core.yaml b/data/core.yaml index 4b6b67dc3..251057a27 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -425,7 +425,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 @@ -668,6 +668,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.' @@ -1641,4 +1642,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 9681ac07d..403a833a3 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -520,7 +520,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", @@ -811,7 +811,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/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 5b618b47c..2b5463531 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 40cd68be3..4678b2aa5 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -8,6 +8,7 @@ import { import { osmEntity, osmNote, qaError } 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'); } \ No newline at end of file diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 8b8ae2212..533b46aac 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -354,7 +354,6 @@ export function modeDragNode(context) { } } - function end(entity) { if (_isCancelled) return; 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/presets/index.js b/modules/presets/index.js index ff346eacf..3f832b1fa 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,40 @@ export function presetIndex() { }); }; + all.allowsVertex = function(entity, resolver) { + return resolver.transient(entity, 'vertexMatch', function() { + var vertexPresets = _index.vertex; + var match; + + 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; + + } + } + + 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 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/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/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/services/osm_wikibase.js b/modules/services/osm_wikibase.js index f600b6f0f..b7b9e8080 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,21 +115,20 @@ 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) { @@ -138,10 +139,20 @@ export default { 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; @@ -273,8 +288,7 @@ export default { if (imageroot && image) { result.imageURL = imageroot + '?' + utilQsString({ title: 'Special:Redirect/file/' + image, - width: 100, - height: 100 + width: 400 }); } } @@ -282,6 +296,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 +304,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') || diff --git a/modules/services/wikidata.js b/modules/services/wikidata.js index 95fbef3bd..d4df4e6fb 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,24 +64,115 @@ export default { 'en' ]); - d3_json(endpoint + utilQsString({ + d3_json(apibase + utilQsString({ action: 'wbgetentities', 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: '*' }), 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] || {}); } }); + }, + + + // 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 || 'No entity'); + return; + } + + var i; + + 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 (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; + } + } + } + } + + 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); + }); } }; 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/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; diff --git a/modules/ui/improveOSM_details.js b/modules/ui/improveOSM_details.js index efa27ef9b..e1bc288ab 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]; @@ -61,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); @@ -70,6 +74,8 @@ export function uiImproveOsmDetails(context) { : this.textContent; var entity = context.hasEntity(entityID); + relatedEntities.push(entityID); + // Add click handler link .on('mouseover', function() { @@ -113,6 +119,9 @@ export function uiImproveOsmDetails(context) { } } }); + + // Don't hide entities related to this error - #5880 + context.features().forceVisible(relatedEntities); } 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/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/modules/ui/sidebar.js b/modules/ui/sidebar.js index 107d821bd..25d8abd62 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]); } } }) @@ -298,7 +299,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; @@ -321,7 +324,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() { @@ -354,4 +357,4 @@ export function uiSidebar(context) { sidebar.toggle = function() {}; return sidebar; -} \ No newline at end of file +} diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index 0123a9460..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')); + } } @@ -143,6 +151,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) { @@ -155,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(); 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]); 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) {