From 09e7b236658e08fde82c0b1a345cb4426ca57edb Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sat, 7 Dec 2019 19:07:53 +0000 Subject: [PATCH] Add Osmose issues UI and filtering Filters out errors not present in the data .json file to enable selective support since Osmose has a wide variety of errors which may be too advanced for iD. Also added processing for the elements associated with an error for forced visibility and highlighting. --- css/65_data.css | 4 + data/core.yaml | 6 ++ data/qa_errors.json | 8 ++ dist/locales/en.json | 9 ++ modules/services/osmose.js | 46 ++++++---- modules/ui/osmose_details.js | 135 ++++++++++++++++++++++++++++ modules/ui/osmose_editor.js | 170 +++++++++++++++++++++++++++++++++++ modules/ui/osmose_header.js | 97 ++++++++++++++++++++ modules/ui/sidebar.js | 15 +++- 9 files changed, 468 insertions(+), 22 deletions(-) create mode 100644 modules/ui/osmose_details.js create mode 100644 modules/ui/osmose_editor.js create mode 100644 modules/ui/osmose_header.js diff --git a/css/65_data.css b/css/65_data.css index 837cc9688..39c1c1603 100644 --- a/css/65_data.css +++ b/css/65_data.css @@ -159,6 +159,10 @@ color: #FFFFFF; } +.osmose.category-structure { + color: #F9C700; +} + /* Custom Map Data (geojson, gpx, kml, vector tile) */ .layer-mapdata { pointer-events: none; diff --git a/data/core.yaml b/data/core.yaml index e030339e9..b5b4c0af9 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -827,6 +827,12 @@ en: cannot_zoom: "Cannot zoom out further in current mode." full_screen: Toggle Full Screen QA: + osmose: + title: Osmose + error_types: + 1070-1: + title: 'Highway intersecting building' + description: '{1} intersects with {0}.' improveOSM: title: ImproveOSM Detection geometry_types: diff --git a/data/qa_errors.json b/data/qa_errors.json index d14306dcd..aad4b3328 100644 --- a/data/qa_errors.json +++ b/data/qa_errors.json @@ -32,6 +32,14 @@ "errorTypes": { } + }, + "osmose": { + "errorTypes": { + "1070-1": { + "icon": "maki-home", + "category": "structure" + } + } } } } \ No newline at end of file diff --git a/dist/locales/en.json b/dist/locales/en.json index 31a21fdba..a38ff70e6 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -1031,6 +1031,15 @@ "cannot_zoom": "Cannot zoom out further in current mode.", "full_screen": "Toggle Full Screen", "QA": { + "osmose": { + "title": "Osmose", + "error_types": { + "1070-1": { + "title": "Highway intersecting building", + "description": "{1} intersects with {0}." + } + } + }, "improveOSM": { "title": "ImproveOSM Detection", "geometry_types": { diff --git a/modules/services/osmose.js b/modules/services/osmose.js index 36181b579..91aa5c2bd 100644 --- a/modules/services/osmose.js +++ b/modules/services/osmose.js @@ -7,7 +7,7 @@ import { geoExtent, geoVecAdd, geoVecScale } from '../geo'; import { qaError } from '../osm'; import { t } from '../util/locale'; import { utilRebind, utilTiler, utilQsString } from '../util'; - +import { services } from '../../data/qa_errors.json'; var tiler = utilTiler(); var dispatch = d3_dispatch('loaded'); @@ -130,29 +130,37 @@ export default { if (data.issues) { data.issues.forEach(function(issue) { // Elements provided as string, separated by _ character - var elems = issue.elems.split('_'); + var elems = issue.elems.split('_').map(function(i) { + return i.substring(0,1) + i.replace(/node|way|relation/, '') + }); var loc = [issue.lon, issue.lat]; + // Item is the type of error, w/ class tells us the sub-type + var type = [issue.item, issue.classs].join('-'); - loc = preventCoincident(loc, true); + // Filter out unsupported error types (some are too specific or advanced) + if (services.osmose.errorTypes[type]) { + loc = preventCoincident(loc, true); - var d = new qaError({ - // Info required for every error - loc: loc, - service: 'osmose', - error_type: [issue.item, issue.classs].join('-'), - // Extra details needed for this service - identifier: issue.id, // this is used to post changes to the error - elems: elems - //object_id: elems[0], - //object_type: elems[0].substring(0,1) - }); + var d = new qaError({ + // Info required for every error + loc: loc, + service: 'osmose', + error_type: type, + // Extra details needed for this service + identifier: issue.id, // this is used to post changes to the error + elems: elems, + object_id: elems.length ? elems[0].substring(1) : '', + object_type: elems.length ? elems[0].substring(0,1) : '' + }); - // Variables used in the description - d.replacements = { - }; + // Variables used in the description + d.replacements = elems.map(function(i) { + return linkEntity(i) + }); - _erCache.data[d.id] = d; - _erCache.rtree.insert(encodeErrorRtree(d)); + _erCache.data[d.id] = d; + _erCache.rtree.insert(encodeErrorRtree(d)); + } }); } }) diff --git a/modules/ui/osmose_details.js b/modules/ui/osmose_details.js new file mode 100644 index 000000000..9c8dbb874 --- /dev/null +++ b/modules/ui/osmose_details.js @@ -0,0 +1,135 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { dataEn } from '../../data'; +import { modeSelect } from '../modes/select'; +import { t } from '../util/locale'; +import { utilDisplayName, utilEntityOrMemberSelector, utilEntityRoot } from '../util'; + + +export function uiOsmoseDetails(context) { + var _error; + + + function errorDetail(d) { + var unknown = t('inspector.unknown'); + + if (!d) return unknown; + + if (d.desc) return d.desc; + + var errorType = d.error_type; + var et = dataEn.QA.osmose.error_types[errorType]; + + var detail; + if (et && et.description) { + detail = t('QA.osmose.error_types.' + errorType + '.description', d.replacements); + } else { + detail = unknown; + } + + return detail; + } + + + function osmoseDetails(selection) { + var details = selection.selectAll('.error-details') + .data( + (_error ? [_error] : []), + function(d) { return d.id + '-' + (d.status || 0); } + ); + + details.exit() + .remove(); + + var detailsEnter = details.enter() + .append('div') + .attr('class', 'error-details error-details-container'); + + + // description + var descriptionEnter = detailsEnter + .append('div') + .attr('class', 'error-details-description'); + + descriptionEnter + .append('h4') + .text(function() { return t('QA.keepRight.detail_description'); }); + + descriptionEnter + .append('div') + .attr('class', 'error-details-description-text') + .html(errorDetail); + + // If there are entity links in the error message.. + var relatedEntities = _error.elems; + descriptionEnter.selectAll('.error_entity_link, .error_object_link') + .each(function() { + var link = d3_select(this); + var isObjectLink = link.classed('error_object_link'); + var entityID = isObjectLink ? + (_error.object_type + _error.object_id) + : this.textContent; + var entity = context.hasEntity(entityID); + + // Add click handler + link + .on('mouseenter', function() { + context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph())) + .classed('hover', true); + }) + .on('mouseleave', function() { + context.surface().selectAll('.hover') + .classed('hover', false); + }) + .on('click', function() { + d3_event.preventDefault(); + var osmlayer = context.layers().layer('osm'); + if (!osmlayer.enabled()) { + osmlayer.enabled(true); + } + + context.map().centerZoom(_error.loc, 20); + + if (entity) { + context.enter(modeSelect(context, [entityID])); + } else { + context.loadEntity(entityID, function() { + context.enter(modeSelect(context, [entityID])); + }); + } + }); + + // Replace with friendly name if possible + // (The entity may not yet be loaded into the graph) + if (entity) { + var name = utilDisplayName(entity); // try to use common name + + if (!name && !isObjectLink) { + var preset = context.presets().match(entity, context.graph()); + name = preset && !preset.isFallback() && preset.name(); // fallback to preset name + } + + if (name) { + this.innerText = name; + } + } + }); + + // Don't hide entities related to this error - #5880 + context.features().forceVisible(relatedEntities); + context.map().pan([0,0]); // trigger a redraw + } + + + osmoseDetails.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return osmoseDetails; + }; + + + return osmoseDetails; +} \ No newline at end of file diff --git a/modules/ui/osmose_editor.js b/modules/ui/osmose_editor.js new file mode 100644 index 000000000..102144dbf --- /dev/null +++ b/modules/ui/osmose_editor.js @@ -0,0 +1,170 @@ +import { dispatch as d3_dispatch } from 'd3-dispatch'; + +import { t } from '../util/locale'; +import { services } from '../services'; +import { modeBrowse } from '../modes/browse'; +import { svgIcon } from '../svg/icon'; + +import { uiOsmoseDetails } from './osmose_details'; +import { uiOsmoseHeader } from './osmose_header'; +import { uiQuickLinks } from './quick_links'; +import { uiTooltipHtml } from './tooltipHtml'; + +import { utilRebind } from '../util'; + + +export function uiOsmoseEditor(context) { + var dispatch = d3_dispatch('change'); + var errorDetails = uiOsmoseDetails(context); + var errorHeader = uiOsmoseHeader(context); + var quickLinks = uiQuickLinks(); + + var _error; + + + function osmoseEditor(selection) { + // quick links + var choices = [{ + id: 'zoom_to', + label: 'inspector.zoom_to.title', + tooltip: function() { + return uiTooltipHtml(t('inspector.zoom_to.tooltip_issue'), t('inspector.zoom_to.key')); + }, + click: function zoomTo() { + context.mode().zoomToSelected(); + } + }]; + + + var header = selection.selectAll('.header') + .data([0]); + + var headerEnter = header.enter() + .append('div') + .attr('class', 'header fillL'); + + headerEnter + .append('button') + .attr('class', 'fr error-editor-close') + .on('click', function() { + context.enter(modeBrowse(context)); + }) + .call(svgIcon('#iD-icon-close')); + + headerEnter + .append('h3') + .text(t('QA.osmose.title')); + + + var body = selection.selectAll('.body') + .data([0]); + + body = body.enter() + .append('div') + .attr('class', 'body') + .merge(body); + + var editor = body.selectAll('.error-editor') + .data([0]); + + editor.enter() + .append('div') + .attr('class', 'modal-section error-editor') + .merge(editor) + .call(errorHeader.error(_error)) + .call(quickLinks.choices(choices)) + .call(errorDetails.error(_error)) + .call(osmoseSaveSection); + } + + function osmoseSaveSection(selection) { + var isSelected = (_error && _error.id === context.selectedErrorID()); + var isShown = (_error && isSelected); + var saveSection = selection.selectAll('.error-save') + .data( + (isShown ? [_error] : []), + function(d) { return d.id + '-' + (d.status || 0); } + ); + + // exit + saveSection.exit() + .remove(); + + // enter + var saveSectionEnter = saveSection.enter() + .append('div') + .attr('class', 'error-save save-section cf'); + + // update + saveSection = saveSectionEnter + .merge(saveSection) + .call(errorSaveButtons); + } + + function errorSaveButtons(selection) { + var isSelected = (_error && _error.id === context.selectedErrorID()); + var buttonSection = selection.selectAll('.buttons') + .data((isSelected ? [_error] : []), function(d) { return d.status + d.id; }); + + // exit + buttonSection.exit() + .remove(); + + // enter + var buttonEnter = buttonSection.enter() + .append('div') + .attr('class', 'buttons'); + + buttonEnter + .append('button') + .attr('class', 'button close-button action'); + + buttonEnter + .append('button') + .attr('class', 'button ignore-button action'); + + + // update + buttonSection = buttonSection + .merge(buttonEnter); + + buttonSection.select('.close-button') + .text(function(d) { + return t('QA.keepRight.close'); + }) + .on('click.close', function(d) { + this.blur(); // avoid keeping focus on the button - #4641 + var errorService = services.osmose; + if (errorService) { + d.newStatus = '/done'; + errorService.postUpdate(d, function(err, error) { + dispatch.call('change', error); + }); + } + }); + + buttonSection.select('.ignore-button') + .text(function(d) { + return t('QA.keepRight.ignore'); + }) + .on('click.ignore', function(d) { + this.blur(); // avoid keeping focus on the button - #4641 + var errorService = services.osmose; + if (errorService) { + d.newStatus = '/false'; + errorService.postUpdate(d, function(err, error) { + dispatch.call('change', error); + }); + } + }); + } + + osmoseEditor.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return osmoseEditor; + }; + + + return utilRebind(osmoseEditor, dispatch, 'on'); +} \ No newline at end of file diff --git a/modules/ui/osmose_header.js b/modules/ui/osmose_header.js new file mode 100644 index 000000000..21995cbcb --- /dev/null +++ b/modules/ui/osmose_header.js @@ -0,0 +1,97 @@ +import { dataEn } from '../../data'; +import { t } from '../util/locale'; + + +export function uiOsmoseHeader() { + var _error; + + + function errorTitle(d) { + var unknown = t('inspector.unknown'); + + if (!d) return unknown; + var errorType = d.error_type; + var et = dataEn.QA.osmose.error_types[errorType]; + + if (et && et.title) { + return t('QA.osmose.error_types.' + errorType + '.title'); + } else { + return unknown; + } + } + + + function osmoseHeader(selection) { + var header = selection.selectAll('.error-header') + .data( + (_error ? [_error] : []), + function(d) { return d.id + '-' + (d.status || 0); } + ); + + header.exit() + .remove(); + + var headerEnter = header.enter() + .append('div') + .attr('class', 'error-header'); + + var iconEnter = headerEnter + .append('div') + .attr('class', 'error-header-icon') + .classed('new', function(d) { return d.id < 0; }); + + var svgEnter = iconEnter + .append('svg') + .attr('width', '20px') + .attr('height', '30px') + .attr('viewbox', '0 0 20 30') + .attr('class', function(d) { + return [ + 'preset-icon-28', + 'qa_error', + d.service, + 'error_id-' + d.id, + 'error_type-' + d.error_type, + 'category-' + d.category + ].join(' '); + }); + + svgEnter + .append('polygon') + .attr('fill', 'currentColor') + .attr('class', 'qa_error-fill') + .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6'); + + svgEnter + .append('use') + .attr('class', 'icon-annotation') + .attr('width', '11px') + .attr('height', '11px') + .attr('transform', 'translate(4.5, 7)') + .attr('xlink:href', function(d) { + var picon = d.icon; + + if (!picon) { + return ''; + } else { + var isMaki = /^maki-/.test(picon); + return '#' + picon + (isMaki ? '-11' : ''); + } + }); + + headerEnter + .append('div') + .attr('class', 'error-header-label') + .text(errorTitle); + } + + + osmoseHeader.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return osmoseHeader; + }; + + + return osmoseHeader; +} \ No newline at end of file diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index 74ceeb52f..ebf9dd6a8 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -16,6 +16,7 @@ import { uiFeatureList } from './feature_list'; import { uiInspector } from './inspector'; import { uiImproveOsmEditor } from './improveOSM_editor'; import { uiKeepRightEditor } from './keepRight_editor'; +import { uiOsmoseEditor } from './osmose_editor'; import { uiNoteEditor } from './note_editor'; import { textDirection } from '../util/locale'; @@ -26,6 +27,7 @@ export function uiSidebar(context) { var noteEditor = uiNoteEditor(context); var improveOsmEditor = uiImproveOsmEditor(context); var keepRightEditor = uiKeepRightEditor(context); + var osmoseEditor = uiOsmoseEditor(context); var _current; var _wasData = false; var _wasNote = false; @@ -147,8 +149,15 @@ export function uiSidebar(context) { datum = errService.getError(datum.id); } - // Temporary solution while only two services - var errEditor = (datum.service === 'keepRight') ? keepRightEditor : improveOsmEditor; + // Currently only three possible services + var errEditor; + if (datum.service === 'keepRight') { + errEditor = keepRightEditor; + } else if (datum.service === 'osmose') { + errEditor = osmoseEditor; + } else { + errEditor = improveOsmEditor; + } d3_selectAll('.qa_error.' + datum.service) .classed('hover', function(d) { return d.id === datum.id; }); @@ -357,4 +366,4 @@ export function uiSidebar(context) { sidebar.toggle = function() {}; return sidebar; -} +} \ No newline at end of file