From 45decdb54c7e19cbb01e5bd0da4bbe1ee7f09c08 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 21 Sep 2020 14:02:41 -0400 Subject: [PATCH] Enable scaling the selection via hotkeys --- data/core.yaml | 24 +++++++++++ data/shortcuts.json | 12 ++++++ dist/locales/en.json | 34 +++++++++++++++ modules/actions/index.js | 1 + modules/actions/scale.js | 24 +++++++++++ modules/modes/select.js | 85 +++++++++++++++++++++++++++++++++++++- modules/ui/zoom.js | 4 ++ modules/util/keybinding.js | 11 +++-- 8 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 modules/actions/scale.js diff --git a/data/core.yaml b/data/core.yaml index 3a7b49899..7c8cd8724 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -400,6 +400,28 @@ en: line: Reversed a line. lines: Reversed multiple lines. features: Reversed multiple features. + scale: + annotation: + down: + feature: + one: Scaled down a feature. + other: "Scaled down {n} features." + up: + feature: + one: Scaled up a feature. + other: "Scaled up {n} features." + too_small: + single: This feature can't be scaled down because it would become too small. + multiple: These features can't be scaled down because they would become too small. + too_large: + single: This feature can't be scaled because not enough of it is currently visible. + multiple: These features can't be scaled because not enough of them are currently visible. + connected_to_hidden: + single: This feature can't be scaled because it is connected to a hidden feature. + multiple: These features can't be scaled because some are connected to hidden features. + not_downloaded: + single: This feature can't be scaled because parts of it have not yet been downloaded. + multiple: These features can't be scaled because parts of them have not yet been downloaded. split: title: Split description: @@ -2172,6 +2194,8 @@ en: move: "Move selected features" nudge: "Nudge selected features" nudge_more: "Nudge selected features by a lot" + scale: "Scale selected features" + scale_more: "Scale selected features by a lot" rotate: "Rotate selected features" orthogonalize: "Square corners of a line or area" straighten: "Straighten a line or points" diff --git a/data/shortcuts.json b/data/shortcuts.json index 6b910899b..2997ed427 100644 --- a/data/shortcuts.json +++ b/data/shortcuts.json @@ -281,6 +281,18 @@ "text": "shortcuts.editing.operations.nudge_more", "separator": "," }, + { + "modifiers": ["⇧"], + "shortcuts": ["+", "-"], + "text": "shortcuts.editing.operations.scale", + "separator": "," + }, + { + "modifiers": ["⌥", "⇧"], + "shortcuts": ["+", "-"], + "text": "shortcuts.editing.operations.scale_more", + "separator": "," + }, { "shortcuts": ["operations.rotate.key"], "text": "shortcuts.editing.operations.rotate" diff --git a/dist/locales/en.json b/dist/locales/en.json index 4f2c141ca..6ffc48503 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -527,6 +527,38 @@ "features": "Reversed multiple features." } }, + "scale": { + "annotation": { + "down": { + "feature": { + "one": "Scaled down a feature.", + "other": "Scaled down {n} features." + } + }, + "up": { + "feature": { + "one": "Scaled up a feature.", + "other": "Scaled up {n} features." + } + } + }, + "too_small": { + "single": "This feature can't be scaled down because it would become too small.", + "multiple": "These features can't be scaled down because they would become too small." + }, + "too_large": { + "single": "This feature can't be scaled because not enough of it is currently visible.", + "multiple": "These features can't be scaled because not enough of them are currently visible." + }, + "connected_to_hidden": { + "single": "This feature can't be scaled because it is connected to a hidden feature.", + "multiple": "These features can't be scaled because some are connected to hidden features." + }, + "not_downloaded": { + "single": "This feature can't be scaled because parts of it have not yet been downloaded.", + "multiple": "These features can't be scaled because parts of them have not yet been downloaded." + } + }, "split": { "title": "Split", "description": { @@ -2674,6 +2706,8 @@ "move": "Move selected features", "nudge": "Nudge selected features", "nudge_more": "Nudge selected features by a lot", + "scale": "Scale selected features", + "scale_more": "Scale selected features by a lot", "rotate": "Rotate selected features", "orthogonalize": "Square corners of a line or area", "straighten": "Straighten a line or points", diff --git a/modules/actions/index.js b/modules/actions/index.js index dfdd1c7e6..ca8fe5f3d 100644 --- a/modules/actions/index.js +++ b/modules/actions/index.js @@ -30,6 +30,7 @@ export { actionRestrictTurn } from './restrict_turn'; export { actionReverse } from './reverse'; export { actionRevert } from './revert'; export { actionRotate } from './rotate'; +export { actionScale } from './scale'; export { actionSplit } from './split'; export { actionStraightenNodes } from './straighten_nodes'; export { actionStraightenWay } from './straighten_way'; diff --git a/modules/actions/scale.js b/modules/actions/scale.js new file mode 100644 index 000000000..c95f0224f --- /dev/null +++ b/modules/actions/scale.js @@ -0,0 +1,24 @@ +import { utilGetAllNodes } from '../util'; + +export function actionScale(ids, pivotLoc, scaleFactor, projection) { + return function(graph) { + return graph.update(function(graph) { + let point, radial; + + utilGetAllNodes(ids, graph).forEach(function(node) { + + point = projection(node.loc); + radial = [ + point[0] - pivotLoc[0], + point[1] - pivotLoc[1] + ]; + point = [ + pivotLoc[0] + (scaleFactor * radial[0]), + pivotLoc[1] + (scaleFactor * radial[1]) + ]; + + graph = graph.replace(node.move(projection.invert(point))); + }); + }); + }; +} diff --git a/modules/modes/select.js b/modules/modes/select.js index ac9e35f29..4bbebe361 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -5,6 +5,7 @@ import { t } from '../core/localizer'; import { actionAddMidpoint } from '../actions/add_midpoint'; import { actionDeleteRelation } from '../actions/delete_relation'; import { actionMove } from '../actions/move'; +import { actionScale } from '../actions/scale'; import { behaviorBreathe } from '../behavior/breathe'; import { behaviorHover } from '../behavior/hover'; @@ -14,7 +15,7 @@ import { behaviorSelect } from '../behavior/select'; import { operationMove } from '../operations/move'; -import { geoExtent, geoChooseEdge } from '../geo'; +import { geoExtent, geoChooseEdge, geoMetersToLat, geoMetersToLon } from '../geo'; import { modeBrowse } from './browse'; import { modeDragNode } from './drag_node'; import { modeDragNote } from './drag_note'; @@ -23,7 +24,7 @@ import * as Operations from '../operations/index'; import { uiCmd } from '../ui/cmd'; import { utilArrayIntersection, utilDeepMemberSelector, utilEntityOrDeepMemberSelector, - utilEntitySelector, utilKeybinding + utilEntitySelector, utilKeybinding, utilTotalExtent, utilGetAllNodes } from '../util'; @@ -243,6 +244,10 @@ export function modeSelect(context, selectedIDs) { .on(uiCmd('⇧⌥↑'), nudgeSelection([0, -100])) .on(uiCmd('⇧⌥→'), nudgeSelection([100, 0])) .on(uiCmd('⇧⌥↓'), nudgeSelection([0, 100])) + .on(utilKeybinding.plusKeys.map((key) => uiCmd('⇧' + key)), scaleSelection(1.05)) + .on(utilKeybinding.plusKeys.map((key) => uiCmd('⇧⌥' + key)), scaleSelection(Math.pow(1.05, 5))) + .on(utilKeybinding.minusKeys.map((key) => uiCmd('⇧' + key)), scaleSelection(1/1.05)) + .on(utilKeybinding.minusKeys.map((key) => uiCmd('⇧⌥' + key)), scaleSelection(1/Math.pow(1.05, 5))) .on(['\\', 'pause'], nextParent) .on('⎋', esc, true); @@ -303,6 +308,82 @@ export function modeSelect(context, selectedIDs) { .text(moveOp.tooltip)(); } else { context.perform(actionMove(selectedIDs, delta, context.projection), moveOp.annotation()); + context.validator().validate(); + } + }; + } + + function scaleSelection(factor) { + return function() { + // prevent scaling during low zoom selection + if (!context.map().withinEditableZoom()) return; + + let nodes = utilGetAllNodes(selectedIDs, context.graph()); + + let isUp = factor > 1; + + // can only scale if multiple nodes are selected + if (nodes.length <= 1) return; + + let extent = utilTotalExtent(selectedIDs, context.graph()); + + // These disabled checks would normally be handled by an operation + // object, but we don't want an actual scale operation at this point. + function scalingDisabled() { + + if (tooSmall()) { + return 'too_small'; + } else if (extent.percentContainedIn(context.map().extent()) < 0.8) { + return 'too_large'; + } else if (someMissing() || selectedIDs.some(incompleteRelation)) { + return 'not_downloaded'; + } else if (selectedIDs.some(context.hasHiddenConnections)) { + return 'connected_to_hidden'; + } + + return false; + + function tooSmall() { + if (isUp) return false; + let dLon = Math.abs(extent[1][0] - extent[0][0]); + let dLat = Math.abs(extent[1][1] - extent[0][1]); + return dLon < geoMetersToLon(1, extent[1][1]) && + dLat < geoMetersToLat(1); + } + + function someMissing() { + if (context.inIntro()) return false; + let osm = context.connection(); + if (osm) { + let missing = nodes.filter(function(n) { return !osm.isDataLoaded(n.loc); }); + if (missing.length) { + missing.forEach(function(loc) { context.loadTileAtLoc(loc); }); + return true; + } + } + return false; + } + + function incompleteRelation(id) { + let entity = context.entity(id); + return entity.type === 'relation' && !entity.isComplete(context.graph()); + } + } + + const disabled = scalingDisabled(); + + if (disabled) { + let multi = (selectedIDs.length === 1 ? 'single' : 'multiple'); + context.ui().flash + .duration(4000) + .iconName('#iD-icon-no') + .iconClass('operation disabled') + .text(t('operations.scale.' + disabled + '.' + multi))(); + } else { + const pivot = context.projection(extent.center()); + const annotation = t('operations.scale.annotation.' + (isUp ? 'up' : 'down') + '.feature', { n: selectedIDs.length }); + context.perform(actionScale(selectedIDs, pivot, factor, context.projection), annotation); + context.validator().validate(); } }; } diff --git a/modules/ui/zoom.js b/modules/ui/zoom.js index 30d58bc4e..e60568d52 100644 --- a/modules/ui/zoom.js +++ b/modules/ui/zoom.js @@ -35,21 +35,25 @@ export function uiZoom(context) { }]; function zoomIn() { + if (d3_event.shiftKey) return; d3_event.preventDefault(); context.map().zoomIn(); } function zoomOut() { + if (d3_event.shiftKey) return; d3_event.preventDefault(); context.map().zoomOut(); } function zoomInFurther() { + if (d3_event.shiftKey) return; d3_event.preventDefault(); context.map().zoomInFurther(); } function zoomOutFurther() { + if (d3_event.shiftKey) return; d3_event.preventDefault(); context.map().zoomOutFurther(); } diff --git a/modules/util/keybinding.js b/modules/util/keybinding.js index fc5482fa2..34a112a33 100644 --- a/modules/util/keybinding.js +++ b/modules/util/keybinding.js @@ -28,17 +28,22 @@ export function utilKeybinding(namespace) { if (matches(binding, true)) { binding.callback(); didMatch = true; + + // match a max of one binding per event + break; } } - // then unshifted keybindings if (didMatch) return; + + // then unshifted keybindings for (i = 0; i < bindings.length; i++) { binding = bindings[i]; if (binding.event.modifiers.shiftKey) continue; // shift if (!!binding.capture !== isCapturing) continue; if (matches(binding, false)) { binding.callback(); + break; } } @@ -214,8 +219,8 @@ utilKeybinding.modifierProperties = { 91: 'metaKey' }; -utilKeybinding.plusKeys = ['plus', 'ffplus', '=', 'ffequals']; -utilKeybinding.minusKeys = ['_', '-', 'ffminus', 'dash']; +utilKeybinding.plusKeys = ['plus', 'ffplus', '=', 'ffequals', '≠', '±']; +utilKeybinding.minusKeys = ['_', '-', 'ffminus', 'dash', '–', '—']; utilKeybinding.keys = { // Backspace key, on Mac: ⌫ (Backspace)