From 4f2646d56731aa4316c08933aba4e240a84eac7d Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 20 Jan 2019 13:21:26 +0000 Subject: [PATCH] Add basic title + description UI --- data/core.yaml | 12 +++ dist/locales/en.json | 17 +++++ modules/behavior/hover.js | 6 +- modules/behavior/select.js | 7 +- modules/modes/select_error.js | 50 ++++++++---- modules/services/improveOSM.js | 22 ++++++ modules/ui/improveOSM_details.js | 127 +++++++++++++++++++++++++++++++ modules/ui/improveOSM_editor.js | 90 ++++++++++++++++++++++ modules/ui/improveOSM_header.js | 66 ++++++++++++++++ modules/ui/index.js | 3 + 10 files changed, 382 insertions(+), 18 deletions(-) create mode 100644 modules/ui/improveOSM_details.js create mode 100644 modules/ui/improveOSM_editor.js create mode 100644 modules/ui/improveOSM_header.js diff --git a/data/core.yaml b/data/core.yaml index 0e9fd2fc8..6a4195511 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -661,6 +661,18 @@ en: cannot_zoom: "Cannot zoom out further in current mode." full_screen: Toggle Full Screen QA: + improveOSM: + title: ImproveOSM Error + error_types: + ow: + title: Missing One-way + description: Aggregate data suggests {var1} may be missing a "oneway" tag. Please confirm via imagery or survey before mapping. + mr: + title: Missing Road + description: Aggregate data suggests there may be an unmapped road here. Please confirm via imagery or survey before mapping. + tr: + title: Missing Turn Restriction + description: Aggregate data suggests this junction may be missing a turn restriction. Please confirm via imagery or survey before mapping. keepRight: title: KeepRight Error detail_title: Error diff --git a/dist/locales/en.json b/dist/locales/en.json index e8667b2f6..bea513f1d 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -803,6 +803,23 @@ "cannot_zoom": "Cannot zoom out further in current mode.", "full_screen": "Toggle Full Screen", "QA": { + "improveOSM": { + "title": "ImproveOSM Error", + "error_types": { + "ow": { + "title": "Missing One-way", + "description": "Aggregate data suggests {var1} may be missing a \"oneway\" tag. Please confirm via imagery or survey before mapping." + }, + "mr": { + "title": "Missing Road", + "description": "Aggregate data suggests there may be an unmapped road here. Please confirm via imagery or survey before mapping." + }, + "tr": { + "title": "Missing Turn Restriction", + "description": "Aggregate data suggests this junction may be missing a turn restriction. Please confirm via imagery or survey before mapping." + } + } + }, "keepRight": { "title": "KeepRight Error", "detail_title": "Error", diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index 58b9187a9..627fba543 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -5,7 +5,7 @@ import { select as d3_select } from 'd3-selection'; -import { osmEntity, osmNote, krError } from '../osm'; +import { osmEntity, osmNote, krError, impOsmError } from '../osm'; import { utilKeybinding, utilRebind } from '../util'; @@ -112,6 +112,10 @@ export function behaviorHover(context) { entity = datum; selector = '.data' + datum.__featurehash__; + } else if (datum instanceof impOsmError) { + entity = datum; + selector = '.iOSM_error-' + datum.id; + } else if (datum instanceof krError) { entity = datum; selector = '.kr_error-' + datum.id; diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 8c445930d..427d6b3b9 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -19,6 +19,7 @@ import { import { osmEntity, osmNote, + impOsmError, krError } from '../osm'; @@ -170,10 +171,14 @@ export function behaviorSelect(context) { context .selectedNoteID(datum.id) .enter(modeSelectNote(context, datum.id)); + } else if (datum instanceof impOsmError & !isMultiselect) { // clicked an improveOSM error + context + .selectedErrorID(datum.id) + .enter(modeSelectError(context, datum.id, 'ImproveOSM')); } else if (datum instanceof krError & !isMultiselect) { // clicked a krError error context .selectedErrorID(datum.id) - .enter(modeSelectError(context, datum.id)); + .enter(modeSelectError(context, datum.id, 'KeepRight')); } else { // clicked nothing.. context.selectedNoteID(null); context.selectedErrorID(null); diff --git a/modules/modes/select_error.js b/modules/modes/select_error.js index dbdae8aff..4472a7da5 100644 --- a/modules/modes/select_error.js +++ b/modules/modes/select_error.js @@ -13,26 +13,44 @@ import { import { t } from '../util/locale'; import { services } from '../services'; import { modeBrowse, modeDragNode, modeDragNote } from '../modes'; -import { uiKeepRightEditor } from '../ui'; +import { uiImproveOsmEditor, uiKeepRightEditor } from '../ui'; import { utilKeybinding } from '../util'; -export function modeSelectError(context, selectedErrorID) { +export function modeSelectError(context, selectedErrorID, selectedErrorSource) { var mode = { id: 'select-error', button: 'browse' }; - var keepRight = services.keepRight; var keybinding = utilKeybinding('select-error'); - var keepRightEditor = uiKeepRightEditor(context) - .on('change', function() { - context.map().pan([0,0]); // trigger a redraw - var error = checkSelectedID(); - if (!error) return; - context.ui().sidebar - .show(keepRightEditor.error(error)); - }); + + var errorService, errorEditor; + switch (selectedErrorSource) { + case 'ImproveOSM': + errorService = services.improveOSM; + errorEditor = uiImproveOsmEditor(context) + .on('change', function() { + context.map().pan([0,0]); // trigger a redraw + var error = checkSelectedID(); + if (!error) return; + context.ui().sidebar + .show(errorEditor.error(error)); + }); + break; + case 'KeepRight': + errorService = services.keepRight; + errorEditor = uiKeepRightEditor(context) + .on('change', function() { + context.map().pan([0,0]); // trigger a redraw + var error = checkSelectedID(); + if (!error) return; + context.ui().sidebar + .show(errorEditor.error(error)); + }); + break; + } + var behaviors = [ behaviorBreathe(context), @@ -45,8 +63,8 @@ export function modeSelectError(context, selectedErrorID) { function checkSelectedID() { - if (!keepRight) return; - var error = keepRight.getError(selectedErrorID); + if (!errorService) return; + var error = errorService.getError(selectedErrorID); if (!error) { context.enter(modeBrowse(context)); } @@ -55,8 +73,8 @@ export function modeSelectError(context, selectedErrorID) { mode.zoomToSelected = function() { - if (!keepRight) return; - var error = keepRight.getError(selectedErrorID); + if (!errorService) return; + var error = errorService.getError(selectedErrorID); if (error) { context.map().centerZoomEase(error.loc, 20); } @@ -78,7 +96,7 @@ export function modeSelectError(context, selectedErrorID) { selectError(); var sidebar = context.ui().sidebar; - sidebar.show(keepRightEditor.error(error)); + sidebar.show(errorEditor.error(error)); context.map() .on('drawn.select-error', selectError); diff --git a/modules/services/improveOSM.js b/modules/services/improveOSM.js index f4695766a..849749632 100644 --- a/modules/services/improveOSM.js +++ b/modules/services/improveOSM.js @@ -218,5 +218,27 @@ export default { return _erCache.rtree.search(bbox).map(function(d) { return d.data; }); + }, + + // get a single error from the cache + getError: function(id) { + return _erCache.data[id]; + }, + + // replace a single error in the cache + replaceError: function(error) { + if (!(error instanceof impOsmError) || !error.id) return; + + _erCache.data[error.id] = error; + updateRtree(encodeErrorRtree(error), true); // true = replace + return error; + }, + + // remove a single error from the cache + removeError: function(error) { + if (!(error instanceof impOsmError) || !error.id) return; + + delete _erCache.data[error.id]; + updateRtree(encodeErrorRtree(error), false); // false = remove } }; diff --git a/modules/ui/improveOSM_details.js b/modules/ui/improveOSM_details.js new file mode 100644 index 000000000..227cef8b9 --- /dev/null +++ b/modules/ui/improveOSM_details.js @@ -0,0 +1,127 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { dataEn } from '../../data'; +import { modeSelect } from '../modes'; +import { t } from '../util/locale'; +import { utilDisplayName, utilEntityOrMemberSelector, utilEntityRoot } from '../util'; + + +export function uiImproveOsmDetails(context) { + var _error; + + + function errorDetail(d) { + var unknown = t('inspector.unknown'); + + if (!d) return unknown; + var errorType = d.parent_error_type; + var et = dataEn.QA.improveOSM.error_types[errorType]; + + var detail; + if (et && et.description) { + detail = t('QA.improveOSM.error_types.' + errorType + '.description'); + } else { + detail = unknown; + } + + return detail; + } + + + function improveOsmDetails(selection) { + var details = selection.selectAll('.kr_error-details') + .data( + (_error ? [_error] : []), + function(d) { return d.id + '-' + (d.status || 0); } + ); + + details.exit() + .remove(); + + var detailsEnter = details.enter() + .append('div') + .attr('class', 'kr_error-details kr_error-details-container'); + + + // description + var descriptionEnter = detailsEnter + .append('div') + .attr('class', 'kr_error-details-description'); + + descriptionEnter + .append('h4') + .text(function() { return t('QA.keepRight.detail_description'); }); + + descriptionEnter + .append('div') + .attr('class', 'kr_error-details-description-text') + .html(errorDetail); + + // If there are entity links in the error message.. + descriptionEnter.selectAll('.kr_error_entity_link, .kr_error_object_link') + .each(function() { + var link = d3_select(this); + var isObjectLink = link.classed('kr_error_object_link'); + var entityID = isObjectLink ? + (utilEntityRoot(_error.object_type) + _error.object_id) + : this.textContent; + var entity = context.hasEntity(entityID); + + // Add click handler + link + .on('mouseover', function() { + context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph())) + .classed('hover', true); + }) + .on('mouseout', function() { + context.surface().selectAll('.hover') + .classed('hover', false); + }) + .on('click', function() { + d3_event.preventDefault(); + var osmlayer = context.layers().layer('osm'); + if (!osmlayer.enabled()) { + osmlayer.enabled(true); + } + + context.map().centerZoom(_error.loc, 20); + + 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; + } + } + }); + } + + + improveOsmDetails.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return improveOsmDetails; + }; + + + return improveOsmDetails; +} diff --git a/modules/ui/improveOSM_editor.js b/modules/ui/improveOSM_editor.js new file mode 100644 index 000000000..ed1d5065c --- /dev/null +++ b/modules/ui/improveOSM_editor.js @@ -0,0 +1,90 @@ +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { select as d3_select } from 'd3-selection'; + +import { t } from '../util/locale'; +import { services } from '../services'; +import { modeBrowse } from '../modes'; +import { svgIcon } from '../svg'; + +import { + uiImproveOsmDetails, + uiImproveOsmHeader, + uiQuickLinks, + uiTooltipHtml +} from './index'; + +import { utilNoAuto, utilRebind } from '../util'; + + +export function uiImproveOsmEditor(context) { + var dispatch = d3_dispatch('change'); + var errorDetails = uiImproveOsmDetails(context); + var errorHeader = uiImproveOsmHeader(context); + var quickLinks = uiQuickLinks(); + + var _error; + + + function improveOsmEditor(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 keepRight-editor-close') + .on('click', function() { + context.enter(modeBrowse(context)); + }) + .call(svgIcon('#iD-icon-close')); + + headerEnter + .append('h3') + .text(t('QA.improveOSM.title')); + + + var body = selection.selectAll('.body') + .data([0]); + + body = body.enter() + .append('div') + .attr('class', 'body') + .merge(body); + + var editor = body.selectAll('.keepRight-editor') + .data([0]); + + editor.enter() + .append('div') + .attr('class', 'modal-section keepRight-editor') + .merge(editor) + .call(errorHeader.error(_error)) + .call(quickLinks.choices(choices)) + .call(errorDetails.error(_error)); + } + + improveOsmEditor.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return improveOsmEditor; + }; + + + return utilRebind(improveOsmEditor, dispatch, 'on'); +} diff --git a/modules/ui/improveOSM_header.js b/modules/ui/improveOSM_header.js new file mode 100644 index 000000000..3a2f19eee --- /dev/null +++ b/modules/ui/improveOSM_header.js @@ -0,0 +1,66 @@ +import { dataEn } from '../../data'; +import { svgIcon } from '../svg'; +import { t } from '../util/locale'; + + +export function uiImproveOsmHeader() { + var _error; + + + function errorTitle(d) { + var unknown = t('inspector.unknown'); + + if (!d) return unknown; + var errorType = d.parent_error_type; + var et = dataEn.QA.improveOSM.error_types[errorType]; + + if (et && et.title) { + return t('QA.improveOSM.error_types.' + errorType + '.title'); + } else { + return unknown; + } + } + + + function improveOsmHeader(selection) { + var header = selection.selectAll('.kr_error-header') + .data( + (_error ? [_error] : []), + function(d) { return d.id + '-' + (d.status || 0); } + ); + + header.exit() + .remove(); + + var headerEnter = header.enter() + .append('div') + .attr('class', 'kr_error-header'); + + var iconEnter = headerEnter + .append('div') + .attr('class', 'kr_error-header-icon') + .classed('new', function(d) { return d.id < 0; }); + + iconEnter + .append('div') + .attr('class', function(d) { + return 'preset-icon-28 iOSM_error iOSM_error-' + d.id + ' iOSM_error_type_' + d.parent_error_type; + }) + .call(svgIcon('#iD-icon-bolt', 'iOSM_error-fill')); + + headerEnter + .append('div') + .attr('class', 'kr_error-header-label') + .text(errorTitle); + } + + + improveOsmHeader.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return improveOsmHeader; + }; + + + return improveOsmHeader; +} diff --git a/modules/ui/index.js b/modules/ui/index.js index de4ff07c7..c1361064e 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -28,6 +28,9 @@ export { uiFormFields } from './form_fields'; export { uiFullScreen } from './full_screen'; export { uiGeolocate } from './geolocate'; export { uiHelp } from './help'; +export { uiImproveOsmDetails } from './improveOSM_details'; +export { uiImproveOsmEditor } from './improveOSM_editor'; +export { uiImproveOsmHeader } from './improveOSM_header'; export { uiInfo } from './info'; export { uiInspector } from './inspector'; export { uiKeepRightDetails } from './keepRight_details';