Enable scaling the selection via hotkeys

This commit is contained in:
Quincy Morgan
2020-09-21 14:02:41 -04:00
parent 28ea082892
commit 45decdb54c
8 changed files with 190 additions and 5 deletions

View File

@@ -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"

View File

@@ -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"

34
dist/locales/en.json vendored
View File

@@ -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",

View File

@@ -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';

24
modules/actions/scale.js Normal file
View File

@@ -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)));
});
});
};
}

View File

@@ -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();
}
};
}

View File

@@ -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();
}

View File

@@ -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)