From ec1c922cda70c2d47a59644b1a6c93cf03e7f75d Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Wed, 2 Dec 2020 15:13:33 -0500 Subject: [PATCH 01/17] Allow moving single nodes using the Move operation (close #8225) --- modules/operations/move.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/operations/move.js b/modules/operations/move.js index 4078714c8..677709d92 100644 --- a/modules/operations/move.js +++ b/modules/operations/move.js @@ -17,8 +17,7 @@ export function operationMove(context, selectedIDs) { operation.available = function() { - return selectedIDs.length > 1 || - context.entity(selectedIDs[0]).type !== 'node'; + return selectedIDs.length > 0; }; From 2dd0b8449f778f753e1b08b57e3a2939342fce1b Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 3 Dec 2020 12:18:39 -0500 Subject: [PATCH 02/17] Rewrite some confusing nested ternaries (close #8117) --- modules/behavior/draw_way.js | 11 +++++++++-- modules/services/improveOSM.js | 4 +++- modules/svg/notes.js | 8 ++++++-- modules/ui/note_header.js | 9 ++++++++- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 34a6dbda0..84d6af211 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -238,8 +238,15 @@ export function behaviorDrawWay(context, wayID, mode, startGraph) { _drawNode = undefined; _didResolveTempEdit = false; _origWay = context.entity(wayID); - _headNodeID = typeof _nodeIndex === 'number' ? _origWay.nodes[_nodeIndex] : - (_origWay.isClosed() ? _origWay.nodes[_origWay.nodes.length - 2] : _origWay.nodes[_origWay.nodes.length - 1]); + + if (typeof _nodeIndex === 'number') { + _headNodeID = _origWay.nodes[_nodeIndex]; + } else if (_origWay.isClosed()) { + _headNodeID = _origWay.nodes[_origWay.nodes.length - 2]; + } else { + _headNodeID = _origWay.nodes[_origWay.nodes.length - 1]; + } + _wayGeometry = _origWay.geometry(context.graph()); _annotation = t((_origWay.nodes.length === (_origWay.isClosed() ? 2 : 1) ? 'operations.start.annotation.' : diff --git a/modules/services/improveOSM.js b/modules/services/improveOSM.js index 9b139bff1..045788591 100644 --- a/modules/services/improveOSM.js +++ b/modules/services/improveOSM.js @@ -109,7 +109,9 @@ function preventCoincident(loc, bumpUp) { let coincident = false; do { // first time, move marker up. after that, move marker right. - let delta = coincident ? [0.00001, 0] : (bumpUp ? [0, 0.00001] : [0, 0]); + let delta = coincident ? [0.00001, 0] : + bumpUp ? [0, 0.00001] : + [0, 0]; loc = geoVecAdd(loc, delta); let bbox = geoExtent(loc).bbox(); coincident = _cache.rtree.search(bbox).length; diff --git a/modules/svg/notes.js b/modules/svg/notes.js index bc8fc55e7..8d2c6376e 100644 --- a/modules/svg/notes.js +++ b/modules/svg/notes.js @@ -153,7 +153,9 @@ export function svgNotes(projection, context, dispatch) { .attr('x', '-3px') .attr('y', '-19px') .attr('xlink:href', function(d) { - return '#iD-icon-' + (d.id < 0 ? 'plus' : (d.status === 'open' ? 'close' : 'apply')); + if (d.id < 0) return '#iD-icon-plus'; + if (d.status === 'open') return '#iD-icon-close'; + return '#iD-icon-apply'; }); // update @@ -196,7 +198,9 @@ export function svgNotes(projection, context, dispatch) { function sortY(a, b) { - return (a.id === selectedID) ? 1 : (b.id === selectedID) ? -1 : b.loc[1] - a.loc[1]; + if (a.id === selectedID) return 1; + if (b.id === selectedID) return -1; + return b.loc[1] - a.loc[1]; } } diff --git a/modules/ui/note_header.js b/modules/ui/note_header.js index 99b1d4c44..c496e5b8c 100644 --- a/modules/ui/note_header.js +++ b/modules/ui/note_header.js @@ -31,7 +31,14 @@ export function uiNoteHeader() { .call(svgIcon('#iD-icon-note', 'note-fill')); iconEnter.each(function(d) { - var statusIcon = '#iD-icon-' + (d.id < 0 ? 'plus' : (d.status === 'open' ? 'close' : 'apply')); + var statusIcon; + if (d.id < 0) { + statusIcon = '#iD-icon-plus'; + } else if (d.status === 'open') { + statusIcon = '#iD-icon-close'; + } else { + statusIcon = '#iD-icon-apply'; + } iconEnter .append('div') .attr('class', 'note-icon-annotation') From 07a59ba3a4c23e0564e83a52df7cdec16c385623 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 3 Dec 2020 12:23:09 -0500 Subject: [PATCH 03/17] Break out some preset icon functions (re: #8117) --- modules/ui/preset_icon.js | 98 ++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/modules/ui/preset_icon.js b/modules/ui/preset_icon.js index 044150be5..5a1e82664 100644 --- a/modules/ui/preset_icon.js +++ b/modules/ui/preset_icon.js @@ -271,6 +271,59 @@ export function uiPresetIcon() { } } + function renderSvgIcon(container, picon, geom, isFramed, tagClasses) { + const isMaki = picon && /^maki-/.test(picon); + const isTemaki = picon && /^temaki-/.test(picon); + const isFa = picon && /^fa[srb]-/.test(picon); + const isiDIcon = picon && !(isMaki || isTemaki || isFa); + + let icon = container.selectAll('.preset-icon') + .data(picon ? [0] : []); + + icon.exit() + .remove(); + + icon = icon.enter() + .append('div') + .attr('class', 'preset-icon') + .call(svgIcon('')) + .merge(icon); + + icon + .attr('class', 'preset-icon ' + (geom ? geom + '-geom' : '')) + .classed('framed', isFramed) + .classed('preset-icon-iD', isiDIcon); + + icon.selectAll('svg') + .attr('class', 'icon ' + picon + ' ' + (!isiDIcon && geom !== 'line' ? '' : tagClasses)); + + var suffix = ''; + if (isMaki) { + suffix = isSmall() && geom === 'point' ? '-11' : '-15'; + } + + icon.selectAll('use') + .attr('href', '#' + picon + suffix); + } + + + function renderImageIcon(container, imageURL) { + let imageIcon = container.selectAll('img.image-icon') + .data(imageURL ? [0] : []); + + imageIcon.exit() + .remove(); + + imageIcon = imageIcon.enter() + .append('img') + .attr('class', 'image-icon') + .on('load', () => container.classed('showing-img', true) ) + .on('error', () => container.classed('showing-img', false) ) + .merge(imageIcon); + + imageIcon + .attr('src', imageURL); + } // Route icons are drawn with a zigzag annotation underneath: // o o @@ -310,10 +363,6 @@ export function uiPresetIcon() { const isFallback = isSmall() && p.isFallback && p.isFallback(); const imageURL = (showThirdPartyIcons === 'true') && p.imageURL; const picon = getIcon(p, geom); - const isMaki = picon && /^maki-/.test(picon); - const isTemaki = picon && /^temaki-/.test(picon); - const isFa = picon && /^fa[srb]-/.test(picon); - const isiDIcon = picon && !(isMaki || isTemaki || isFa); const isCategory = !p.setTags; const drawPoint = picon && geom === 'point' && isSmall() && !isFallback; const drawVertex = picon !== null && geom === 'vertex' && (!isSmall() || !isFallback); @@ -349,45 +398,8 @@ export function uiPresetIcon() { renderSquareFill(container, drawArea, tagClasses); renderLine(container, drawLine, tagClasses); renderRoute(container, drawRoute, p); - - let icon = container.selectAll('.preset-icon') - .data(picon ? [0] : []); - - icon.exit() - .remove(); - - icon = icon.enter() - .append('div') - .attr('class', 'preset-icon') - .call(svgIcon('')) - .merge(icon); - - icon - .attr('class', 'preset-icon ' + (geom ? geom + '-geom' : '')) - .classed('framed', isFramed) - .classed('preset-icon-iD', isiDIcon); - - icon.selectAll('svg') - .attr('class', 'icon ' + picon + ' ' + (!isiDIcon && geom !== 'line' ? '' : tagClasses)); - - icon.selectAll('use') - .attr('href', '#' + picon + (isMaki ? (isSmall() && geom === 'point' ? '-11' : '-15') : '')); - - let imageIcon = container.selectAll('img.image-icon') - .data(imageURL ? [0] : []); - - imageIcon.exit() - .remove(); - - imageIcon = imageIcon.enter() - .append('img') - .attr('class', 'image-icon') - .on('load', () => container.classed('showing-img', true) ) - .on('error', () => container.classed('showing-img', false) ) - .merge(imageIcon); - - imageIcon - .attr('src', imageURL); + renderSvgIcon(container, picon, geom, isFramed, tagClasses); + renderImageIcon(container, imageURL); } From 9f6aaf2438fe3c6efbde78c3107fa5a49018338c Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 3 Dec 2020 13:13:08 -0500 Subject: [PATCH 04/17] Add folder border to preset category icons (close #6085) --- css/80_app.css | 12 ++++++++++-- modules/ui/preset_icon.js | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index a38f15886..65fec69fa 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1094,6 +1094,14 @@ a.hide-toggle { fill: transparent; } +.preset-icon-category-border path { + stroke: #999; + stroke-width: 1px; + fill: transparent; + backface-visibility: hidden; + vector-effect: non-scaling-stroke; +} + .preset-icon-line { margin: auto; position: absolute; @@ -1169,7 +1177,7 @@ a.hide-toggle { .preset-icon.framed .icon { transform: scale(0.4); } -.preset-icon.framed.line-geom .icon, +.preset-icon.framed.line-geom:not(.category) .icon, .preset-icon.framed.route-geom .icon { top: 20%; transform: translateY(-30%) scale(0.4); @@ -1180,7 +1188,7 @@ a.hide-toggle { .preset-icon-iD.framed .icon { transform: scale(0.74); } -.preset-icon-iD.framed.line-geom .icon, +.preset-icon-iD.framed.line-geom:not(.category) .icon, .preset-icon-iD.framed.route-geom .icon { transform: translateY(-30%) scale(0.74); } diff --git a/modules/ui/preset_icon.js b/modules/ui/preset_icon.js index 5a1e82664..51bdae9d3 100644 --- a/modules/ui/preset_icon.js +++ b/modules/ui/preset_icon.js @@ -59,6 +59,30 @@ export function uiPresetIcon() { } + function renderCategoryBorder(container, drawBorder) { + let categoryBorder = container.selectAll('.preset-icon-category-border') + .data(drawBorder ? [0] : []); + + categoryBorder.exit() + .remove(); + + let categoryBorderEnter = categoryBorder.enter(); + + const d = 60; + + categoryBorderEnter + .append('svg') + .attr('class', 'preset-icon-fill preset-icon-category-border') + .attr('width', d) + .attr('height', d) + .attr('viewBox', `0 0 ${d} ${d}`) + .append('path') + .attr('d', 'M9.5,7.5 L25.5,7.5 L28.5,12.5 L49.5,12.5 C51.709139,12.5 53.5,14.290861 53.5,16.5 L53.5,43.5 C53.5,45.709139 51.709139,47.5 49.5,47.5 L10.5,47.5 C8.290861,47.5 6.5,45.709139 6.5,43.5 L6.5,12.5 L9.5,7.5 Z'); + + categoryBorder = categoryBorderEnter.merge(categoryBorder); + } + + function renderCircleFill(container, drawVertex) { let vertexFill = container.selectAll('.preset-icon-fill-vertex') .data(drawVertex ? [0] : []); @@ -271,7 +295,7 @@ export function uiPresetIcon() { } } - function renderSvgIcon(container, picon, geom, isFramed, tagClasses) { + function renderSvgIcon(container, picon, geom, isFramed, category, tagClasses) { const isMaki = picon && /^maki-/.test(picon); const isTemaki = picon && /^temaki-/.test(picon); const isFa = picon && /^fa[srb]-/.test(picon); @@ -291,6 +315,7 @@ export function uiPresetIcon() { icon .attr('class', 'preset-icon ' + (geom ? geom + '-geom' : '')) + .classed('category', category) .classed('framed', isFramed) .classed('preset-icon-iD', isiDIcon); @@ -367,9 +392,9 @@ export function uiPresetIcon() { const drawPoint = picon && geom === 'point' && isSmall() && !isFallback; const drawVertex = picon !== null && geom === 'vertex' && (!isSmall() || !isFallback); const drawLine = picon && geom === 'line' && !isFallback && !isCategory; - const drawArea = picon && geom === 'area' && !isFallback; + const drawArea = picon && geom === 'area' && !isFallback && !isCategory; const drawRoute = picon && geom === 'route'; - const isFramed = (drawVertex || drawArea || drawLine || drawRoute); + const isFramed = drawVertex || drawArea || drawLine || drawRoute || isCategory; let tags = !isCategory ? p.setTags({}, geom) : {}; for (let k in tags) { @@ -393,12 +418,13 @@ export function uiPresetIcon() { .classed('showing-img', !!imageURL) .classed('fallback', isFallback); + renderCategoryBorder(container, isCategory); renderPointBorder(container, drawPoint); renderCircleFill(container, drawVertex); renderSquareFill(container, drawArea, tagClasses); renderLine(container, drawLine, tagClasses); renderRoute(container, drawRoute, p); - renderSvgIcon(container, picon, geom, isFramed, tagClasses); + renderSvgIcon(container, picon, geom, isFramed, isCategory, tagClasses); renderImageIcon(container, imageURL); } From a85ac34a35cef949cd5c3eaa9174af73f5afdff6 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 3 Dec 2020 13:50:56 -0500 Subject: [PATCH 05/17] Add tooltip to the review request checkbox (close #7227) --- data/core.yaml | 1 + dist/locales/en.json | 1 + modules/ui/commit.js | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/data/core.yaml b/data/core.yaml index c97715be5..33595c94e 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -580,6 +580,7 @@ en: upload_explanation: "The changes you upload will be visible on all maps that use OpenStreetMap data." upload_explanation_with_user: "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data." request_review: "I would like someone to review my edits." + request_review_info: "Unsure about something? Invite an experienced mapper to check your work once it's live." save: Upload cancel: Cancel changes: Changes diff --git a/dist/locales/en.json b/dist/locales/en.json index 8e1e19044..8896251bb 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -769,6 +769,7 @@ "upload_explanation": "The changes you upload will be visible on all maps that use OpenStreetMap data.", "upload_explanation_with_user": "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data.", "request_review": "I would like someone to review my edits.", + "request_review_info": "Unsure about something? Invite an experienced mapper to check your work once it's live.", "save": "Upload", "cancel": "Cancel", "changes": "Changes", diff --git a/modules/ui/commit.js b/modules/ui/commit.js index e3ad030cb..5beec1448 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -323,6 +323,11 @@ export function uiCommit(context) { .append('label') .attr('for', requestReviewDomId); + if (!labelEnter.empty()) { + labelEnter + .call(uiTooltip().title(t.html('commit.request_review_info')).placement('top')); + } + labelEnter .append('input') .attr('type', 'checkbox') From 611abf8ed0dec55246eb1493eb26a30a15e9b29e Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 3 Dec 2020 15:04:21 -0500 Subject: [PATCH 06/17] Enable rotating selection even when mouse isn't over the map (re: #8187) --- modules/modes/rotate.js | 4 ++-- modules/renderer/map.js | 2 +- modules/util/util.js | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 85b761b6e..68aa601a9 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -50,7 +50,7 @@ export function modeRotate(context, entityIDs) { var _pivot; - function doRotate() { + function doRotate(d3_event) { var fn; if (context.graph() !== _prevGraph) { fn = context.perform; @@ -73,7 +73,7 @@ export function modeRotate(context, entityIDs) { } - var currMouse = context.map().mouse(); + var currMouse = context.map().mouse(d3_event); var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]); if (typeof _prevAngle === 'undefined') _prevAngle = currAngle; diff --git a/modules/renderer/map.js b/modules/renderer/map.js index c2177da29..4d5c6491f 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -710,7 +710,7 @@ export function rendererMap(context) { map.mouse = function(d3_event) { - var event = _lastPointerEvent || d3_event; + var event = d3_event || _lastPointerEvent; if (event) { var s; while ((s = event.sourceEvent)) { event = s; } diff --git a/modules/util/util.js b/modules/util/util.js index 7131a4d3f..390396981 100644 --- a/modules/util/util.js +++ b/modules/util/util.js @@ -447,7 +447,8 @@ export function utilFastMouse(container) { return function(e) { return [ e.clientX - rectLeft - clientLeft, - e.clientY - rectTop - clientTop]; + e.clientY - rectTop - clientTop + ]; }; } From 655c3a692cd14cc0aaa6f95e3063179aa7c7a8d5 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 3 Dec 2020 15:05:19 -0500 Subject: [PATCH 07/17] Enable dragging the map during feature move and selection without exiting the mode (close #8187) --- modules/behavior/draw.js | 3 ++- modules/modes/move.js | 42 ++++++++++++++++++++++++++++++++------- modules/modes/rotate.js | 43 ++++++++++++++++++++++++++++++++-------- 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 7f6c2216d..5a541ba80 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -89,7 +89,8 @@ export function behaviorDraw(context) { var p2 = downPointer.pointerLocGetter(d3_event); var dist = geoVecLength(downPointer.downLoc, p2); - if (dist < _closeTolerance || (dist < _tolerance && (t2 - downPointer.downTime) < 500)) { + if (dist < _closeTolerance || + (dist < _tolerance && (t2 - downPointer.downTime) < 500)) { // Prevent a quick second click d3_select(window).on('click.draw-block', function() { d3_event.stopPropagation(); diff --git a/modules/modes/move.js b/modules/modes/move.js index 4e7de30db..d1cfb7ba8 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -7,10 +7,12 @@ import { t } from '../core/localizer'; import { actionMove } from '../actions/move'; import { actionNoop } from '../actions/noop'; import { behaviorEdit } from '../behavior/edit'; -import { geoViewportEdge, geoVecSubtract } from '../geo'; +import { geoVecLength, geoVecSubtract } from '../geo/vector'; +import { geoViewportEdge } from '../geo/geom'; import { modeBrowse } from './browse'; import { modeSelect } from './select'; import { utilKeybinding } from '../util'; +import { utilFastMouse } from '../util/util'; import { operationCircularize } from '../operations/circularize'; @@ -21,6 +23,9 @@ import { operationRotate } from '../operations/rotate'; export function modeMove(context, entityIDs, baseGraph) { + + var _tolerancePx = 4; // see also behaviorDrag, behaviorSelect, modeRotate + var mode = { id: 'move', button: 'browse' @@ -45,6 +50,9 @@ export function modeMove(context, entityIDs, baseGraph) { var _origin; var _nudgeInterval; + // use pointer events on supported platforms; fallback to mouse events + var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse'; + function doMove(nudge) { nudge = nudge || [0, 0]; @@ -129,12 +137,29 @@ export function modeMove(context, entityIDs, baseGraph) { behaviors.forEach(context.install); + var downEvent; + context.surface() - .on('mousemove.move', move) - .on('click.move', finish); + .on(_pointerPrefix + 'down.modeMove', function(d3_event) { + downEvent = d3_event; + }); + + d3_select(window) + .on(_pointerPrefix + 'move.modeMove', move, true) + .on(_pointerPrefix + 'up.modeMove', function(d3_event) { + if (!downEvent) return; + var mapNode = context.container().select('.main-map').node(); + var pointGetter = utilFastMouse(mapNode); + var p1 = pointGetter(downEvent); + var p2 = pointGetter(d3_event); + var dist = geoVecLength(p1, p2); + + if (dist <= _tolerancePx) finish(d3_event); + downEvent = null; + }, true); context.history() - .on('undone.move', undone); + .on('undone.modeMove', undone); keybinding .on('⎋', cancel) @@ -153,11 +178,14 @@ export function modeMove(context, entityIDs, baseGraph) { }); context.surface() - .on('mousemove.move', null) - .on('click.move', null); + .on(_pointerPrefix + 'down.modeMove', null); + + d3_select(window) + .on(_pointerPrefix + 'move.modeMove', null, true) + .on(_pointerPrefix + 'up.modeMove', null, true); context.history() - .on('undone.move', null); + .on('undone.modeMove', null); d3_select(document) .call(keybinding.unbind); diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 68aa601a9..454e0e028 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -11,7 +11,7 @@ import { t } from '../core/localizer'; import { actionRotate } from '../actions/rotate'; import { actionNoop } from '../actions/noop'; import { behaviorEdit } from '../behavior/edit'; -import { geoVecInterp } from '../geo'; +import { geoVecInterp, geoVecLength } from '../geo/vector'; import { modeBrowse } from './browse'; import { modeSelect } from './select'; @@ -21,10 +21,14 @@ import { operationMove } from '../operations/move'; import { operationOrthogonalize } from '../operations/orthogonalize'; import { operationReflectLong, operationReflectShort } from '../operations/reflect'; -import { utilGetAllNodes, utilKeybinding } from '../util'; +import { utilKeybinding } from '../util/keybinding'; +import { utilFastMouse, utilGetAllNodes } from '../util/util'; export function modeRotate(context, entityIDs) { + + var _tolerancePx = 4; // see also behaviorDrag, behaviorSelect, modeMove + var mode = { id: 'rotate', button: 'browse' @@ -49,6 +53,9 @@ export function modeRotate(context, entityIDs) { var _prevTransform; var _pivot; + // use pointer events on supported platforms; fallback to mouse events + var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse'; + function doRotate(d3_event) { var fn; @@ -127,12 +134,29 @@ export function modeRotate(context, entityIDs) { behaviors.forEach(context.install); + var downEvent; + context.surface() - .on('mousemove.rotate', doRotate) - .on('click.rotate', finish); + .on(_pointerPrefix + 'down.modeRotate', function(d3_event) { + downEvent = d3_event; + }); + + d3_select(window) + .on(_pointerPrefix + 'move.modeRotate', doRotate, true) + .on(_pointerPrefix + 'up.modeRotate', function(d3_event) { + if (!downEvent) return; + var mapNode = context.container().select('.main-map').node(); + var pointGetter = utilFastMouse(mapNode); + var p1 = pointGetter(downEvent); + var p2 = pointGetter(d3_event); + var dist = geoVecLength(p1, p2); + + if (dist <= _tolerancePx) finish(d3_event); + downEvent = null; + }, true); context.history() - .on('undone.rotate', undone); + .on('undone.modeRotate', undone); keybinding .on('⎋', cancel) @@ -147,11 +171,14 @@ export function modeRotate(context, entityIDs) { behaviors.forEach(context.uninstall); context.surface() - .on('mousemove.rotate', null) - .on('click.rotate', null); + .on(_pointerPrefix + 'down.modeRotate', null); + + d3_select(window) + .on(_pointerPrefix + 'move.modeRotate', null, true) + .on(_pointerPrefix + 'up.modeRotate', null, true); context.history() - .on('undone.rotate', null); + .on('undone.modeRotate', null); d3_select(document) .call(keybinding.unbind); From 0bd2351d68aef6cba627b2ce85da101d01fddcf0 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Thu, 3 Dec 2020 16:26:58 -0500 Subject: [PATCH 08/17] Don't remove multilingual name list items when deleting the name value (close #8164) Allow removing multilingual name list items that don't have full values --- modules/ui/fields/localized.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/modules/ui/fields/localized.js b/modules/ui/fields/localized.js index 89639f4d1..fc0fdbde1 100644 --- a/modules/ui/fields/localized.js +++ b/modules/ui/fields/localized.js @@ -125,7 +125,7 @@ export function uiFieldLocalized(field, context) { var existingLangs = new Set(existingLangsOrdered.filter(Boolean)); for (var k in tags) { - var m = k.match(/^(.*):(.+)$/); + var m = k.match(/^(.*):(.*)$/); if (m && m[1] === field.key && m[2]) { var item = { lang: m[2], value: tags[k] }; if (existingLangs.has(item.lang)) { @@ -424,7 +424,7 @@ export function uiFieldLocalized(field, context) { function changeValue(d3_event, d) { if (!d.lang) return; - var value = context.cleanTagValue(utilGetSetValue(d3_select(this))) || undefined; + var value = context.cleanTagValue(utilGetSetValue(d3_select(this))) || ''; // don't override multiple values with blank string if (!value && Array.isArray(d.value)) return; @@ -512,16 +512,19 @@ export function uiFieldLocalized(field, context) { if (field.locked()) return; d3_event.preventDefault(); - if (!d.lang || !d.value) { - _multilingual.splice(index, 1); - renderMultilingual(selection); - } else { - // remove from entity tags - var t = {}; - t[key(d.lang)] = undefined; - dispatch.call('change', this, t); + if (d.lang) { + var langKey = key(d.lang); + if (langKey in _tags) { + // remove from entity tags + var t = {}; + t[langKey] = undefined; + dispatch.call('change', this, t); + return; + } } + _multilingual.splice(index, 1); + renderMultilingual(selection); }) .call(svgIcon('#iD-operation-delete')); @@ -562,9 +565,8 @@ export function uiFieldLocalized(field, context) { entries.order(); - entries.classed('present', function(d) { - return d.lang && d.value; - }); + // allow removing the entry UIs even if there isn't a tag to remove + entries.classed('present', true); utilGetSetValue(entries.select('.localized-lang'), function(d) { var langItem = _languagesArray.find(function(item) { From 7d81c81f34532ca6551b38e7b0e8fe9d1b660b47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Dec 2020 05:31:19 +0000 Subject: [PATCH 09/17] Bump d3 from 6.2.0 to 6.3.0 Bumps [d3](https://github.com/d3/d3) from 6.2.0 to 6.3.0. - [Release notes](https://github.com/d3/d3/releases) - [Changelog](https://github.com/d3/d3/blob/master/CHANGES.md) - [Commits](https://github.com/d3/d3/compare/v6.2.0...v6.3.0) Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e56199c7..5b031fa18 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "cldr-localenames-full": "37.0.0", "colors": "^1.1.2", "concat-files": "^0.1.1", - "d3": "~6.2.0", + "d3": "~6.3.0", "editor-layer-index": "github:osmlab/editor-layer-index#gh-pages", "eslint": "^7.1.0", "gaze": "^1.1.3", From 0bc2b1b6a027d878bfd065825c8fab1abc8fae10 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Fri, 4 Dec 2020 10:22:18 -0500 Subject: [PATCH 10/17] Improve multilingual name UI updates, don't use blank tag values (re: #8164) --- modules/ui/fields/localized.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/ui/fields/localized.js b/modules/ui/fields/localized.js index fc0fdbde1..6bb630a26 100644 --- a/modules/ui/fields/localized.js +++ b/modules/ui/fields/localized.js @@ -138,8 +138,12 @@ export function uiFieldLocalized(field, context) { } } - _multilingual = _multilingual.filter(function(item) { - return !item.lang || !existingLangs.has(item.lang); + // Don't remove items based on deleted tags, since this makes the UI + // disappear unexpectedly when clearing values - #8164 + _multilingual.forEach(function(item) { + if (item.lang && existingLangs.has(item.lang)) { + item.value = ''; + } }); } @@ -424,7 +428,7 @@ export function uiFieldLocalized(field, context) { function changeValue(d3_event, d) { if (!d.lang) return; - var value = context.cleanTagValue(utilGetSetValue(d3_select(this))) || ''; + var value = context.cleanTagValue(utilGetSetValue(d3_select(this))) || undefined; // don't override multiple values with blank string if (!value && Array.isArray(d.value)) return; @@ -512,6 +516,9 @@ export function uiFieldLocalized(field, context) { if (field.locked()) return; d3_event.preventDefault(); + // remove the UI item manually + _multilingual.splice(_multilingual.indexOf(d), 1); + if (d.lang) { var langKey = key(d.lang); if (langKey in _tags) { @@ -519,11 +526,9 @@ export function uiFieldLocalized(field, context) { var t = {}; t[langKey] = undefined; dispatch.call('change', this, t); - return; } } - _multilingual.splice(index, 1); renderMultilingual(selection); }) .call(svgIcon('#iD-operation-delete')); From 40362f1d08ed1d80e26972671bcd6c152001de66 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Fri, 4 Dec 2020 11:59:51 -0500 Subject: [PATCH 11/17] Fix issue with removing multilingual name entries --- modules/ui/fields/localized.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/ui/fields/localized.js b/modules/ui/fields/localized.js index 6bb630a26..25193a9f5 100644 --- a/modules/ui/fields/localized.js +++ b/modules/ui/fields/localized.js @@ -519,14 +519,14 @@ export function uiFieldLocalized(field, context) { // remove the UI item manually _multilingual.splice(_multilingual.indexOf(d), 1); - if (d.lang) { - var langKey = key(d.lang); - if (langKey in _tags) { - // remove from entity tags - var t = {}; - t[langKey] = undefined; - dispatch.call('change', this, t); - } + var langKey = d.lang && key(d.lang); + if (langKey && langKey in _tags) { + delete _tags[langKey]; + // remove from entity tags + var t = {}; + t[langKey] = undefined; + dispatch.call('change', this, t); + return; } renderMultilingual(selection); From 67773d12a82033c30cf4804db93a6a5ebe7fcdd9 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Fri, 4 Dec 2020 12:05:09 -0500 Subject: [PATCH 12/17] Fix an issue where the map attribution container would unexpectedly block map interaction (close #8192) --- css/80_app.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/css/80_app.css b/css/80_app.css index 65fec69fa..62c26044b 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -4390,9 +4390,12 @@ img.tile-debug { justify-content: space-between; align-items: flex-end; z-index: 0; + pointer-events: none; } -.attribution-wrap * { pointer-events: all; } +.attribution-wrap > * { + pointer-events: auto; +} .attribution-wrap .base-layer-attribution, .attribution-wrap .overlay-layer-attribution { From 4eabf46d28af8699f18580c7f927fa411f3a6c12 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Fri, 4 Dec 2020 13:41:32 -0500 Subject: [PATCH 13/17] Don't point transifex config to presets source file (close #8234) --- .tx/config | 1 - 1 file changed, 1 deletion(-) diff --git a/.tx/config b/.tx/config index 21389fb4a..6fad31b2f 100644 --- a/.tx/config +++ b/.tx/config @@ -15,7 +15,6 @@ type = YAML [id-editor.presets] file_filter = .tx/tmp/presets/.yaml -source_file = data/presets.yaml source_lang = en type = YAML From a7b3985237b4c9ac966e10a8b67347a7a3ff4eed Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Fri, 4 Dec 2020 16:22:36 -0500 Subject: [PATCH 14/17] Flag points as areas or lines, areas or lines as points, and lines as areas (close #8231) --- ARCHITECTURE.md | 11 +- data/core.yaml | 11 ++ dist/locales/en.json | 18 ++- modules/validations/mismatched_geometry.js | 144 ++++++++++++++++----- 4 files changed, 147 insertions(+), 37 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3e0ac166c..83c0aadee 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -457,8 +457,17 @@ An issue with the active [MapRules](https://github.com/radiant-maxar/maprules) v A feature's tags indicate it should have a different geometry than it currently does. * `area_as_line`: an unclosed way has tags implying it should be a closed area (e.g. `area=yes` or `building=yes`) -* `vertex_as_point`: a detached node has tags implying it should be part of a way (e.g. `highway=stop`) +* `area_as_point` +* `area_as_vertex` +* `line_as_area` +* `line_as_point` +* `line_as_vertex`: a detached node has tags implying it should be a line (e.g. `highway=motorway`) +* `point_as_area` +* `point_as_line` * `point_as_vertex`: a vertex node has tags implying it should be detached from ways (e.g. `amenity=cafe`) +* `vertex_as_area` +* `vertex_as_line` +* `vertex_as_point`: a detached node has tags implying it should be part of a way (e.g. `highway=stop`) * `unclosed_multipolygon_part`: a relation is tagged as a multipolygon but not all of its member ways form closed rings ##### `missing_role` diff --git a/data/core.yaml b/data/core.yaml index 33595c94e..5eb5a0349 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1650,6 +1650,8 @@ en: message: "{feature} ends very close to itself but does not reconnect" highway-highway: reference: Intersecting highways should share a junction vertex. + area_as_point: + message: '{feature} should be an area, not a point' close_nodes: title: "Very Close Points" tip: "Find redundant and crowded points" @@ -1729,9 +1731,14 @@ en: message: '{feature} has an invalid email address' message_multi: '{feature} has multiple invalid email addresses' reference: 'Email addresses must look like "user@example.com".' + line_as_area: + message: '{feature} should be a line, not an area' + line_as_point: + message: '{feature} should be a line, not a point' mismatched_geometry: title: Mismatched Geometry tip: "Find features with conflicting tags and geometry" + reference: Most features are limited to certain geometry types. missing_role: title: Missing Roles message: "{member} has no role within {relation}" @@ -1763,6 +1770,10 @@ en: message: "{feature} looks like a brand with nonstandard tags" message_incomplete: "{feature} looks like a brand with incomplete tags" reference: "All features of the same brand should be tagged the same way." + point_as_area: + message: '{feature} should be a point, not an area' + point_as_line: + message: '{feature} should be a point, not a line' point_as_vertex: message: '{feature} should be a standalone point based on its tags' reference: "Some features shouldn't be part of lines or areas." diff --git a/dist/locales/en.json b/dist/locales/en.json index 8896251bb..771b017c1 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -2067,6 +2067,9 @@ "reference": "Intersecting highways should share a junction vertex." } }, + "area_as_point": { + "message": "{feature} should be an area, not a point" + }, "close_nodes": { "title": "Very Close Points", "tip": "Find redundant and crowded points", @@ -2177,9 +2180,16 @@ "reference": "Email addresses must look like \"user@example.com\"." } }, + "line_as_area": { + "message": "{feature} should be a line, not an area" + }, + "line_as_point": { + "message": "{feature} should be a line, not a point" + }, "mismatched_geometry": { "title": "Mismatched Geometry", - "tip": "Find features with conflicting tags and geometry" + "tip": "Find features with conflicting tags and geometry", + "reference": "Most features are limited to certain geometry types." }, "missing_role": { "title": "Missing Roles", @@ -2222,6 +2232,12 @@ "reference": "All features of the same brand should be tagged the same way." } }, + "point_as_area": { + "message": "{feature} should be a point, not an area" + }, + "point_as_line": { + "message": "{feature} should be a point, not a line" + }, "point_as_vertex": { "message": "{feature} should be a standalone point based on its tags", "reference": "Some features shouldn't be part of lines or areas." diff --git a/modules/validations/mismatched_geometry.js b/modules/validations/mismatched_geometry.js index d2945d602..5f7c5a5f7 100644 --- a/modules/validations/mismatched_geometry.js +++ b/modules/validations/mismatched_geometry.js @@ -1,3 +1,4 @@ +import deepEqual from 'fast-deep-equal'; import { actionAddVertex } from '../actions/add_vertex'; import { actionChangeTags } from '../actions/change_tags'; import { actionMergeNodes } from '../actions/merge_nodes'; @@ -139,7 +140,7 @@ export function validationMismatchedGeometry() { } } - function vertexTaggedAsPointIssue(entity, graph) { + function vertexPointIssue(entity, graph) { // we only care about nodes if (entity.type !== 'node') return null; @@ -196,46 +197,115 @@ export function validationMismatchedGeometry() { .html(t.html('issues.point_as_vertex.reference')); }, entityIds: [entity.id], - dynamicFixes: function(context) { - - var entityId = this.entityIds[0]; - - var extractOnClick = null; - if (!context.hasHiddenConnections(entityId)) { - - extractOnClick = function(context) { - var entityId = this.issue.entityIds[0]; - var action = actionExtract(entityId); - context.perform( - action, - t('operations.extract.annotation', { n: 1 }) - ); - // re-enter mode to trigger updates - context.enter(modeSelect(context, [action.getExtractedNodeID()])); - }; - } - - return [ - new validationIssueFix({ - icon: 'iD-operation-extract', - title: t.html('issues.fix.extract_point.title'), - onClick: extractOnClick - }) - ]; - } + dynamicFixes: dynamicExtractFixes }); } return null; } + + function otherMismatchIssue(entity, graph) { + // ignore boring features + if (!entity.hasInterestingTags()) return null; + + if (entity.type !== 'node' && entity.type !== 'way') return null; + + var sourceGeom = entity.geometry(graph); + + var targetGeoms = entity.type === 'way' ? ['point', 'vertex'] : ['line', 'area']; + + if (sourceGeom === 'area') targetGeoms.unshift('line'); + + var targetGeom = targetGeoms.find(nodeGeom => { + var asSource = presetManager.matchTags(entity.tags, sourceGeom); + var asTarget = presetManager.matchTags(entity.tags, nodeGeom); + if (!asSource || !asTarget || + asSource === asTarget || + // sometimes there are two presets with the same tags for different geometries + deepEqual(asSource.tags, asTarget.tags)) return false; + + if (asTarget.isFallback()) return false; + + var primaryKey = Object.keys(asTarget.tags)[0]; + + // special case: buildings-as-points are discouraged by iD, but common in OSM, so ignore them + if (primaryKey === 'building') return false; + + if (asTarget.tags[primaryKey] === '*') return false; + + return asSource.isFallback() || asSource.tags[primaryKey] === '*'; + }); + + if (!targetGeom) return null; + + var subtype = targetGeom + '_as_' + sourceGeom; + + if (targetGeom === 'vertex') targetGeom = 'point'; + if (sourceGeom === 'vertex') sourceGeom = 'point'; + + var referenceId = targetGeom + '_as_' + sourceGeom; + + var dynamicFixes = targetGeom === 'point' ? dynamicExtractFixes : null; + + return new validationIssue({ + type: type, + subtype: subtype, + severity: 'warning', + message: function(context) { + var entity = context.hasEntity(this.entityIds[0]); + return entity ? t.html('issues.' + referenceId + '.message', { + feature: utilDisplayLabel(entity, targetGeom) + }) : ''; + }, + reference: function showReference(selection) { + selection.selectAll('.issue-reference') + .data([0]) + .enter() + .append('div') + .attr('class', 'issue-reference') + .html(t.html('issues.mismatched_geometry.reference')); + }, + entityIds: [entity.id], + dynamicFixes: dynamicFixes + }); + } + + function dynamicExtractFixes(context) { + + var entityId = this.entityIds[0]; + + var extractOnClick = null; + if (!context.hasHiddenConnections(entityId)) { + + extractOnClick = function(context) { + var entityId = this.issue.entityIds[0]; + var action = actionExtract(entityId); + context.perform( + action, + t('operations.extract.annotation', { n: 1 }) + ); + // re-enter mode to trigger updates + context.enter(modeSelect(context, [action.getExtractedNodeID()])); + }; + } + + return [ + new validationIssueFix({ + icon: 'iD-operation-extract', + title: t.html('issues.fix.extract_point.title'), + onClick: extractOnClick + }) + ]; + } + function unclosedMultipolygonPartIssues(entity, graph) { if (entity.type !== 'relation' || !entity.isMultipolygon() || entity.isDegenerate() || // cannot determine issues for incompletely-downloaded relations - !entity.isComplete(graph)) return null; + !entity.isComplete(graph)) return []; var sequences = osmJoinWays(entity.members, graph); @@ -285,12 +355,16 @@ export function validationMismatchedGeometry() { } var validation = function checkMismatchedGeometry(entity, graph) { - var issues = [ - vertexTaggedAsPointIssue(entity, graph), - lineTaggedAsAreaIssue(entity) - ]; - issues = issues.concat(unclosedMultipolygonPartIssues(entity, graph)); - return issues.filter(Boolean); + var vertexPoint = vertexPointIssue(entity, graph); + if (vertexPoint) return [vertexPoint]; + + var lineAsArea = lineTaggedAsAreaIssue(entity); + if (lineAsArea) return [lineAsArea]; + + var mismatch = otherMismatchIssue(entity, graph); + if (mismatch) return [mismatch]; + + return unclosedMultipolygonPartIssues(entity, graph); }; validation.type = type; From c43fac8d640808baccca059746fd1188cd1c936f Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 7 Dec 2020 09:46:37 -0500 Subject: [PATCH 15/17] Ignore nodes on address lines for the mismatched geometry validation --- modules/validations/mismatched_geometry.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/validations/mismatched_geometry.js b/modules/validations/mismatched_geometry.js index 5f7c5a5f7..554c1e438 100644 --- a/modules/validations/mismatched_geometry.js +++ b/modules/validations/mismatched_geometry.js @@ -211,6 +211,9 @@ export function validationMismatchedGeometry() { if (entity.type !== 'node' && entity.type !== 'way') return null; + // address lines are special so just ignore them + if (entity.type === 'node' && entity.isOnAddressLine(graph)) return null; + var sourceGeom = entity.geometry(graph); var targetGeoms = entity.type === 'way' ? ['point', 'vertex'] : ['line', 'area']; From 2dd6bce851dc016c9669445cbda71a5e98135dae Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 7 Dec 2020 09:47:18 -0500 Subject: [PATCH 16/17] Add quick fix to convert areas to lines for mismatched geometry validation --- data/core.yaml | 3 ++ dist/locales/en.json | 4 ++ modules/validations/mismatched_geometry.js | 47 ++++++++++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 5eb5a0349..cf60722c6 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1847,6 +1847,9 @@ en: title: Continue drawing from start continue_from_end: title: Continue drawing from end + convert_to_line: + title: Convert this to a line + annotation: Converted an area to a line. delete_feature: title: Delete this feature extract_point: diff --git a/dist/locales/en.json b/dist/locales/en.json index 771b017c1..a8d02f7f9 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -2340,6 +2340,10 @@ "continue_from_end": { "title": "Continue drawing from end" }, + "convert_to_line": { + "title": "Convert this to a line", + "annotation": "Converted an area to a line." + }, "delete_feature": { "title": "Delete this feature" }, diff --git a/modules/validations/mismatched_geometry.js b/modules/validations/mismatched_geometry.js index 554c1e438..fe449a2e9 100644 --- a/modules/validations/mismatched_geometry.js +++ b/modules/validations/mismatched_geometry.js @@ -5,7 +5,7 @@ import { actionMergeNodes } from '../actions/merge_nodes'; import { actionExtract } from '../actions/extract'; import { modeSelect } from '../modes/select'; import { osmJoinWays } from '../osm/multipolygon'; -import { osmNodeGeometriesForTags } from '../osm/tags'; +import { osmNodeGeometriesForTags, osmTagSuggestingArea } from '../osm/tags'; import { presetManager } from '../presets'; import { geoHasSelfIntersections, geoSphericalDistance } from '../geo'; import { t } from '../core/localizer'; @@ -197,7 +197,7 @@ export function validationMismatchedGeometry() { .html(t.html('issues.point_as_vertex.reference')); }, entityIds: [entity.id], - dynamicFixes: dynamicExtractFixes + dynamicFixes: extractPointDynamicFixes }); } @@ -249,7 +249,13 @@ export function validationMismatchedGeometry() { var referenceId = targetGeom + '_as_' + sourceGeom; - var dynamicFixes = targetGeom === 'point' ? dynamicExtractFixes : null; + var dynamicFixes; + if (targetGeom === 'point') { + dynamicFixes = extractPointDynamicFixes; + + } else if (sourceGeom === 'area' && targetGeom === 'line') { + dynamicFixes = lintToAreaDynamicFixes; + } return new validationIssue({ type: type, @@ -274,7 +280,40 @@ export function validationMismatchedGeometry() { }); } - function dynamicExtractFixes(context) { + function lintToAreaDynamicFixes(context) { + + var convertOnClick; + + var entityId = this.entityIds[0]; + var entity = context.entity(entityId); + var tags = Object.assign({}, entity.tags); // shallow copy + delete tags.area; + if (!osmTagSuggestingArea(tags)) { + // if removing the area tag would make this a line, offer that as a quick fix + convertOnClick = function(context) { + var entityId = this.issue.entityIds[0]; + var entity = context.entity(entityId); + var tags = Object.assign({}, entity.tags); // shallow copy + if (tags.area) { + delete tags.area; + } + context.perform( + actionChangeTags(entityId, tags), + t('issues.fix.convert_to_line.annotation') + ); + }; + } + + return [ + new validationIssueFix({ + icon: 'iD-icon-line', + title: t.html('issues.fix.convert_to_line.title'), + onClick: convertOnClick + }) + ]; + } + + function extractPointDynamicFixes(context) { var entityId = this.entityIds[0]; From 32dcfcc08e5dd8b4a7ce0d81e9065a313c877a5b Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 7 Dec 2020 10:55:43 -0500 Subject: [PATCH 17/17] Fix spelling --- modules/validations/mismatched_geometry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/validations/mismatched_geometry.js b/modules/validations/mismatched_geometry.js index fe449a2e9..e1f0e0179 100644 --- a/modules/validations/mismatched_geometry.js +++ b/modules/validations/mismatched_geometry.js @@ -254,7 +254,7 @@ export function validationMismatchedGeometry() { dynamicFixes = extractPointDynamicFixes; } else if (sourceGeom === 'area' && targetGeom === 'line') { - dynamicFixes = lintToAreaDynamicFixes; + dynamicFixes = lineToAreaDynamicFixes; } return new validationIssue({ @@ -280,7 +280,7 @@ export function validationMismatchedGeometry() { }); } - function lintToAreaDynamicFixes(context) { + function lineToAreaDynamicFixes(context) { var convertOnClick;