From 1884c7070eec3f994da1ddb36e7ce644771c8cb6 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 19 Dec 2016 16:55:11 -0500 Subject: [PATCH 01/14] Add a keybinding behavior for operations --- modules/behavior/index.js | 1 + modules/behavior/operation.js | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 modules/behavior/operation.js diff --git a/modules/behavior/index.js b/modules/behavior/index.js index da78fa23a..2dfb951bf 100644 --- a/modules/behavior/index.js +++ b/modules/behavior/index.js @@ -8,6 +8,7 @@ export { behaviorEdit } from './edit'; export { behaviorHash } from './hash'; export { behaviorHover } from './hover'; export { behaviorLasso } from './lasso'; +export { behaviorOperation } from './operation'; export { behaviorPaste } from './paste'; export { behaviorSelect } from './select'; export { behaviorTail } from './tail'; diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js new file mode 100644 index 000000000..fa57ddc41 --- /dev/null +++ b/modules/behavior/operation.js @@ -0,0 +1,40 @@ +import * as d3 from 'd3'; +import { d3keybinding } from '../lib/d3.keybinding.js'; + + +/* Creates a keybinding behavior for an operation */ +export function behaviorOperation(context) { + var which, keybinding; + + + var behavior = function () { + if (which) { + keybinding = d3keybinding('behavior.key.' + which.id); + keybinding.on(which.keys, function() { + d3.event.preventDefault(); + if (!(context.inIntro() || which.disabled())) { + which(); + } + }); + d3.select(document).call(keybinding); + } + return behavior; + }; + + + behavior.off = function() { + if (keybinding) { + d3.select(document).call(keybinding.off); + } + }; + + + behavior.which = function (_) { + if (!arguments.length) return which; + which = _; + return behavior; + }; + + + return behavior; +} From 35aae816b0db463c1d49c5ce826dd9ee2ca92bd2 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 19 Dec 2016 16:55:49 -0500 Subject: [PATCH 02/14] Expose Reflect operation behavior --- modules/operations/reflect.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/operations/reflect.js b/modules/operations/reflect.js index 3944fb4ef..b7e5e1808 100644 --- a/modules/operations/reflect.js +++ b/modules/operations/reflect.js @@ -1,5 +1,6 @@ import { t } from '../util/locale'; import { actionReflect } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; export function operationReflectShort(selectedIDs, context) { @@ -22,17 +23,15 @@ export function operationReflect(selectedIDs, context, axis) { var operation = function() { - context.perform( - action, - t('operations.reflect.annotation.' + axis) - ); + context.perform(action, t('operations.reflect.annotation.' + axis)); }; + operation.available = function() { - return selectedIDs.length === 1 && - context.geometry(entityId) === 'area'; + return selectedIDs.length === 1 && context.geometry(entityId) === 'area'; }; + operation.disabled = function() { if (extent.percentContainedIn(context.extent()) < 0.8) { return 'too_large'; @@ -43,6 +42,7 @@ export function operationReflect(selectedIDs, context, axis) { } }; + operation.tooltip = function() { var disable = operation.disabled(); return disable ? @@ -50,9 +50,12 @@ export function operationReflect(selectedIDs, context, axis) { t('operations.reflect.description.' + axis); }; + operation.id = 'reflect-' + axis; operation.keys = [t('operations.reflect.key.' + axis)]; operation.title = t('operations.reflect.title'); + operation.behavior = behaviorOperation(context).which(operation); + return operation; } From 068a40e6cc01f55d90cf5bf9258830b3644d6bb5 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 19 Dec 2016 16:56:32 -0500 Subject: [PATCH 03/14] Support Reflect behaviors in Move mode Also some refactor and added support diagonal nudging --- modules/modes/move.js | 100 ++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 33 deletions(-) diff --git a/modules/modes/move.js b/modules/modes/move.js index 6dbff9502..f174d14ae 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -1,10 +1,20 @@ import * as d3 from 'd3'; import { d3keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; -import { modeBrowse, modeSelect } from './index'; -import { actionMove, actionNoop } from '../actions/index'; + +import { actionMove } from '../actions/index'; import { behaviorEdit } from '../behavior/index'; +import { + modeBrowse, + modeSelect +} from './index'; + +import { + operationReflectLong, + operationReflectShort +} from '../operations/index'; + export function modeMove(context, entityIDs, baseGraph) { var mode = { @@ -13,25 +23,62 @@ export function modeMove(context, entityIDs, baseGraph) { }; var keybinding = d3keybinding('move'), - edit = behaviorEdit(context), + behaviors = [ + behaviorEdit(context), + operationReflectLong(entityIDs, context).behavior, + operationReflectShort(entityIDs, context).behavior, + ], annotation = entityIDs.length === 1 ? t('operations.move.annotation.' + context.geometry(entityIDs[0])) : t('operations.move.annotation.multiple'), + prevGraph, cache, origin, nudgeInterval; - function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1]]; } + function vecSub(a, b) { + return [a[0] - b[0], a[1] - b[1]]; + } function edge(point, size) { - var pad = [30, 100, 30, 100]; - if (point[0] > size[0] - pad[0]) return [-10, 0]; - else if (point[0] < pad[2]) return [10, 0]; - else if (point[1] > size[1] - pad[1]) return [0, -10]; - else if (point[1] < pad[3]) return [0, 10]; - return null; + var pad = [30, 100, 30, 100], + x = 0, + y = 0; + + if (point[0] > size[0] - pad[0]) + x = -10; + if (point[0] < pad[2]) + x = 10; + if (point[1] > size[1] - pad[1]) + y = -10; + if (point[1] < pad[3]) + y = 10; + + if (x || y) return [x, y]; + else return null; + } + + + function doMove(nudge) { + nudge = nudge || [0, 0]; + + var fn; + if (prevGraph !== context.graph()) { + cache = {}; + origin = context.map().mouseCoordinates(); + fn = context.perform; + } else { + fn = context.overwrite; + } + + var currMouse = context.mouse(), + origMouse = context.projection(origin), + delta = vecSub(vecSub(currMouse, origMouse), nudge); + + fn(actionMove(entityIDs, delta, context.projection, cache), annotation); + prevGraph = context.graph(); } @@ -39,14 +86,7 @@ export function modeMove(context, entityIDs, baseGraph) { if (nudgeInterval) window.clearInterval(nudgeInterval); nudgeInterval = window.setInterval(function() { context.pan(nudge); - - var currMouse = context.mouse(), - origMouse = context.projection(origin), - delta = vecSub(vecSub(currMouse, origMouse), nudge), - action = actionMove(entityIDs, delta, context.projection, cache); - - context.overwrite(action, annotation); - + doMove(nudge); }, 50); } @@ -58,14 +98,8 @@ export function modeMove(context, entityIDs, baseGraph) { function move() { - var currMouse = context.mouse(), - origMouse = context.projection(origin), - delta = vecSub(currMouse, origMouse), - action = actionMove(entityIDs, delta, context.projection, cache); - - context.overwrite(action, annotation); - - var nudge = edge(currMouse, context.map().dimensions()); + doMove(); + var nudge = edge(context.mouse(), context.map().dimensions()); if (nudge) startNudge(nudge); else stopNudge(); } @@ -97,14 +131,12 @@ export function modeMove(context, entityIDs, baseGraph) { mode.enter = function() { origin = context.map().mouseCoordinates(); + prevGraph = null; cache = {}; - context.install(edit); - - context.perform( - actionNoop(), - annotation - ); + behaviors.forEach(function(behavior) { + context.install(behavior); + }); context.surface() .on('mousemove.move', move) @@ -125,7 +157,9 @@ export function modeMove(context, entityIDs, baseGraph) { mode.exit = function() { stopNudge(); - context.uninstall(edit); + behaviors.forEach(function(behavior) { + context.uninstall(behavior); + }); context.surface() .on('mousemove.move', null) From 087a8c62d1cbc46e05adc2feea97b0d8260d8b03 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 20 Dec 2016 00:28:24 -0500 Subject: [PATCH 04/14] Behaviors for all Operations --- modules/behavior/operation.js | 2 +- modules/modes/select.js | 25 ++++++++++--------------- modules/operations/circularize.js | 7 ++++--- modules/operations/continue.js | 3 ++- modules/operations/delete.js | 7 ++++--- modules/operations/disconnect.js | 4 +++- modules/operations/merge.js | 3 ++- modules/operations/move.js | 6 ++++-- modules/operations/orthogonalize.js | 7 ++++--- modules/operations/reflect.js | 1 - modules/operations/reverse.js | 11 ++++------- modules/operations/rotate.js | 4 +++- modules/operations/split.js | 5 +++-- modules/operations/straighten.js | 6 +++--- 14 files changed, 47 insertions(+), 44 deletions(-) diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index fa57ddc41..0e2741dd8 100644 --- a/modules/behavior/operation.js +++ b/modules/behavior/operation.js @@ -12,7 +12,7 @@ export function behaviorOperation(context) { keybinding = d3keybinding('behavior.key.' + which.id); keybinding.on(which.keys, function() { d3.event.preventDefault(); - if (!(context.inIntro() || which.disabled())) { + if (which.available() && !which.disabled() && !context.inIntro()) { which(); } }); diff --git a/modules/modes/select.js b/modules/modes/select.js index 077fc670c..a4d2dc7a6 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -392,16 +392,22 @@ export function modeSelect(context, selectedIDs) { if (!checkSelectedIDs()) return; - behaviors.forEach(function(behavior) { - context.install(behavior); - }); - var operations = _.without(d3.values(Operations), Operations.operationDelete) .map(function(o) { return o(selectedIDs, context); }) .filter(function(o) { return o.available(); }); operations.unshift(Operations.operationDelete(selectedIDs, context)); + operations.forEach(function(operation) { + if (operation.behavior) { + behaviors.push(operation.behavior); + } + }); + + behaviors.forEach(function(behavior) { + context.install(behavior); + }); + keybinding .on(['[','pgup'], previousVertex) .on([']', 'pgdown'], nextVertex) @@ -411,17 +417,6 @@ export function modeSelect(context, selectedIDs) { .on('⎋', esc, true) .on('space', toggleMenu); - operations.forEach(function(operation) { - operation.keys.forEach(function(key) { - keybinding.on(key, function() { - d3.event.preventDefault(); - if (!(context.inIntro() || operation.disabled())) { - operation(); - } - }); - }); - }); - d3.select(document) .call(keybinding); diff --git a/modules/operations/circularize.js b/modules/operations/circularize.js index 475523ecb..8aebef11e 100644 --- a/modules/operations/circularize.js +++ b/modules/operations/circularize.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import { t } from '../util/locale'; import { actionCircularize } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; export function operationCircularize(selectedIDs, context) { @@ -10,9 +11,9 @@ export function operationCircularize(selectedIDs, context) { geometry = context.geometry(entityId), action = actionCircularize(entityId, context.projection); + var operation = function() { - var annotation = t('operations.circularize.annotation.' + geometry); - context.perform(action, annotation); + context.perform(action, t('operations.circularize.annotation.' + geometry)); }; @@ -45,7 +46,7 @@ export function operationCircularize(selectedIDs, context) { operation.id = 'circularize'; operation.keys = [t('operations.circularize.key')]; operation.title = t('operations.circularize.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/continue.js b/modules/operations/continue.js index e0b286d83..4feaab5ef 100644 --- a/modules/operations/continue.js +++ b/modules/operations/continue.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import { t } from '../util/locale'; import { modeDrawLine } from '../modes/index'; +import { behaviorOperation } from '../behavior/index'; export function operationContinue(selectedIDs, context) { @@ -54,7 +55,7 @@ export function operationContinue(selectedIDs, context) { operation.id = 'continue'; operation.keys = [t('operations.continue.key')]; operation.title = t('operations.continue.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/delete.js b/modules/operations/delete.js index af046916f..ffdcb5a27 100644 --- a/modules/operations/delete.js +++ b/modules/operations/delete.js @@ -1,9 +1,10 @@ import _ from 'lodash'; import { t } from '../util/locale'; -import { modeBrowse, modeSelect } from '../modes/index'; import { actionDeleteMultiple } from '../actions/index'; -import { uiCmd } from '../ui/index'; +import { behaviorOperation } from '../behavior/index'; import { geoSphericalDistance } from '../geo/index'; +import { modeBrowse, modeSelect } from '../modes/index'; +import { uiCmd } from '../ui/index'; export function operationDelete(selectedIDs, context) { @@ -82,7 +83,7 @@ export function operationDelete(selectedIDs, context) { operation.id = 'delete'; operation.keys = [uiCmd('⌘⌫'), uiCmd('⌘⌦'), uiCmd('⌦')]; operation.title = t('operations.delete.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/disconnect.js b/modules/operations/disconnect.js index 842aa4afa..420162ecd 100644 --- a/modules/operations/disconnect.js +++ b/modules/operations/disconnect.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import { t } from '../util/locale'; import { actionDisconnect } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; export function operationDisconnect(selectedIDs, context) { @@ -15,6 +16,7 @@ export function operationDisconnect(selectedIDs, context) { action.limitWays(_.without(selectedIDs, entityId)); } + var operation = function() { context.perform(action, t('operations.disconnect.annotation')); }; @@ -45,7 +47,7 @@ export function operationDisconnect(selectedIDs, context) { operation.id = 'disconnect'; operation.keys = [t('operations.disconnect.key')]; operation.title = t('operations.disconnect.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/merge.js b/modules/operations/merge.js index 345440cce..d58a9a8bf 100644 --- a/modules/operations/merge.js +++ b/modules/operations/merge.js @@ -5,6 +5,7 @@ import { actionMergePolygon } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; import { modeSelect } from '../modes/index'; @@ -68,7 +69,7 @@ export function operationMerge(selectedIDs, context) { operation.id = 'merge'; operation.keys = [t('operations.merge.key')]; operation.title = t('operations.merge.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/move.js b/modules/operations/move.js index 69a2f2a4b..2c8fb58d0 100644 --- a/modules/operations/move.js +++ b/modules/operations/move.js @@ -1,7 +1,8 @@ import _ from 'lodash'; import { t } from '../util/locale'; -import { geoExtent } from '../geo/index'; import { actionMove } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; +import { geoExtent } from '../geo/index'; import { modeMove } from '../modes/index'; @@ -10,6 +11,7 @@ export function operationMove(selectedIDs, context) { return extent.extend(context.entity(id).extent(context.graph())); }, geoExtent()); + var operation = function() { context.enter(modeMove(context, selectedIDs)); }; @@ -44,7 +46,7 @@ export function operationMove(selectedIDs, context) { operation.id = 'move'; operation.keys = [t('operations.move.key')]; operation.title = t('operations.move.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/orthogonalize.js b/modules/operations/orthogonalize.js index b8143edce..8275e4f7a 100644 --- a/modules/operations/orthogonalize.js +++ b/modules/operations/orthogonalize.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import { t } from '../util/locale'; import { actionOrthogonalize } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; export function operationOrthogonalize(selectedIDs, context) { @@ -10,9 +11,9 @@ export function operationOrthogonalize(selectedIDs, context) { geometry = context.geometry(entityId), action = actionOrthogonalize(entityId, context.projection); + var operation = function() { - var annotation = t('operations.orthogonalize.annotation.' + geometry); - context.perform(action, annotation); + context.perform(action, t('operations.orthogonalize.annotation.' + geometry)); }; @@ -46,7 +47,7 @@ export function operationOrthogonalize(selectedIDs, context) { operation.id = 'orthogonalize'; operation.keys = [t('operations.orthogonalize.key')]; operation.title = t('operations.orthogonalize.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/reflect.js b/modules/operations/reflect.js index b7e5e1808..7656e6495 100644 --- a/modules/operations/reflect.js +++ b/modules/operations/reflect.js @@ -56,6 +56,5 @@ export function operationReflect(selectedIDs, context, axis) { operation.title = t('operations.reflect.title'); operation.behavior = behaviorOperation(context).which(operation); - return operation; } diff --git a/modules/operations/reverse.js b/modules/operations/reverse.js index 6c75df732..2e0d39b40 100644 --- a/modules/operations/reverse.js +++ b/modules/operations/reverse.js @@ -1,21 +1,18 @@ import { t } from '../util/locale'; import { actionReverse } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; export function operationReverse(selectedIDs, context) { var entityId = selectedIDs[0]; var operation = function() { - context.perform( - actionReverse(entityId), - t('operations.reverse.annotation') - ); + context.perform(actionReverse(entityId), t('operations.reverse.annotation')); }; operation.available = function() { - return selectedIDs.length === 1 && - context.geometry(entityId) === 'line'; + return selectedIDs.length === 1 && context.geometry(entityId) === 'line'; }; @@ -32,7 +29,7 @@ export function operationReverse(selectedIDs, context) { operation.id = 'reverse'; operation.keys = [t('operations.reverse.key')]; operation.title = t('operations.reverse.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/rotate.js b/modules/operations/rotate.js index 42cadfd1c..431e56df9 100644 --- a/modules/operations/rotate.js +++ b/modules/operations/rotate.js @@ -1,5 +1,6 @@ import { t } from '../util/locale'; import { modeRotateWay } from '../modes/index'; +import { behaviorOperation } from '../behavior/index'; export function operationRotate(selectedIDs, context) { @@ -8,6 +9,7 @@ export function operationRotate(selectedIDs, context) { extent = entity.extent(context.graph()), geometry = context.geometry(entityId); + var operation = function() { context.enter(modeRotateWay(context, entityId)); }; @@ -47,7 +49,7 @@ export function operationRotate(selectedIDs, context) { operation.id = 'rotate'; operation.keys = [t('operations.rotate.key')]; operation.title = t('operations.rotate.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/split.js b/modules/operations/split.js index 0f6aba352..cc81c90a3 100644 --- a/modules/operations/split.js +++ b/modules/operations/split.js @@ -1,7 +1,8 @@ import _ from 'lodash'; import { t } from '../util/locale'; -import { modeSelect } from '../modes/index'; import { actionSplit } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; +import { modeSelect } from '../modes/index'; export function operationSplit(selectedIDs, context) { @@ -64,7 +65,7 @@ export function operationSplit(selectedIDs, context) { operation.id = 'split'; operation.keys = [t('operations.split.key')]; operation.title = t('operations.split.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } diff --git a/modules/operations/straighten.js b/modules/operations/straighten.js index e0d42cb8d..8a029caa0 100644 --- a/modules/operations/straighten.js +++ b/modules/operations/straighten.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import { t } from '../util/locale'; import { actionStraighten } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; export function operationStraighten(selectedIDs, context) { @@ -9,8 +10,7 @@ export function operationStraighten(selectedIDs, context) { function operation() { - var annotation = t('operations.straighten.annotation'); - context.perform(action, annotation); + context.perform(action, t('operations.straighten.annotation')); } @@ -43,7 +43,7 @@ export function operationStraighten(selectedIDs, context) { operation.id = 'straighten'; operation.keys = [t('operations.straighten.key')]; operation.title = t('operations.straighten.title'); - + operation.behavior = behaviorOperation(context).which(operation); return operation; } From addd12ae99fbc83c6dd922b4f4cc009f117d5073 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 20 Dec 2016 01:31:49 -0500 Subject: [PATCH 05/14] Exclude child nodes from newIDs if their parent way was also copied for #3656 item 1 --- modules/behavior/paste.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/behavior/paste.js b/modules/behavior/paste.js index a8898cf22..b1ebe3970 100644 --- a/modules/behavior/paste.js +++ b/modules/behavior/paste.js @@ -59,15 +59,29 @@ export function behaviorPaste(context) { context.perform(action); var copies = action.copies(); + var originals = _.invert(_.mapValues(copies, 'id')); for (var id in copies) { var oldEntity = oldGraph.entity(id), newEntity = copies[id]; extent._extend(oldEntity.extent(oldGraph)); - newIDs.push(newEntity.id); context.perform( actionChangeTags(newEntity.id, _.omit(newEntity.tags, omitTag)) ); + + // Exclude child nodes from newIDs if their parent way was also copied. + var parents = context.graph().parentWays(newEntity), + parentCopied = false; + for (var i = 0; i < parents.length; i++) { + if (originals[parents[i].id]) { + parentCopied = true; + break; + } + } + + if (!parentCopied) { + newIDs.push(newEntity.id); + } } // Put pasted objects where mouse pointer is.. From 57696ab5eb95fbc6113da518f5ecf6e91bae779a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 20 Dec 2016 13:44:39 -0500 Subject: [PATCH 06/14] Rename `rotate-way` mode to just `rotate` --- modules/modes/index.js | 2 +- modules/modes/move.js | 10 +++++++++- modules/modes/{rotate_way.js => rotate.js} | 18 +++++++++--------- modules/operations/rotate.js | 4 ++-- 4 files changed, 21 insertions(+), 13 deletions(-) rename modules/modes/{rotate_way.js => rotate.js} (84%) diff --git a/modules/modes/index.js b/modules/modes/index.js index 98cefa8c1..4b2737be1 100644 --- a/modules/modes/index.js +++ b/modules/modes/index.js @@ -6,6 +6,6 @@ export { modeDragNode } from './drag_node'; export { modeDrawArea } from './draw_area'; export { modeDrawLine } from './draw_line'; export { modeMove } from './move'; -export { modeRotateWay } from './rotate_way'; +export { modeRotate } from './rotate'; export { modeSave } from './save'; export { modeSelect } from './select'; diff --git a/modules/modes/move.js b/modules/modes/move.js index f174d14ae..7ec0c3542 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -11,8 +11,12 @@ import { } from './index'; import { + operationCircularize, + operationDelete, + operationOrthogonalize, operationReflectLong, - operationReflectShort + operationReflectShort, + operationRotate } from '../operations/index'; @@ -25,8 +29,12 @@ export function modeMove(context, entityIDs, baseGraph) { var keybinding = d3keybinding('move'), behaviors = [ behaviorEdit(context), + operationCircularize(entityIDs, context).behavior, + operationDelete(entityIDs, context).behavior, + operationOrthogonalize(entityIDs, context).behavior, operationReflectLong(entityIDs, context).behavior, operationReflectShort(entityIDs, context).behavior, + operationRotate(entityIDs, context).behavior ], annotation = entityIDs.length === 1 ? t('operations.move.annotation.' + context.geometry(entityIDs[0])) : diff --git a/modules/modes/rotate_way.js b/modules/modes/rotate.js similarity index 84% rename from modules/modes/rotate_way.js rename to modules/modes/rotate.js index 80cf0147c..c75b0be8c 100644 --- a/modules/modes/rotate_way.js +++ b/modules/modes/rotate.js @@ -7,13 +7,13 @@ import { actionNoop, actionRotateWay } from '../actions/index'; import { behaviorEdit } from '../behavior/index'; -export function modeRotateWay(context, wayId) { +export function modeRotate(context, wayId) { var mode = { - id: 'rotate-way', + id: 'rotate', button: 'browse' }; - var keybinding = d3keybinding('rotate-way'), + var keybinding = d3keybinding('rotate'), edit = behaviorEdit(context); @@ -33,11 +33,11 @@ export function modeRotateWay(context, wayId) { ); context.surface() - .on('mousemove.rotate-way', rotate) - .on('click.rotate-way', finish); + .on('mousemove.rotate', rotate) + .on('click.rotate', finish); context.history() - .on('undone.rotate-way', undone); + .on('undone.rotate', undone); keybinding .on('⎋', cancel) @@ -84,11 +84,11 @@ export function modeRotateWay(context, wayId) { context.uninstall(edit); context.surface() - .on('mousemove.rotate-way', null) - .on('click.rotate-way', null); + .on('mousemove.rotate', null) + .on('click.rotate', null); context.history() - .on('undone.rotate-way', null); + .on('undone.rotate', null); keybinding.off(); }; diff --git a/modules/operations/rotate.js b/modules/operations/rotate.js index 431e56df9..a24c0e66e 100644 --- a/modules/operations/rotate.js +++ b/modules/operations/rotate.js @@ -1,5 +1,5 @@ import { t } from '../util/locale'; -import { modeRotateWay } from '../modes/index'; +import { modeRotate } from '../modes/index'; import { behaviorOperation } from '../behavior/index'; @@ -11,7 +11,7 @@ export function operationRotate(selectedIDs, context) { var operation = function() { - context.enter(modeRotateWay(context, entityId)); + context.enter(modeRotate(context, entityId)); }; From a2f50f448526a1f957621cdbf389611c505efb22 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 20 Dec 2016 14:55:02 -0500 Subject: [PATCH 07/14] Support behaviors in Rotate mode --- modules/modes/rotate.js | 86 +++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index c75b0be8c..eff29c988 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -2,10 +2,28 @@ import * as d3 from 'd3'; import _ from 'lodash'; import { d3keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; -import { modeBrowse, modeSelect } from './index'; -import { actionNoop, actionRotateWay } from '../actions/index'; + +import { + actionNoop, + actionRotateWay +} from '../actions/index'; + import { behaviorEdit } from '../behavior/index'; +import { + modeBrowse, + modeSelect +} from './index'; + +import { + operationCircularize, + operationDelete, + operationMove, + operationOrthogonalize, + operationReflectLong, + operationReflectShort +} from '../operations/index'; + export function modeRotate(context, wayId) { var mode = { @@ -14,26 +32,37 @@ export function modeRotate(context, wayId) { }; var keybinding = d3keybinding('rotate'), - edit = behaviorEdit(context); + 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 + ], + prevGraph, + prevAngle, + pivot; mode.enter = function() { - context.install(edit); - - var annotation = t('operations.rotate.annotation.' + context.geometry(wayId)), - way = context.graph().entity(wayId), + 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), - angle; + points = nodes.map(function(n) { return context.projection(n.loc); }); - context.perform( - actionNoop(), - annotation - ); + 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', rotate) + .on('mousemove.rotate', doRotate) .on('click.rotate', finish); context.history() @@ -47,18 +76,23 @@ export function modeRotate(context, wayId) { .call(keybinding); - function rotate() { - var mousePoint = context.mouse(), - newAngle = Math.atan2(mousePoint[1] - pivot[1], mousePoint[0] - pivot[0]); + function doRotate() { + var fn; + if (prevGraph !== context.graph()) { + fn = context.perform; + } else { + fn = context.replace; + } - if (typeof angle === 'undefined') angle = newAngle; + var currMouse = context.mouse(), + currAngle = Math.atan2(currMouse[1] - pivot[1], currMouse[0] - pivot[0]); - context.replace( - actionRotateWay(wayId, pivot, newAngle - angle, context.projection), - annotation - ); + if (typeof prevAngle === 'undefined') prevAngle = currAngle; + var delta = currAngle - prevAngle; - angle = newAngle; + fn(actionRotateWay(wayId, pivot, delta, context.projection), annotation); + prevAngle = currAngle; + prevGraph = context.graph(); } @@ -81,7 +115,9 @@ export function modeRotate(context, wayId) { mode.exit = function() { - context.uninstall(edit); + behaviors.forEach(function(behavior) { + context.uninstall(behavior); + }); context.surface() .on('mousemove.rotate', null) From 6ab9489fe79210f4766578789887d2532bdca3d1 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 20 Dec 2016 22:37:53 -0500 Subject: [PATCH 08/14] Add utilGetAllNodes to get all nodes and descendants --- modules/actions/{rotate_way.js => rotate.js} | 0 modules/util/index.js | 1 + modules/util/util.js | 24 +++++++++ test/spec/util/util.js | 54 ++++++++++++++++++++ 4 files changed, 79 insertions(+) rename modules/actions/{rotate_way.js => rotate.js} (100%) diff --git a/modules/actions/rotate_way.js b/modules/actions/rotate.js similarity index 100% rename from modules/actions/rotate_way.js rename to modules/actions/rotate.js diff --git a/modules/util/index.js b/modules/util/index.js index 75e691fc2..e84dc86db 100644 --- a/modules/util/index.js +++ b/modules/util/index.js @@ -1,6 +1,7 @@ export { utilTagText } from './util'; export { utilEntitySelector } from './util'; export { utilEntityOrMemberSelector } from './util'; +export { utilGetAllNodes } from './util'; export { utilDisplayName } from './util'; export { utilDisplayType } from './util'; export { utilStringQs } from './util'; diff --git a/modules/util/util.js b/modules/util/util.js index 4e142178a..3302a3f56 100644 --- a/modules/util/util.js +++ b/modules/util/util.js @@ -32,6 +32,30 @@ export function utilEntityOrMemberSelector(ids, graph) { } +export function utilGetAllNodes(ids, graph) { + var seen = {}; + var nodes = []; + ids.forEach(getNodes); + return nodes; + + function getNodes(id) { + if (seen[id]) return; + seen[id] = true; + + var entity = graph.hasEntity(id); + if (!entity) return; + + if (entity.type === 'node') { + nodes.push(entity); + } else if (entity.type === 'way') { + entity.nodes.forEach(getNodes); + } else { + entity.members.map(function(member) { return member.id; }).forEach(getNodes); + } + } +} + + export function utilDisplayName(entity) { var localizedNameKey = 'name:' + utilDetect().locale.toLowerCase().split('-')[0], name = entity.tags[localizedNameKey] || entity.tags.name || '', diff --git a/test/spec/util/util.js b/test/spec/util/util.js index f235f4fb0..da64f309e 100644 --- a/test/spec/util/util.js +++ b/test/spec/util/util.js @@ -1,4 +1,58 @@ describe('iD.util', function() { + + describe('utilGetAllNodes', function() { + it('gets all descendant nodes of a way', function() { + var a = iD.Node({ id: 'a' }), + b = iD.Node({ id: 'b' }), + w = iD.Way({ id: 'w', nodes: ['a','b','a'] }), + graph = iD.Graph([a, b, w]), + result = iD.utilGetAllNodes(['w'], graph); + + expect(result).to.have.members([a, b]); + expect(result).to.have.lengthOf(2); + }); + + it('gets all descendant nodes of a relation', function() { + var a = iD.Node({ id: 'a' }), + b = iD.Node({ id: 'b' }), + c = iD.Node({ id: 'c' }), + w = iD.Way({ id: 'w', nodes: ['a','b','a'] }), + r = iD.Relation({ id: 'r', members: [{id: 'w'}, {id: 'c'}] }), + graph = iD.Graph([a, b, c, w, r]), + result = iD.utilGetAllNodes(['r'], graph); + + expect(result).to.have.members([a, b, c]); + expect(result).to.have.lengthOf(3); + }); + + it('gets all descendant nodes of multiple ids', function() { + var a = iD.Node({ id: 'a' }), + b = iD.Node({ id: 'b' }), + c = iD.Node({ id: 'c' }), + d = iD.Node({ id: 'd' }), + e = iD.Node({ id: 'e' }), + w1 = iD.Way({ id: 'w1', nodes: ['a','b','a'] }), + w2 = iD.Way({ id: 'w2', nodes: ['c','b','a','c'] }), + r = iD.Relation({ id: 'r', members: [{id: 'w1'}, {id: 'd'}] }), + graph = iD.Graph([a, b, c, d, e, w1, w2, r]), + result = iD.utilGetAllNodes(['r', 'w2', 'e'], graph); + + expect(result).to.have.members([a, b, c, d, e]); + expect(result).to.have.lengthOf(5); + }); + + it('handles recursive relations', function() { + var a = iD.Node({ id: 'a' }), + r1 = iD.Relation({ id: 'r1', members: [{id: 'r2'}] }), + r2 = iD.Relation({ id: 'r2', members: [{id: 'r1'}, {id: 'a'}] }), + graph = iD.Graph([a, r1, r2]), + result = iD.utilGetAllNodes(['r1'], graph); + + expect(result).to.have.members([a]); + expect(result).to.have.lengthOf(1); + }); + }); + it('utilTagText', function() { expect(iD.utilTagText({})).to.eql(''); expect(iD.utilTagText({tags:{foo:'bar'}})).to.eql('foo=bar'); From 573f476cddb9ed8da1e23eb94641417a2ad69425 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 20 Dec 2016 22:38:30 -0500 Subject: [PATCH 09/14] Rename actionRotateWay -> actionRotate --- modules/actions/index.js | 2 +- modules/actions/rotate.js | 2 +- modules/modes/rotate.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/actions/index.js b/modules/actions/index.js index e45777bf8..ff768cdd0 100644 --- a/modules/actions/index.js +++ b/modules/actions/index.js @@ -27,7 +27,7 @@ export { actionOrthogonalize } from './orthogonalize'; export { actionRestrictTurn } from './restrict_turn'; export { actionReverse } from './reverse'; export { actionRevert } from './revert'; -export { actionRotateWay } from './rotate_way'; +export { actionRotate } from './rotate'; export { actionSplit } from './split'; export { actionStraighten } from './straighten'; export { actionUnrestrictTurn } from './unrestrict_turn'; diff --git a/modules/actions/rotate.js b/modules/actions/rotate.js index 6f259da22..f1c25db0d 100644 --- a/modules/actions/rotate.js +++ b/modules/actions/rotate.js @@ -1,7 +1,7 @@ import _ from 'lodash'; -export function actionRotateWay(wayId, pivot, angle, projection) { +export function actionRotate(wayId, pivot, angle, projection) { return function(graph) { return graph.update(function(graph) { var way = graph.entity(wayId); diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index eff29c988..b6da3e84f 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -5,7 +5,7 @@ import { t } from '../util/locale'; import { actionNoop, - actionRotateWay + actionRotate } from '../actions/index'; import { behaviorEdit } from '../behavior/index'; @@ -90,7 +90,7 @@ export function modeRotate(context, wayId) { if (typeof prevAngle === 'undefined') prevAngle = currAngle; var delta = currAngle - prevAngle; - fn(actionRotateWay(wayId, pivot, delta, context.projection), annotation); + fn(actionRotate(wayId, pivot, delta, context.projection), annotation); prevAngle = currAngle; prevGraph = context.graph(); } From 063e7712b8ebd5123a6be195f7d04dcac1c1b65d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 20 Dec 2016 23:08:22 -0500 Subject: [PATCH 10/14] Move geo functions from index.js to geo.js --- modules/geo/geo.js | 264 +++++++++++++++++++++++++++++++++++++++ modules/geo/index.js | 285 +++---------------------------------------- 2 files changed, 284 insertions(+), 265 deletions(-) create mode 100644 modules/geo/geo.js diff --git a/modules/geo/geo.js b/modules/geo/geo.js new file mode 100644 index 000000000..218124f95 --- /dev/null +++ b/modules/geo/geo.js @@ -0,0 +1,264 @@ +import _ from 'lodash'; + + +export function geoRoundCoords(c) { + return [Math.floor(c[0]), Math.floor(c[1])]; +} + + +export function geoInterp(p1, p2, t) { + return [p1[0] + (p2[0] - p1[0]) * t, + p1[1] + (p2[1] - p1[1]) * t]; +} + + +// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. +// Returns a positive value, if OAB makes a counter-clockwise turn, +// negative for clockwise turn, and zero if the points are collinear. +export function geoCross(o, a, b) { + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); +} + + +// http://jsperf.com/id-dist-optimization +export function geoEuclideanDistance(a, b) { + var x = a[0] - b[0], y = a[1] - b[1]; + return Math.sqrt((x * x) + (y * y)); +} + + +// using WGS84 polar radius (6356752.314245179 m) +// const = 2 * PI * r / 360 +export function geoLatToMeters(dLat) { + return dLat * 110946.257617; +} + + +// using WGS84 equatorial radius (6378137.0 m) +// const = 2 * PI * r / 360 +export function geoLonToMeters(dLon, atLat) { + return Math.abs(atLat) >= 90 ? 0 : + dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); +} + + +// using WGS84 polar radius (6356752.314245179 m) +// const = 2 * PI * r / 360 +export function geoMetersToLat(m) { + return m / 110946.257617; +} + + +// using WGS84 equatorial radius (6378137.0 m) +// const = 2 * PI * r / 360 +export function geoMetersToLon(m, atLat) { + return Math.abs(atLat) >= 90 ? 0 : + m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); +} + + +export function geoOffsetToMeters(offset) { + var equatRadius = 6356752.314245179, + polarRadius = 6378137.0, + tileSize = 256; + + return [ + offset[0] * 2 * Math.PI * equatRadius / tileSize, + -offset[1] * 2 * Math.PI * polarRadius / tileSize + ]; +} + + +export function geoMetersToOffset(meters) { + var equatRadius = 6356752.314245179, + polarRadius = 6378137.0, + tileSize = 256; + + return [ + meters[0] * tileSize / (2 * Math.PI * equatRadius), + -meters[1] * tileSize / (2 * Math.PI * polarRadius) + ]; +} + + +// Equirectangular approximation of spherical distances on Earth +export function geoSphericalDistance(a, b) { + var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), + y = geoLatToMeters(a[1] - b[1]); + return Math.sqrt((x * x) + (y * y)); +} + + +export function geoEdgeEqual(a, b) { + return (a[0] === b[0] && a[1] === b[1]) || + (a[0] === b[1] && a[1] === b[0]); +} + + +// Return the counterclockwise angle in the range (-pi, pi) +// between the positive X axis and the line intersecting a and b. +export function geoAngle(a, b, projection) { + a = projection(a.loc); + b = projection(b.loc); + return Math.atan2(b[1] - a[1], b[0] - a[0]); +} + + +// Choose the edge with the minimal distance from `point` to its orthogonal +// projection onto that edge, if such a projection exists, or the distance to +// the closest vertex on that edge. Returns an object with the `index` of the +// chosen edge, the chosen `loc` on that edge, and the `distance` to to it. +export function geoChooseEdge(nodes, point, projection) { + var dist = geoEuclideanDistance, + points = nodes.map(function(n) { return projection(n.loc); }), + min = Infinity, + idx, loc; + + function dot(p, q) { + return p[0] * q[0] + p[1] * q[1]; + } + + for (var i = 0; i < points.length - 1; i++) { + var o = points[i], + s = [points[i + 1][0] - o[0], + points[i + 1][1] - o[1]], + v = [point[0] - o[0], + point[1] - o[1]], + proj = dot(v, s) / dot(s, s), + p; + + if (proj < 0) { + p = o; + } else if (proj > 1) { + p = points[i + 1]; + } else { + p = [o[0] + proj * s[0], o[1] + proj * s[1]]; + } + + var d = dist(p, point); + if (d < min) { + min = d; + idx = i + 1; + loc = projection.invert(p); + } + } + + return { + index: idx, + distance: min, + loc: loc + }; +} + + +// Return the intersection point of 2 line segments. +// From https://github.com/pgkelley4/line-segments-intersect +// This uses the vector cross product approach described below: +// http://stackoverflow.com/a/565282/786339 +export function geoLineIntersection(a, b) { + function subtractPoints(point1, point2) { + return [point1[0] - point2[0], point1[1] - point2[1]]; + } + function crossProduct(point1, point2) { + return point1[0] * point2[1] - point1[1] * point2[0]; + } + + var p = [a[0][0], a[0][1]], + p2 = [a[1][0], a[1][1]], + q = [b[0][0], b[0][1]], + q2 = [b[1][0], b[1][1]], + r = subtractPoints(p2, p), + s = subtractPoints(q2, q), + uNumerator = crossProduct(subtractPoints(q, p), r), + denominator = crossProduct(r, s); + + if (uNumerator && denominator) { + var u = uNumerator / denominator, + t = crossProduct(subtractPoints(q, p), s) / denominator; + + if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { + return geoInterp(p, p2, t); + } + } + + return null; +} + + +export function geoPathIntersections(path1, path2) { + var intersections = []; + for (var i = 0; i < path1.length - 1; i++) { + for (var j = 0; j < path2.length - 1; j++) { + var a = [ path1[i], path1[i+1] ], + b = [ path2[j], path2[j+1] ], + hit = geoLineIntersection(a, b); + if (hit) intersections.push(hit); + } + } + return intersections; +} + + +// Return whether point is contained in polygon. +// +// `point` should be a 2-item array of coordinates. +// `polygon` should be an array of 2-item arrays of coordinates. +// +// From https://github.com/substack/point-in-polygon. +// ray-casting algorithm based on +// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html +// +export function geoPointInPolygon(point, polygon) { + var x = point[0], + y = point[1], + inside = false; + + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0], yi = polygon[i][1]; + var xj = polygon[j][0], yj = polygon[j][1]; + + var intersect = ((yi > y) !== (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + if (intersect) inside = !inside; + } + + return inside; +} + + +export function geoPolygonContainsPolygon(outer, inner) { + return _.every(inner, function(point) { + return geoPointInPolygon(point, outer); + }); +} + + +export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { + function testSegments(outer, inner) { + for (var i = 0; i < outer.length - 1; i++) { + for (var j = 0; j < inner.length - 1; j++) { + var a = [ outer[i], outer[i+1] ], + b = [ inner[j], inner[j+1] ]; + if (geoLineIntersection(a, b)) return true; + } + } + return false; + } + + function testPoints(outer, inner) { + return _.some(inner, function(point) { + return geoPointInPolygon(point, outer); + }); + } + + return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); +} + + +export function geoPathLength(path) { + var length = 0; + for (var i = 0; i < path.length - 1; i++) { + length += geoEuclideanDistance(path[i], path[i + 1]); + } + return length; +} diff --git a/modules/geo/index.js b/modules/geo/index.js index e7cbfbbf7..cea5b48cd 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -1,267 +1,22 @@ -import _ from 'lodash'; - +export { geoAngle } from './geo.js'; +export { geoChooseEdge } from './geo.js'; +export { geoCross } from './geo.js'; +export { geoEdgeEqual } from './geo.js'; +export { geoEuclideanDistance } from './geo.js'; export { geoExtent } from './extent.js'; +export { geoInterp } from './geo.js'; export { geoRawMercator } from './raw_mercator.js'; - - -export function geoRoundCoords(c) { - return [Math.floor(c[0]), Math.floor(c[1])]; -} - - -export function geoInterp(p1, p2, t) { - return [p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t]; -} - - -// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. -// Returns a positive value, if OAB makes a counter-clockwise turn, -// negative for clockwise turn, and zero if the points are collinear. -export function geoCross(o, a, b) { - return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); -} - - -// http://jsperf.com/id-dist-optimization -export function geoEuclideanDistance(a, b) { - var x = a[0] - b[0], y = a[1] - b[1]; - return Math.sqrt((x * x) + (y * y)); -} - - -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 -export function geoLatToMeters(dLat) { - return dLat * 110946.257617; -} - - -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 -export function geoLonToMeters(dLon, atLat) { - return Math.abs(atLat) >= 90 ? 0 : - dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); -} - - -// using WGS84 polar radius (6356752.314245179 m) -// const = 2 * PI * r / 360 -export function geoMetersToLat(m) { - return m / 110946.257617; -} - - -// using WGS84 equatorial radius (6378137.0 m) -// const = 2 * PI * r / 360 -export function geoMetersToLon(m, atLat) { - return Math.abs(atLat) >= 90 ? 0 : - m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); -} - - -export function geoOffsetToMeters(offset) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - - return [ - offset[0] * 2 * Math.PI * equatRadius / tileSize, - -offset[1] * 2 * Math.PI * polarRadius / tileSize - ]; -} - - -export function geoMetersToOffset(meters) { - var equatRadius = 6356752.314245179, - polarRadius = 6378137.0, - tileSize = 256; - - return [ - meters[0] * tileSize / (2 * Math.PI * equatRadius), - -meters[1] * tileSize / (2 * Math.PI * polarRadius) - ]; -} - - -// Equirectangular approximation of spherical distances on Earth -export function geoSphericalDistance(a, b) { - var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), - y = geoLatToMeters(a[1] - b[1]); - return Math.sqrt((x * x) + (y * y)); -} - - -export function geoEdgeEqual(a, b) { - return (a[0] === b[0] && a[1] === b[1]) || - (a[0] === b[1] && a[1] === b[0]); -} - - -// Return the counterclockwise angle in the range (-pi, pi) -// between the positive X axis and the line intersecting a and b. -export function geoAngle(a, b, projection) { - a = projection(a.loc); - b = projection(b.loc); - return Math.atan2(b[1] - a[1], b[0] - a[0]); -} - - -// Choose the edge with the minimal distance from `point` to its orthogonal -// projection onto that edge, if such a projection exists, or the distance to -// the closest vertex on that edge. Returns an object with the `index` of the -// chosen edge, the chosen `loc` on that edge, and the `distance` to to it. -export function geoChooseEdge(nodes, point, projection) { - var dist = geoEuclideanDistance, - points = nodes.map(function(n) { return projection(n.loc); }), - min = Infinity, - idx, loc; - - function dot(p, q) { - return p[0] * q[0] + p[1] * q[1]; - } - - for (var i = 0; i < points.length - 1; i++) { - var o = points[i], - s = [points[i + 1][0] - o[0], - points[i + 1][1] - o[1]], - v = [point[0] - o[0], - point[1] - o[1]], - proj = dot(v, s) / dot(s, s), - p; - - if (proj < 0) { - p = o; - } else if (proj > 1) { - p = points[i + 1]; - } else { - p = [o[0] + proj * s[0], o[1] + proj * s[1]]; - } - - var d = dist(p, point); - if (d < min) { - min = d; - idx = i + 1; - loc = projection.invert(p); - } - } - - return { - index: idx, - distance: min, - loc: loc - }; -} - - -// Return the intersection point of 2 line segments. -// From https://github.com/pgkelley4/line-segments-intersect -// This uses the vector cross product approach described below: -// http://stackoverflow.com/a/565282/786339 -export function geoLineIntersection(a, b) { - function subtractPoints(point1, point2) { - return [point1[0] - point2[0], point1[1] - point2[1]]; - } - function crossProduct(point1, point2) { - return point1[0] * point2[1] - point1[1] * point2[0]; - } - - var p = [a[0][0], a[0][1]], - p2 = [a[1][0], a[1][1]], - q = [b[0][0], b[0][1]], - q2 = [b[1][0], b[1][1]], - r = subtractPoints(p2, p), - s = subtractPoints(q2, q), - uNumerator = crossProduct(subtractPoints(q, p), r), - denominator = crossProduct(r, s); - - if (uNumerator && denominator) { - var u = uNumerator / denominator, - t = crossProduct(subtractPoints(q, p), s) / denominator; - - if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { - return geoInterp(p, p2, t); - } - } - - return null; -} - - -export function geoPathIntersections(path1, path2) { - var intersections = []; - for (var i = 0; i < path1.length - 1; i++) { - for (var j = 0; j < path2.length - 1; j++) { - var a = [ path1[i], path1[i+1] ], - b = [ path2[j], path2[j+1] ], - hit = geoLineIntersection(a, b); - if (hit) intersections.push(hit); - } - } - return intersections; -} - - -// Return whether point is contained in polygon. -// -// `point` should be a 2-item array of coordinates. -// `polygon` should be an array of 2-item arrays of coordinates. -// -// From https://github.com/substack/point-in-polygon. -// ray-casting algorithm based on -// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html -// -export function geoPointInPolygon(point, polygon) { - var x = point[0], - y = point[1], - inside = false; - - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - - var intersect = ((yi > y) !== (yj > y)) && - (x < (xj - xi) * (y - yi) / (yj - yi) + xi); - if (intersect) inside = !inside; - } - - return inside; -} - - -export function geoPolygonContainsPolygon(outer, inner) { - return _.every(inner, function(point) { - return geoPointInPolygon(point, outer); - }); -} - - -export function geoPolygonIntersectsPolygon(outer, inner, checkSegments) { - function testSegments(outer, inner) { - for (var i = 0; i < outer.length - 1; i++) { - for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i+1] ], - b = [ inner[j], inner[j+1] ]; - if (geoLineIntersection(a, b)) return true; - } - } - return false; - } - - function testPoints(outer, inner) { - return _.some(inner, function(point) { - return geoPointInPolygon(point, outer); - }); - } - - return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); -} - - -export function geoPathLength(path) { - var length = 0; - for (var i = 0; i < path.length - 1; i++) { - length += geoEuclideanDistance(path[i], path[i + 1]); - } - return length; -} +export { geoRoundCoords } from './geo.js'; +export { geoLatToMeters } from './geo.js'; +export { geoLineIntersection } from './geo.js'; +export { geoLonToMeters } from './geo.js'; +export { geoMetersToLat } from './geo.js'; +export { geoMetersToLon } from './geo.js'; +export { geoMetersToOffset } from './geo.js'; +export { geoOffsetToMeters } from './geo.js'; +export { geoPathIntersections } from './geo.js'; +export { geoPathLength } from './geo.js'; +export { geoPointInPolygon } from './geo.js'; +export { geoPolygonContainsPolygon } from './geo.js'; +export { geoPolygonIntersectsPolygon } from './geo.js'; +export { geoSphericalDistance } from './geo.js'; From cad4c0090ced813e39f2dad30c44d3f356d5e657 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 21 Dec 2016 11:21:06 -0500 Subject: [PATCH 11/14] Refactor rotation code to geoRotate, add tests --- modules/actions/reflect.js | 16 ++++--------- modules/actions/rotate.js | 20 ++++------------ modules/geo/geo.js | 12 ++++++++++ modules/geo/index.js | 1 + test/spec/geo/geo.js | 48 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 27 deletions(-) diff --git a/modules/actions/reflect.js b/modules/actions/reflect.js index 3cdbcb62a..056932c58 100644 --- a/modules/actions/reflect.js +++ b/modules/actions/reflect.js @@ -6,7 +6,8 @@ import { import { geoEuclideanDistance, - geoExtent + geoExtent, + geoRotate } from '../geo'; @@ -14,15 +15,6 @@ import { export function actionReflect(wayId, projection) { var useLongAxis = true; - function rotatePolygon(polygon, angle, centroid) { - return polygon.map(function(point) { - var radial = [point[0] - centroid[0], point[1] - centroid[1]]; - return [ - radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + centroid[0], - radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + centroid[1] - ]; - }); - } // 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 @@ -39,7 +31,7 @@ export function actionReflect(wayId, projection) { for (var i = 0; i < hull.length - 1; i++) { var c2 = hull[i + 1], angle = Math.atan2(c2[1] - c1[1], c2[0] - c1[0]), - poly = rotatePolygon(hull, -angle, centroid), + poly = geoRotate(hull, -angle, centroid), extent = poly.reduce(function(extent, point) { return extent.extend(geoExtent(point)); }, geoExtent()), @@ -54,7 +46,7 @@ export function actionReflect(wayId, projection) { } return { - poly: rotatePolygon(ssrExtent.polygon(), ssrAngle, centroid), + poly: geoRotate(ssrExtent.polygon(), ssrAngle, centroid), angle: ssrAngle }; } diff --git a/modules/actions/rotate.js b/modules/actions/rotate.js index f1c25db0d..8667876f7 100644 --- a/modules/actions/rotate.js +++ b/modules/actions/rotate.js @@ -1,29 +1,19 @@ import _ from 'lodash'; +import { geoRotate } from '../geo'; export function actionRotate(wayId, pivot, angle, projection) { - return function(graph) { + 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 = projection(node.loc), - radial = [0,0]; - - radial[0] = point[0] - pivot[0]; - radial[1] = point[1] - pivot[1]; - - point = [ - radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + pivot[0], - radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + pivot[1] - ]; + point = geoRotate([projection(node.loc)], angle, pivot)[0]; graph = graph.replace(node.move(projection.invert(point))); - }); - }); }; + + return action; } diff --git a/modules/geo/geo.js b/modules/geo/geo.js index 218124f95..096c0a18e 100644 --- a/modules/geo/geo.js +++ b/modules/geo/geo.js @@ -104,6 +104,18 @@ export function geoAngle(a, b, projection) { } +// Rotate all points counterclockwise around a pivot point by given angle +export function geoRotate(points, angle, around) { + return points.map(function(point) { + var radial = [point[0] - around[0], point[1] - around[1]]; + return [ + radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0], + radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1] + ]; + }); +} + + // Choose the edge with the minimal distance from `point` to its orthogonal // projection onto that edge, if such a projection exists, or the distance to // the closest vertex on that edge. Returns an object with the `index` of the diff --git a/modules/geo/index.js b/modules/geo/index.js index cea5b48cd..5e37b16fe 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -7,6 +7,7 @@ export { geoExtent } from './extent.js'; export { geoInterp } from './geo.js'; export { geoRawMercator } from './raw_mercator.js'; export { geoRoundCoords } from './geo.js'; +export { geoRotate } from './geo.js'; export { geoLatToMeters } from './geo.js'; export { geoLineIntersection } from './geo.js'; export { geoLonToMeters } from './geo.js'; diff --git a/test/spec/geo/geo.js b/test/spec/geo/geo.js index 96332a8c2..6b2e0cb85 100644 --- a/test/spec/geo/geo.js +++ b/test/spec/geo/geo.js @@ -185,6 +185,54 @@ describe('iD.geo', function() { }); }); + describe('geoEdgeEqual', function() { + it('returns false for inequal edges', function() { + expect(iD.geoEdgeEqual(['a','b'], ['a','c'])).to.be.false; + }); + + it('returns true for equal edges along same direction', function() { + expect(iD.geoEdgeEqual(['a','b'], ['a','b'])).to.be.true; + }); + + it('returns true for equal edges along opposite direction', function() { + expect(iD.geoEdgeEqual(['a','b'], ['b','a'])).to.be.true; + }); + }); + + describe('geoAngle', function() { + it('returns angle between a and b', function() { + var projection = function (_) { return _; }; + expect(iD.geoAngle({loc:[0, 0]}, {loc:[1, 0]}, projection)).to.be.closeTo(0, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, 1]}, projection)).to.be.closeTo(Math.PI / 2, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[-1, 0]}, projection)).to.be.closeTo(Math.PI, 1e-6); + expect(iD.geoAngle({loc:[0, 0]}, {loc:[0, -1]}, projection)).to.be.closeTo(-Math.PI / 2, 1e-6); + }); + }); + + describe('geoRotate', function() { + it('rotates points around [0, 0]', function() { + var points = [[5, 0], [5, 1]], + angle = Math.PI, + around = [0, 0], + result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(-5, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(-5, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + + it('rotates points around [3, 0]', function() { + var points = [[5, 0], [5, 1]], + angle = Math.PI, + around = [3, 0], + result = iD.geoRotate(points, angle, around); + expect(result[0][0]).to.be.closeTo(1, 1e-6); + expect(result[0][1]).to.be.closeTo(0, 1e-6); + expect(result[1][0]).to.be.closeTo(1, 1e-6); + expect(result[1][1]).to.be.closeTo(-1, 1e-6); + }); + }); + describe('geoChooseEdge', function() { var projection = function (l) { return l; }; projection.invert = projection; From 38e4900355025b031422c0d1ef35f066f02b87fe Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 21 Dec 2016 16:44:40 -0500 Subject: [PATCH 12/14] 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')); }; From 37534aed0e9af6f456c8b0df31547bbc8fdc4089 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 21 Dec 2016 23:58:13 -0500 Subject: [PATCH 13/14] 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 --- data/core.yaml | 75 +++++++++++++++++----- dist/locales/en.json | 96 ++++++++++++++++++++++------ modules/actions/delete_multiple.js | 9 --- modules/actions/delete_node.js | 5 -- modules/actions/delete_relation.js | 6 -- modules/actions/delete_way.js | 15 ----- modules/actions/move.js | 11 ---- modules/actions/reflect.js | 25 +++----- modules/actions/rotate.js | 11 ++-- modules/modes/rotate.js | 2 +- modules/operations/delete.js | 36 +++++++++-- modules/operations/move.js | 16 +++-- modules/operations/reflect.js | 41 +++++++----- modules/operations/rotate.js | 9 +-- test/spec/actions/delete_multiple.js | 19 +++--- test/spec/actions/delete_relation.js | 17 ++--- test/spec/actions/delete_way.js | 51 +++++++-------- test/spec/actions/move.js | 43 +++++++------ test/spec/actions/reflect.js | 25 ++------ 19 files changed, 298 insertions(+), 214 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index afc6b6344..e614c9fd0 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -77,7 +77,9 @@ en: connected_to_hidden: This line can't be straightened because it is connected to a hidden feature. delete: title: Delete - description: Delete object permanently. + description: + single: Delete this object permanently. + multiple: Delete these objects permanently. annotation: point: Deleted a point. vertex: Deleted a node from a way. @@ -85,9 +87,15 @@ en: area: Deleted an area. relation: Deleted a relation. multiple: "Deleted {n} objects." - incomplete_relation: This feature can't be deleted because it hasn't been fully downloaded. - part_of_relation: This feature can't be deleted because it's part of a larger relation. You must remove it from the relation first. - connected_to_hidden: This can't be deleted because it is connected to a hidden feature. + incomplete_relation: + single: This object can't be deleted because it hasn't been fully downloaded. + multiple: These objects can't be deleted because they haven't been fully downloaded. + part_of_relation: + single: This object can't be deleted because it is part of a larger relation. You must remove it from the relation first. + multiple: These objects can't be deleted because they are part of larger relations. You must remove them from the relations first. + connected_to_hidden: + single: This object can't be deleted because it is connected to a hidden feature. + multiple: These objects can't be deleted because some are connected to hidden features. add_member: annotation: Added a member to a relation. delete_member: @@ -118,7 +126,9 @@ en: conflicting_tags: These features can't be merged because some of their tags have conflicting values. move: title: Move - description: Move this to a different location. + description: + single: Move this object to a different location. + multiple: Move these objects to a different location. key: M annotation: point: Moved a point. @@ -126,31 +136,62 @@ en: line: Moved a line. area: Moved an area. multiple: Moved multiple objects. - incomplete_relation: This feature can't be moved because it hasn't been fully downloaded. - too_large: This can't be moved because not enough of it is currently visible. - connected_to_hidden: This can't be moved because it is connected to a hidden feature. + incomplete_relation: + single: This object can't be moved because it hasn't been fully downloaded. + multiple: These objects can't be moved because they haven't been fully downloaded. + too_large: + single: This object can't be moved because not enough of it is currently visible. + multiple: These objects can't be moved because not enough of them are currently visible. + connected_to_hidden: + single: This object can't be moved because it is connected to a hidden feature. + multiple: These objects can't be moved because some are connected to hidden features. reflect: title: reflect description: - long: Reflect this object across its long axis. - short: Reflect this object across its short axis. + long: + single: Reflect this object across its long axis. + multiple: Reflect these objects across their long axis. + short: + single: Reflect this object across its short axis. + multiple: Reflect these objects across their short axis. key: long: T short: Y annotation: - long: Reflected an area across its long axis. - short: Reflected an area across its short axis. - too_large: This can't be reflected because not enough of it is currently visible. - connected_to_hidden: This can't be reflected because it is connected to a hidden feature. + long: + area: Reflected an area across its long axis. + multiple: Reflected multiple objects across their long axis. + short: + area: Reflected an area across its short axis. + multiple: Reflected multiple objects across their short axis. + incomplete_relation: + single: This object can't be reflected because it hasn't been fully downloaded. + multiple: These objects can't be reflected because they haven't been fully downloaded. + too_large: + single: This object can't be reflected because not enough of it is currently visible. + multiple: These objects can't be reflected because not enough of them are currently visible. + connected_to_hidden: + single: This object can't be reflected because it is connected to a hidden feature. + multiple: These objects can't be reflected because some are connected to hidden features. rotate: title: Rotate - description: Rotate this object around its center point. + description: + single: Rotate this object around its center point. + multiple: Rotate these objects around their center point. key: R annotation: line: Rotated a line. area: Rotated an area. - too_large: This can't be rotated because not enough of it is currently visible. - connected_to_hidden: This can't be rotated because it is connected to a hidden feature. + multiple: Rotated multiple objects. + incomplete_relation: + single: This object can't be rotated because it hasn't been fully downloaded. + multiple: These objects can't be rotated because they haven't been fully downloaded. + too_large: + single: This object can't be rotated because not enough of it is currently visible. + multiple: These objects can't be rotated because not enough of them are currently visible. + connected_to_hidden: + single: This object can't be rotated because it is connected to a hidden feature. + multiple: These objects can't be rotated because some are connected to hidden features. reverse: title: Reverse description: Make this line go in the opposite direction. diff --git a/dist/locales/en.json b/dist/locales/en.json index 40322b142..b50995a57 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -101,7 +101,10 @@ }, "delete": { "title": "Delete", - "description": "Delete object permanently.", + "description": { + "single": "Delete this object permanently.", + "multiple": "Delete these objects permanently." + }, "annotation": { "point": "Deleted a point.", "vertex": "Deleted a node from a way.", @@ -110,9 +113,18 @@ "relation": "Deleted a relation.", "multiple": "Deleted {n} objects." }, - "incomplete_relation": "This feature can't be deleted because it hasn't been fully downloaded.", - "part_of_relation": "This feature can't be deleted because it's part of a larger relation. You must remove it from the relation first.", - "connected_to_hidden": "This can't be deleted because it is connected to a hidden feature." + "incomplete_relation": { + "single": "This object can't be deleted because it hasn't been fully downloaded.", + "multiple": "These objects can't be deleted because they haven't been fully downloaded." + }, + "part_of_relation": { + "single": "This object can't be deleted because it is part of a larger relation. You must remove it from the relation first.", + "multiple": "These objects can't be deleted because they are part of larger relations. You must remove them from the relations first." + }, + "connected_to_hidden": { + "single": "This object can't be deleted because it is connected to a hidden feature.", + "multiple": "These objects can't be deleted because some are connected to hidden features." + } }, "add_member": { "annotation": "Added a member to a relation." @@ -150,7 +162,10 @@ }, "move": { "title": "Move", - "description": "Move this to a different location.", + "description": { + "single": "Move this object to a different location.", + "multiple": "Move these objects to a different location." + }, "key": "M", "annotation": { "point": "Moved a point.", @@ -159,37 +174,82 @@ "area": "Moved an area.", "multiple": "Moved multiple objects." }, - "incomplete_relation": "This feature can't be moved because it hasn't been fully downloaded.", - "too_large": "This can't be moved because not enough of it is currently visible.", - "connected_to_hidden": "This can't be moved because it is connected to a hidden feature." + "incomplete_relation": { + "single": "This object can't be moved because it hasn't been fully downloaded.", + "multiple": "These objects can't be moved because they haven't been fully downloaded." + }, + "too_large": { + "single": "This object can't be moved because not enough of it is currently visible.", + "multiple": "These objects can't be moved because not enough of them are currently visible." + }, + "connected_to_hidden": { + "single": "This object can't be moved because it is connected to a hidden feature.", + "multiple": "These objects can't be moved because some are connected to hidden features." + } }, "reflect": { "title": "reflect", "description": { - "long": "Reflect this object across its long axis.", - "short": "Reflect this object across its short axis." + "long": { + "single": "Reflect this object across its long axis.", + "multiple": "Reflect these objects across their long axis." + }, + "short": { + "single": "Reflect this object across its short axis.", + "multiple": "Reflect these objects across their short axis." + } }, "key": { "long": "T", "short": "Y" }, "annotation": { - "long": "Reflected an area across its long axis.", - "short": "Reflected an area across its short axis." + "long": { + "area": "Reflected an area across its long axis.", + "multiple": "Reflected multiple objects across their long axis." + }, + "short": { + "area": "Reflected an area across its short axis.", + "multiple": "Reflected multiple objects across their short axis." + } }, - "too_large": "This can't be reflected because not enough of it is currently visible.", - "connected_to_hidden": "This can't be reflected because it is connected to a hidden feature." + "incomplete_relation": { + "single": "This object can't be reflected because it hasn't been fully downloaded.", + "multiple": "These objects can't be reflected because they haven't been fully downloaded." + }, + "too_large": { + "single": "This object can't be reflected because not enough of it is currently visible.", + "multiple": "These objects can't be reflected because not enough of them are currently visible." + }, + "connected_to_hidden": { + "single": "This object can't be reflected because it is connected to a hidden feature.", + "multiple": "These objects can't be reflected because some are connected to hidden features." + } }, "rotate": { "title": "Rotate", - "description": "Rotate this object around its center point.", + "description": { + "single": "Rotate this object around its center point.", + "multiple": "Rotate these objects around their center point." + }, "key": "R", "annotation": { "line": "Rotated a line.", - "area": "Rotated an area." + "area": "Rotated an area.", + "multiple": "Rotated multiple objects." }, - "too_large": "This can't be rotated because not enough of it is currently visible.", - "connected_to_hidden": "This can't be rotated because it is connected to a hidden feature." + "incomplete_relation": { + "single": "This object can't be rotated because it hasn't been fully downloaded.", + "multiple": "These objects can't be rotated because they haven't been fully downloaded." + }, + "too_large": { + "single": "This object can't be rotated because not enough of it is currently visible.", + "multiple": "These objects can't be rotated because not enough of them are currently visible." + }, + "connected_to_hidden": { + "single": "This object can't be rotated because it is connected to a hidden feature.", + "multiple": "These objects can't be rotated because some are connected to hidden features." + } }, "reverse": { "title": "Reverse", diff --git a/modules/actions/delete_multiple.js b/modules/actions/delete_multiple.js index db90d53ae..c9208d8c1 100644 --- a/modules/actions/delete_multiple.js +++ b/modules/actions/delete_multiple.js @@ -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; } diff --git a/modules/actions/delete_node.js b/modules/actions/delete_node.js index f8d9d99c3..402fe0cfe 100644 --- a/modules/actions/delete_node.js +++ b/modules/actions/delete_node.js @@ -31,10 +31,5 @@ export function actionDeleteNode(nodeId) { }; - action.disabled = function() { - return false; - }; - - return action; } diff --git a/modules/actions/delete_relation.js b/modules/actions/delete_relation.js index cb74101a6..0e0072682 100644 --- a/modules/actions/delete_relation.js +++ b/modules/actions/delete_relation.js @@ -39,11 +39,5 @@ export function actionDeleteRelation(relationId) { }; - action.disabled = function(graph) { - if (!graph.entity(relationId).isComplete(graph)) - return 'incomplete_relation'; - }; - - return action; } diff --git a/modules/actions/delete_way.js b/modules/actions/delete_way.js index 1917cd765..77905ae63 100644 --- a/modules/actions/delete_way.js +++ b/modules/actions/delete_way.js @@ -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; } diff --git a/modules/actions/move.js b/modules/actions/move.js index 70c203f36..fd44076bc 100644 --- a/modules/actions/move.js +++ b/modules/actions/move.js @@ -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; }; diff --git a/modules/actions/reflect.js b/modules/actions/reflect.js index 056932c58..6382f83c8 100644 --- a/modules/actions/reflect.js +++ b/modules/actions/reflect.js @@ -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], diff --git a/modules/actions/rotate.js b/modules/actions/rotate.js index 8667876f7..751b998c6 100644 --- a/modules/actions/rotate.js +++ b/modules/actions/rotate.js @@ -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))); }); }); diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 00e0e70f3..51a8fff57 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -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, diff --git a/modules/operations/delete.js b/modules/operations/delete.js index ffdcb5a27..ead998bb2 100644 --- a/modules/operations/delete.js +++ b/modules/operations/delete.js @@ -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); }; diff --git a/modules/operations/move.js b/modules/operations/move.js index 2c8fb58d0..7c9a9277a 100644 --- a/modules/operations/move.js +++ b/modules/operations/move.js @@ -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); }; diff --git a/modules/operations/reflect.js b/modules/operations/reflect.js index 7656e6495..413d8226e 100644 --- a/modules/operations/reflect.js +++ b/modules/operations/reflect.js @@ -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); }; diff --git a/modules/operations/rotate.js b/modules/operations/rotate.js index 115e8510e..78e42a9a8 100644 --- a/modules/operations/rotate.js +++ b/modules/operations/rotate.js @@ -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); }; diff --git a/test/spec/actions/delete_multiple.js b/test/spec/actions/delete_multiple.js index 9e834f1d5..4de9a077c 100644 --- a/test/spec/actions/delete_multiple.js +++ b/test/spec/actions/delete_multiple.js @@ -19,13 +19,14 @@ describe('iD.actionDeleteMultiple', function () { expect(graph.hasEntity(n.id)).to.be.undefined; }); - describe('#disabled', function () { - it('returns the result of the first action that is disabled', function () { - var node = iD.Node(), - relation = iD.Relation({members: [{id: 'w'}]}), - graph = iD.Graph([node, relation]), - action = iD.actionDeleteMultiple([node.id, relation.id]); - expect(action.disabled(graph)).to.equal('incomplete_relation'); - }); - }); + // This was moved to operationDelete. We should test operations and move this test there. + // describe('#disabled', function () { + // it('returns the result of the first action that is disabled', function () { + // var node = iD.Node(), + // relation = iD.Relation({members: [{id: 'w'}]}), + // graph = iD.Graph([node, relation]), + // action = iD.actionDeleteMultiple([node.id, relation.id]); + // expect(action.disabled(graph)).to.equal('incomplete_relation'); + // }); + // }); }); diff --git a/test/spec/actions/delete_relation.js b/test/spec/actions/delete_relation.js index 9d0b615b0..acf3fb241 100644 --- a/test/spec/actions/delete_relation.js +++ b/test/spec/actions/delete_relation.js @@ -82,12 +82,13 @@ describe('iD.actionDeleteRelation', function () { expect(graph.hasEntity(parent.id)).to.be.undefined; }); - describe('#disabled', function() { - it('returns \'incomplete_relation\' if the relation is incomplete', function() { - var relation = iD.Relation({members: [{id: 'w'}]}), - graph = iD.Graph([relation]), - action = iD.actionDeleteRelation(relation.id); - expect(action.disabled(graph)).to.equal('incomplete_relation'); - }); - }); + // This was moved to operationDelete. We should test operations and move this test there. + // describe('#disabled', function() { + // it('returns \'incomplete_relation\' if the relation is incomplete', function() { + // var relation = iD.Relation({members: [{id: 'w'}]}), + // graph = iD.Graph([relation]), + // action = iD.actionDeleteRelation(relation.id); + // expect(action.disabled(graph)).to.equal('incomplete_relation'); + // }); + // }); }); diff --git a/test/spec/actions/delete_way.js b/test/spec/actions/delete_way.js index 3bb83b88e..889a08f48 100644 --- a/test/spec/actions/delete_way.js +++ b/test/spec/actions/delete_way.js @@ -69,31 +69,32 @@ describe('iD.actionDeleteWay', function() { expect(graph.hasEntity(relation.id)).to.be.undefined; }); - describe('#disabled', function () { - it('returns \'part_of_relation\' for members of route and boundary relations', function () { - var a = iD.Way({id: 'a'}), - b = iD.Way({id: 'b'}), - route = iD.Relation({members: [{id: 'a'}], tags: {type: 'route'}}), - boundary = iD.Relation({members: [{id: 'b'}], tags: {type: 'boundary'}}), - graph = iD.Graph([a, b, route, boundary]); - expect(iD.actionDeleteWay('a').disabled(graph)).to.equal('part_of_relation'); - expect(iD.actionDeleteWay('b').disabled(graph)).to.equal('part_of_relation'); - }); + // This was moved to operationDelete. We should test operations and move this test there. + // describe('#disabled', function () { + // it('returns \'part_of_relation\' for members of route and boundary relations', function () { + // var a = iD.Way({id: 'a'}), + // b = iD.Way({id: 'b'}), + // route = iD.Relation({members: [{id: 'a'}], tags: {type: 'route'}}), + // boundary = iD.Relation({members: [{id: 'b'}], tags: {type: 'boundary'}}), + // graph = iD.Graph([a, b, route, boundary]); + // expect(iD.actionDeleteWay('a').disabled(graph)).to.equal('part_of_relation'); + // expect(iD.actionDeleteWay('b').disabled(graph)).to.equal('part_of_relation'); + // }); - it('returns \'part_of_relation\' for outer members of multipolygons', function () { - var way = iD.Way({id: 'w'}), - relation = iD.Relation({members: [{id: 'w', role: 'outer'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([way, relation]), - action = iD.actionDeleteWay(way.id); - expect(action.disabled(graph)).to.equal('part_of_relation'); - }); + // it('returns \'part_of_relation\' for outer members of multipolygons', function () { + // var way = iD.Way({id: 'w'}), + // relation = iD.Relation({members: [{id: 'w', role: 'outer'}], tags: {type: 'multipolygon'}}), + // graph = iD.Graph([way, relation]), + // action = iD.actionDeleteWay(way.id); + // expect(action.disabled(graph)).to.equal('part_of_relation'); + // }); - it('returns falsy for inner members of multipolygons', function () { - var way = iD.Way({id: 'w'}), - relation = iD.Relation({members: [{id: 'w', role: 'inner'}], tags: {type: 'multipolygon'}}), - graph = iD.Graph([way, relation]), - action = iD.actionDeleteWay(way.id); - expect(action.disabled(graph)).not.ok; - }); - }); + // it('returns falsy for inner members of multipolygons', function () { + // var way = iD.Way({id: 'w'}), + // relation = iD.Relation({members: [{id: 'w', role: 'inner'}], tags: {type: 'multipolygon'}}), + // graph = iD.Graph([way, relation]), + // action = iD.actionDeleteWay(way.id); + // expect(action.disabled(graph)).not.ok; + // }); + // }); }); diff --git a/test/spec/actions/move.js b/test/spec/actions/move.js index 15230424a..554a44d35 100644 --- a/test/spec/actions/move.js +++ b/test/spec/actions/move.js @@ -1,29 +1,30 @@ describe('iD.actionMove', function() { var projection = d3.geoMercator().scale(250 / Math.PI); - describe('#disabled', function() { - it('returns falsy by default', function() { - var node = iD.Node({loc: [0, 0]}), - action = iD.actionMove([node.id], [0, 0], projection), - graph = iD.Graph([node]); - expect(action.disabled(graph)).not.to.be.ok; - }); + // This was moved to operationMove. We should test operations and move this test there. + // describe('#disabled', function() { + // it('returns falsy by default', function() { + // var node = iD.Node({loc: [0, 0]}), + // action = iD.actionMove([node.id], [0, 0], projection), + // graph = iD.Graph([node]); + // expect(action.disabled(graph)).not.to.be.ok; + // }); - it('returns \'incomplete_relation\' for an incomplete relation', function() { - var relation = iD.Relation({members: [{id: 1}]}), - action = iD.actionMove([relation.id], [0, 0], projection), - graph = iD.Graph([relation]); - expect(action.disabled(graph)).to.equal('incomplete_relation'); - }); + // it('returns \'incomplete_relation\' for an incomplete relation', function() { + // var relation = iD.Relation({members: [{id: 1}]}), + // action = iD.actionMove([relation.id], [0, 0], projection), + // graph = iD.Graph([relation]); + // expect(action.disabled(graph)).to.equal('incomplete_relation'); + // }); - it('returns falsy for a complete relation', function() { - var node = iD.Node({loc: [0, 0]}), - relation = iD.Relation({members: [{id: node.id}]}), - action = iD.actionMove([relation.id], [0, 0], projection), - graph = iD.Graph([node, relation]); - expect(action.disabled(graph)).not.to.be.ok; - }); - }); + // it('returns falsy for a complete relation', function() { + // var node = iD.Node({loc: [0, 0]}), + // relation = iD.Relation({members: [{id: node.id}]}), + // action = iD.actionMove([relation.id], [0, 0], projection), + // graph = iD.Graph([node, relation]); + // expect(action.disabled(graph)).not.to.be.ok; + // }); + // }); it('moves all nodes in a way by the given amount', function() { var node1 = iD.Node({loc: [0, 0]}), diff --git a/test/spec/actions/reflect.js b/test/spec/actions/reflect.js index aaf1b022d..1a8b22061 100644 --- a/test/spec/actions/reflect.js +++ b/test/spec/actions/reflect.js @@ -2,19 +2,6 @@ describe('iD.actionReflect', function() { var projection = d3.geoMercator(); it('does not create or remove nodes', function () { - var graph = iD.Graph([ - iD.Node({id: 'a', loc: [0, 0]}), - iD.Node({id: 'b', loc: [4, 0]}), - iD.Node({id: 'c', loc: [4, 2]}), - iD.Node({id: 'd', loc: [1, 2]}), - iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a'], tags: { area: 'yes'}}) - ]); - graph = iD.actionReflect('-', projection)(graph); - expect(graph.entity('-').nodes).to.have.length(5); - }); - - - it('only operates on areas', function () { var graph = iD.Graph([ iD.Node({id: 'a', loc: [0, 0]}), iD.Node({id: 'b', loc: [4, 0]}), @@ -22,8 +9,8 @@ describe('iD.actionReflect', function() { iD.Node({id: 'd', loc: [1, 2]}), iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) ]); - var graph2 = iD.actionReflect('-', projection)(graph); - expect(graph2).to.deep.equal(graph); + graph = iD.actionReflect(['-'], projection)(graph); + expect(graph.entity('-').nodes).to.have.length(5); }); @@ -38,9 +25,9 @@ describe('iD.actionReflect', function() { iD.Node({id: 'b', loc: [4, 0]}), iD.Node({id: 'c', loc: [4, 2]}), iD.Node({id: 'd', loc: [1, 2]}), - iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a'], tags: { area: 'yes'}}) + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) ]); - graph = iD.actionReflect('-', projection)(graph); + graph = iD.actionReflect(['-'], projection)(graph); expect(graph.entity('a').loc[0]).to.be.closeTo(0, 1e-6); expect(graph.entity('a').loc[1]).to.be.closeTo(2, 1e-6); expect(graph.entity('b').loc[0]).to.be.closeTo(4, 1e-6); @@ -63,9 +50,9 @@ describe('iD.actionReflect', function() { iD.Node({id: 'b', loc: [4, 0]}), iD.Node({id: 'c', loc: [4, 2]}), iD.Node({id: 'd', loc: [1, 2]}), - iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a'], tags: { area: 'yes'}}) + iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) ]); - graph = iD.actionReflect('-', projection).useLongAxis(false)(graph); + graph = iD.actionReflect(['-'], projection).useLongAxis(false)(graph); expect(graph.entity('a').loc[0]).to.be.closeTo(4, 1e-6); expect(graph.entity('a').loc[1]).to.be.closeTo(0, 1e-6); expect(graph.entity('b').loc[0]).to.be.closeTo(0, 1e-6); From 9a922c0731999232bb8e587b0331d8040916024f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 22 Dec 2016 15:16:08 -0500 Subject: [PATCH 14/14] Make Reflect/Rotate unavailable for strictly linear features --- modules/operations/reflect.js | 9 +++++++-- modules/operations/rotate.js | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/operations/reflect.js b/modules/operations/reflect.js index 413d8226e..9a3ffad97 100644 --- a/modules/operations/reflect.js +++ b/modules/operations/reflect.js @@ -31,8 +31,13 @@ export function operationReflect(selectedIDs, context, axis) { operation.available = function() { - return selectedIDs.length > 1 || - context.entity(selectedIDs[0]).type !== 'node'; + return _.some(selectedIDs, hasArea); + + function hasArea(id) { + var entity = context.entity(id); + return (entity.type === 'way' && entity.isClosed()) || + (entity.type ==='relation' && entity.isMultipolygon()); + } }; diff --git a/modules/operations/rotate.js b/modules/operations/rotate.js index 78e42a9a8..aa7359f09 100644 --- a/modules/operations/rotate.js +++ b/modules/operations/rotate.js @@ -18,8 +18,13 @@ export function operationRotate(selectedIDs, context) { operation.available = function() { - return selectedIDs.length > 1 || - context.entity(selectedIDs[0]).type !== 'node'; + return _.some(selectedIDs, hasArea); + + function hasArea(id) { + var entity = context.entity(id); + return (entity.type === 'way' && entity.isClosed()) || + (entity.type ==='relation' && entity.isMultipolygon()); + } };