From 42bada72def6e7ec64302e283eb36e580c2212d0 Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Fri, 3 Jan 2025 20:16:28 +1300 Subject: [PATCH] automatically download adjacent ways when splitting a line --- modules/behavior/operation.js | 6 ++++ modules/operations/split.js | 56 ++++++++++++++++++++++++++++++++++- modules/ui/edit_menu.js | 15 ++++++++-- modules/ui/modal_async.js | 55 ++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 modules/ui/modal_async.js diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index b10fcd3cb5..f39d66e7e1 100644 --- a/modules/behavior/operation.js +++ b/modules/behavior/operation.js @@ -15,6 +15,12 @@ export function behaviorOperation(context) { var disabled = _operation.disabled(); if (disabled) { + const interrupt = _operation.interrupts?.[disabled]; + if (interrupt) { + interrupt(); + return; + } + context.ui().flash .duration(4000) .iconName('#iD-operation-' + _operation.id) diff --git a/modules/operations/split.js b/modules/operations/split.js index 771511428c..9901c06ad3 100644 --- a/modules/operations/split.js +++ b/modules/operations/split.js @@ -2,6 +2,8 @@ import { t } from '../core/localizer'; import { actionSplit } from '../actions/split'; import { behaviorOperation } from '../behavior/operation'; import { modeSelect } from '../modes/select'; +import { uiAsyncModal } from '../ui/modal_async'; +import { uiLoading } from '../ui'; export function operationSplit(context, selectedIDs) { @@ -65,10 +67,62 @@ export function operationSplit(context, selectedIDs) { return false; }; + operation.interrupts = { + async parent_incomplete() { + const graph = context.graph(); + + const confirmed = await uiAsyncModal(context).open( + t.append('operations.split.title'), + t.append('operations.split.parent_incomplete'), + ); + + if (!confirmed) return; // user cancelled the operation + + + const loading = uiLoading(context).blocking(true); + context.container().call(loading); + + /** @type {Set} */ + const requiredWayIds = new Set(); + + // find vertex->ways->relations, then find the adjacent way for + // each relation. + for (const nodeId of _vertexIds) { + const ways = _action.waysForNode(nodeId, graph); + for (const way of ways) { + const relations = graph.parentRelations(way); + for (const relation of relations) { + const indexOfWay = relation.members.findIndex(m => m.id === way.id); + + const prevWay = relation.members[indexOfWay - 1]?.id; + const nextWay = relation.members[indexOfWay + 1]?.id; + + if (prevWay) requiredWayIds.add(prevWay); + if (nextWay) requiredWayIds.add(nextWay); + } + } + } + + // only download the ways that aren't downloaded yet + const promises = [...requiredWayIds] + .filter(wayId => !context.graph().hasEntity(wayId)) + .map((wayId) => new Promise(resolve => { + context.loadEntity(wayId, resolve); + })); + + await Promise.all(promises); + + loading.close(); + + // now we can resume the interrupted operation + operation(); + } + }; + operation.tooltip = function() { var disable = operation.disabled(); - return disable ? + return disable && !operation.interrupts?.[disable] ? t.append('operations.split.' + disable) : t.append('operations.split.description.' + _geometry + '.' + _waysAmount + '.' + _nodesAmount + '_node'); }; diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index c91827b7c6..d03f65b5c1 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -129,7 +129,12 @@ export function uiEditMenu(context) { // update buttonsEnter .merge(buttons) - .classed('disabled', function(d) { return d.disabled(); }); + .classed('disabled', d => { + // if a disabled operation is interruptable, then don't + // show it as disabled. + const reason = d.disabled(); + return reason && !d.interrupts?.[reason]; + }); updatePosition(); @@ -157,8 +162,12 @@ export function uiEditMenu(context) { utilHighlightEntities(operation.relatedEntityIds(), false, context); } - if (operation.disabled()) { - if (lastPointerUpType === 'touch' || + const disabled = operation.disabled(); + if (disabled) { + const interrupt = operation.interrupts?.[disabled]; + if (interrupt) { + interrupt(); + } else if (lastPointerUpType === 'touch' || lastPointerUpType === 'pen') { // there are no tooltips for touch interactions so flash feedback instead context.ui().flash diff --git a/modules/ui/modal_async.js b/modules/ui/modal_async.js new file mode 100644 index 0000000000..a64296a92d --- /dev/null +++ b/modules/ui/modal_async.js @@ -0,0 +1,55 @@ +import { t } from '../core/localizer'; +import { uiConfirm } from './confirm'; + +/** @param {iD.Context} context */ +export function uiAsyncModal(context) { + let _modal; + + /** + * Open a model, and returns a promise. The promise + * resolves with `true` if the user clicked 'Okay', + * or `false` if they clicked 'Cancel' + * @returns {Promise} + */ + function open(title, subtitle) { + return new Promise((resolve) => { + context.container().call(selection => { + _modal = uiConfirm(selection).okButton(); + + _modal.select('.modal-section.header') + .append('h3') + .call(title); + + // insert the modal body + const textSection = _modal.select('.modal-section.message-text'); + textSection.call(subtitle); + + // insert a cancel button + const buttonSection = _modal.select('.modal-section.buttons'); + + buttonSection + .insert('button', '.ok-button') + .attr('class', 'button cancel-button secondary-action') + .call(t.append('confirm.cancel')); + + + buttonSection.select('.cancel-button') + .on('click.cancel', () => { + _modal.remove(); + resolve(false); + }); + + buttonSection.select('.ok-button') + .on('click.save', () => resolve(true)); + }); + }); + } + + function close() { + _modal.remove(); + _modal = undefined; + } + + + return { open, close }; +}