More cleanup of operations and post-paste behavior

* Support move, rotate, reflect, delete post paste on multiselection
* Improve text and error msgs for singular vs multi selections
* Move `disabled` checks from actions to operations
* Reproject center of rotation (closes #3667)
* Cleanup tests
This commit is contained in:
Bryan Housel
2016-12-21 23:58:13 -05:00
parent 38e4900355
commit 37534aed0e
19 changed files with 298 additions and 214 deletions
-9
View File
@@ -22,14 +22,5 @@ export function actionDeleteMultiple(ids) {
};
action.disabled = function(graph) {
for (var i = 0; i < ids.length; i++) {
var id = ids[i],
disabled = actions[graph.entity(id).type](id).disabled(graph);
if (disabled) return disabled;
}
};
return action;
}
-5
View File
@@ -31,10 +31,5 @@ export function actionDeleteNode(nodeId) {
};
action.disabled = function() {
return false;
};
return action;
}
-6
View File
@@ -39,11 +39,5 @@ export function actionDeleteRelation(relationId) {
};
action.disabled = function(graph) {
if (!graph.entity(relationId).isComplete(graph))
return 'incomplete_relation';
};
return action;
}
-15
View File
@@ -39,20 +39,5 @@ export function actionDeleteWay(wayId) {
};
action.disabled = function(graph) {
var disabled = false;
graph.parentRelations(graph.entity(wayId)).forEach(function(parent) {
var type = parent.tags.type,
role = parent.memberById(wayId).role || 'outer';
if (type === 'route' || type === 'boundary' || (type === 'multipolygon' && role === 'outer')) {
disabled = 'part_of_relation';
}
});
return disabled;
};
return action;
}
-11
View File
@@ -286,17 +286,6 @@ export function actionMove(moveIds, tryDelta, projection, cache) {
};
action.disabled = function(graph) {
function incompleteRelation(id) {
var entity = graph.entity(id);
return entity.type === 'relation' && !entity.isComplete(graph);
}
if (_.some(moveIds, incompleteRelation))
return 'incomplete_relation';
};
action.delta = function() {
return delta;
};
+10 -15
View File
@@ -1,4 +1,3 @@
import _ from 'lodash';
import {
polygonHull as d3polygonHull,
polygonCentroid as d3polygonCentroid
@@ -10,17 +9,18 @@ import {
geoRotate
} from '../geo';
import { utilGetAllNodes } from '../util';
/* Reflect the given area around its axis of symmetry */
export function actionReflect(wayId, projection) {
export function actionReflect(reflectIds, projection) {
var useLongAxis = true;
// http://gis.stackexchange.com/questions/22895/finding-minimum-area-rectangle-for-given-points
// http://gis.stackexchange.com/questions/3739/generalisation-strategies-for-building-outlines/3756#3756
function getSmallestSurroundingRectangle(graph, way) {
var nodes = _.uniq(graph.childNodes(way)),
points = nodes.map(function(n) { return projection(n.loc); }),
function getSmallestSurroundingRectangle(graph, nodes) {
var points = nodes.map(function(n) { return projection(n.loc); }),
hull = d3polygonHull(points),
centroid = d3polygonCentroid(hull),
minArea = Infinity,
@@ -52,14 +52,9 @@ export function actionReflect(wayId, projection) {
}
var action = function (graph) {
var targetWay = graph.entity(wayId);
if (!targetWay.isArea()) {
return graph;
}
var ssr = getSmallestSurroundingRectangle(graph, targetWay),
nodes = targetWay.nodes;
var action = function(graph) {
var nodes = utilGetAllNodes(reflectIds, graph),
ssr = getSmallestSurroundingRectangle(graph, nodes);
// Choose line pq = axis of symmetry.
// The shape's surrounding rectangle has 2 axes of symmetry.
@@ -85,8 +80,8 @@ export function actionReflect(wayId, projection) {
var dy = q[1] - p[1];
var a = (dx * dx - dy * dy) / (dx * dx + dy * dy);
var b = 2 * dx * dy / (dx * dx + dy * dy);
for (var i = 0; i < nodes.length - 1; i++) {
var node = graph.entity(nodes[i]);
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var c = projection(node.loc);
var c2 = [
a * (c[0] - p[0]) + b * (c[1] - p[1]) + p[0],
+4 -7
View File
@@ -1,15 +1,12 @@
import _ from 'lodash';
import { geoRotate } from '../geo';
import { utilGetAllNodes } from '../util';
export function actionRotate(rotateIds, pivot, angle, projection) {
export function actionRotate(wayId, pivot, angle, projection) {
var action = function(graph) {
return graph.update(function(graph) {
var way = graph.entity(wayId);
_.uniq(way.nodes).forEach(function(id) {
var node = graph.entity(id),
point = geoRotate([projection(node.loc)], angle, pivot)[0];
utilGetAllNodes(rotateIds, graph).forEach(function(node) {
var point = geoRotate([projection(node.loc)], angle, pivot)[0];
graph = graph.replace(node.move(projection.invert(point)));
});
});
+1 -1
View File
@@ -45,7 +45,7 @@ export function modeRotate(context, entityIDs) {
],
annotation = entityIDs.length === 1 ?
t('operations.rotate.annotation.' + context.geometry(entityIDs[0])) :
t('operations.move.annotation.multiple'),
t('operations.rotate.annotation.multiple'),
prevGraph,
prevAngle,
prevTransform,
+32 -4
View File
@@ -8,7 +8,9 @@ import { uiCmd } from '../ui/index';
export function operationDelete(selectedIDs, context) {
var action = actionDeleteMultiple(selectedIDs);
var multi = (selectedIDs.length === 1 ? 'single' : 'multiple'),
action = actionDeleteMultiple(selectedIDs);
var operation = function() {
var annotation,
@@ -67,16 +69,42 @@ export function operationDelete(selectedIDs, context) {
var reason;
if (_.some(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
} else if (_.some(selectedIDs, protectedMember)) {
reason = 'part_of_relation';
} else if (_.some(selectedIDs, incompleteRelation)) {
reason = 'incomplete_relation';
}
return action.disabled(context.graph()) || reason;
return reason;
function incompleteRelation(id) {
var entity = context.entity(id);
return entity.type === 'relation' && !entity.isComplete(context.graph());
}
function protectedMember(id) {
var entity = context.entity(id);
if (entity.type !== 'way') return false;
var parents = context.graph().parentRelations(entity);
for (var i = 0; i < parents.length; i++) {
var parent = parents[i],
type = parent.tags.type,
role = parent.memberById(id).role || 'outer';
if (type === 'route' || type === 'boundary' || (type === 'multipolygon' && role === 'outer')) {
return true;
}
}
return false;
}
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.delete.' + disable) :
t('operations.delete.description');
t('operations.delete.' + disable + '.' + multi) :
t('operations.delete.description' + '.' + multi);
};
+11 -5
View File
@@ -1,13 +1,13 @@
import _ from 'lodash';
import { t } from '../util/locale';
import { actionMove } from '../actions/index';
import { behaviorOperation } from '../behavior/index';
import { geoExtent } from '../geo/index';
import { modeMove } from '../modes/index';
export function operationMove(selectedIDs, context) {
var extent = selectedIDs.reduce(function(extent, id) {
var multi = (selectedIDs.length === 1 ? 'single' : 'multiple'),
extent = selectedIDs.reduce(function(extent, id) {
return extent.extend(context.entity(id).extent(context.graph()));
}, geoExtent());
@@ -29,17 +29,23 @@ export function operationMove(selectedIDs, context) {
reason = 'too_large';
} else if (_.some(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
} else if (_.some(selectedIDs, incompleteRelation)) {
reason = 'incomplete_relation';
}
return reason;
return actionMove(selectedIDs).disabled(context.graph()) || reason;
function incompleteRelation(id) {
var entity = context.entity(id);
return entity.type === 'relation' && !entity.isComplete(context.graph());
}
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.move.' + disable) :
t('operations.move.description');
t('operations.move.' + disable + '.' + multi) :
t('operations.move.description.' + multi);
};
+26 -15
View File
@@ -1,6 +1,8 @@
import _ from 'lodash';
import { t } from '../util/locale';
import { actionReflect } from '../actions/index';
import { behaviorOperation } from '../behavior/index';
import { geoExtent } from '../geo/index';
export function operationReflectShort(selectedIDs, context) {
@@ -15,30 +17,39 @@ export function operationReflectLong(selectedIDs, context) {
export function operationReflect(selectedIDs, context, axis) {
axis = axis || 'long';
var entityId = selectedIDs[0];
var entity = context.entity(entityId);
var extent = entity.extent(context.graph());
var action = actionReflect(entityId, context.projection)
.useLongAxis(Boolean(axis === 'long'));
var multi = (selectedIDs.length === 1 ? 'single' : 'multiple'),
extent = selectedIDs.reduce(function(extent, id) {
return extent.extend(context.entity(id).extent(context.graph()));
}, geoExtent());
var operation = function() {
context.perform(action, t('operations.reflect.annotation.' + axis));
var action = actionReflect(selectedIDs, context.projection)
.useLongAxis(Boolean(axis === 'long'));
context.perform(action, t('operations.reflect.annotation.' + axis + '.' + multi));
};
operation.available = function() {
return selectedIDs.length === 1 && context.geometry(entityId) === 'area';
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(context.graph());
}
};
@@ -46,8 +57,8 @@ export function operationReflect(selectedIDs, context, axis) {
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.reflect.' + disable) :
t('operations.reflect.description.' + axis);
t('operations.reflect.' + disable + '.' + multi) :
t('operations.reflect.description.' + axis + '.' + multi);
};
+5 -4
View File
@@ -6,7 +6,8 @@ import { modeRotate } from '../modes/index';
export function operationRotate(selectedIDs, context) {
var extent = selectedIDs.reduce(function(extent, id) {
var multi = (selectedIDs.length === 1 ? 'single' : 'multiple'),
extent = selectedIDs.reduce(function(extent, id) {
return extent.extend(context.entity(id).extent(context.graph()));
}, geoExtent());
@@ -35,7 +36,7 @@ export function operationRotate(selectedIDs, context) {
function incompleteRelation(id) {
var entity = context.entity(id);
return entity.type === 'relation' && !entity.isComplete(graph);
return entity.type === 'relation' && !entity.isComplete(context.graph());
}
};
@@ -43,8 +44,8 @@ export function operationRotate(selectedIDs, context) {
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.rotate.' + disable) :
t('operations.rotate.description.' + (selectedIDs.length === 1 ? 'single' : 'multiple'));
t('operations.rotate.' + disable + '.' + multi) :
t('operations.rotate.description.' + multi);
};