From 38e4900355025b031422c0d1ef35f066f02b87fe Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 21 Dec 2016 16:44:40 -0500 Subject: [PATCH] Allow rotate of multiple selected objects (closes #1719) --- modules/modes/rotate.js | 134 +++++++++++++++++++---------------- modules/operations/rotate.js | 44 ++++++------ 2 files changed, 97 insertions(+), 81 deletions(-) diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index b6da3e84f..00e0e70f3 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -1,13 +1,8 @@ import * as d3 from 'd3'; -import _ from 'lodash'; import { d3keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; -import { - actionNoop, - actionRotate -} from '../actions/index'; - +import { actionRotate } from '../actions/index'; import { behaviorEdit } from '../behavior/index'; import { @@ -24,8 +19,15 @@ import { operationReflectShort } from '../operations/index'; +import { + polygonHull as d3polygonHull, + polygonCentroid as d3polygonCentroid +} from 'd3'; -export function modeRotate(context, wayId) { +import { utilGetAllNodes } from '../util'; + + +export function modeRotate(context, entityIDs) { var mode = { id: 'rotate', button: 'browse' @@ -34,33 +36,82 @@ export function modeRotate(context, wayId) { var keybinding = d3keybinding('rotate'), behaviors = [ behaviorEdit(context), - operationCircularize([wayId], context).behavior, - operationDelete([wayId], context).behavior, - operationMove([wayId], context).behavior, - operationOrthogonalize([wayId], context).behavior, - operationReflectLong([wayId], context).behavior, - operationReflectShort([wayId], context).behavior + operationCircularize(entityIDs, context).behavior, + operationDelete(entityIDs, context).behavior, + operationMove(entityIDs, context).behavior, + operationOrthogonalize(entityIDs, context).behavior, + operationReflectLong(entityIDs, context).behavior, + operationReflectShort(entityIDs, context).behavior ], + annotation = entityIDs.length === 1 ? + t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) : + t('operations.move.annotation.multiple'), prevGraph, prevAngle, + prevTransform, pivot; + function doRotate() { + var fn; + if (context.graph() !== prevGraph) { + fn = context.perform; + } else { + fn = context.replace; + } + + // projection changed, recalculate pivot + var projection = context.projection; + var currTransform = projection.transform(); + if (!prevTransform || + currTransform.k !== prevTransform.k || + currTransform.x !== prevTransform.x || + currTransform.y !== prevTransform.y) { + + var nodes = utilGetAllNodes(entityIDs, context.graph()), + points = nodes.map(function(n) { return projection(n.loc); }); + + pivot = d3polygonCentroid(d3polygonHull(points)); + prevAngle = undefined; + } + + + var currMouse = context.mouse(), + currAngle = Math.atan2(currMouse[1] - pivot[1], currMouse[0] - pivot[0]); + + if (typeof prevAngle === 'undefined') prevAngle = currAngle; + var delta = currAngle - prevAngle; + + fn(actionRotate(entityIDs, pivot, delta, projection), annotation); + + prevTransform = currTransform; + prevAngle = currAngle; + prevGraph = context.graph(); + } + + + function finish() { + d3.event.stopPropagation(); + context.enter(modeSelect(context, entityIDs).suppressMenu(true)); + } + + + function cancel() { + context.pop(); + context.enter(modeSelect(context, entityIDs).suppressMenu(true)); + } + + + function undone() { + context.enter(modeBrowse(context)); + } + + mode.enter = function() { - var way = context.graph().entity(wayId), - nodes = _.uniq(context.graph().childNodes(way)), - points = nodes.map(function(n) { return context.projection(n.loc); }); - - pivot = d3.polygonCentroid(points); - behaviors.forEach(function(behavior) { context.install(behavior); }); - var annotation = t('operations.rotate.annotation.' + context.geometry(wayId)); - - context.perform(actionNoop(), annotation); - context.surface() .on('mousemove.rotate', doRotate) .on('click.rotate', finish); @@ -74,43 +125,6 @@ export function modeRotate(context, wayId) { d3.select(document) .call(keybinding); - - - function doRotate() { - var fn; - if (prevGraph !== context.graph()) { - fn = context.perform; - } else { - fn = context.replace; - } - - var currMouse = context.mouse(), - currAngle = Math.atan2(currMouse[1] - pivot[1], currMouse[0] - pivot[0]); - - if (typeof prevAngle === 'undefined') prevAngle = currAngle; - var delta = currAngle - prevAngle; - - fn(actionRotate(wayId, pivot, delta, context.projection), annotation); - prevAngle = currAngle; - prevGraph = context.graph(); - } - - - function finish() { - d3.event.stopPropagation(); - context.enter(modeSelect(context, [wayId]).suppressMenu(true)); - } - - - function cancel() { - context.pop(); - context.enter(modeSelect(context, [wayId]).suppressMenu(true)); - } - - - function undone() { - context.enter(modeBrowse(context)); - } }; diff --git a/modules/operations/rotate.js b/modules/operations/rotate.js index a24c0e66e..115e8510e 100644 --- a/modules/operations/rotate.js +++ b/modules/operations/rotate.js @@ -1,39 +1,41 @@ +import _ from 'lodash'; import { t } from '../util/locale'; -import { modeRotate } from '../modes/index'; import { behaviorOperation } from '../behavior/index'; +import { geoExtent } from '../geo/index'; +import { modeRotate } from '../modes/index'; export function operationRotate(selectedIDs, context) { - var entityId = selectedIDs[0], - entity = context.entity(entityId), - extent = entity.extent(context.graph()), - geometry = context.geometry(entityId); + var extent = selectedIDs.reduce(function(extent, id) { + return extent.extend(context.entity(id).extent(context.graph())); + }, geoExtent()); var operation = function() { - context.enter(modeRotate(context, entityId)); + context.enter(modeRotate(context, selectedIDs)); }; operation.available = function() { - if (selectedIDs.length !== 1 || entity.type !== 'way') - return false; - if (geometry === 'area') - return true; - if (entity.isClosed() && - context.graph().parentRelations(entity).some(function(r) { return r.isMultipolygon(); })) - return true; - return false; + return selectedIDs.length > 1 || + context.entity(selectedIDs[0]).type !== 'node'; }; operation.disabled = function() { - if (extent.percentContainedIn(context.extent()) < 0.8) { - return 'too_large'; - } else if (context.hasHiddenConnections(entityId)) { - return 'connected_to_hidden'; - } else { - return false; + var reason; + if (extent.area() && extent.percentContainedIn(context.extent()) < 0.8) { + reason = 'too_large'; + } else if (_.some(selectedIDs, context.hasHiddenConnections)) { + reason = 'connected_to_hidden'; + } else if (_.some(selectedIDs, incompleteRelation)) { + reason = 'incomplete_relation'; + } + return reason; + + function incompleteRelation(id) { + var entity = context.entity(id); + return entity.type === 'relation' && !entity.isComplete(graph); } }; @@ -42,7 +44,7 @@ export function operationRotate(selectedIDs, context) { var disable = operation.disabled(); return disable ? t('operations.rotate.' + disable) : - t('operations.rotate.description'); + t('operations.rotate.description.' + (selectedIDs.length === 1 ? 'single' : 'multiple')); };