diff --git a/Makefile b/Makefile index b22d35af0..70f5d558e 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,8 @@ MODULE_TARGETS = \ js/lib/id/presets.js \ js/lib/id/util.js \ js/lib/id/validations.js \ - js/lib/id/geo.js + js/lib/id/geo.js \ + js/lib/id/operations.js js/lib/id/actions.js: modules/ node_modules/.bin/rollup -f umd -n iD.actions modules/actions/index.js --no-strict > $@ @@ -68,6 +69,9 @@ js/lib/id/validations.js: modules/ js/lib/id/geo.js: modules/ node_modules/.bin/rollup -f umd -n iD.geo modules/geo/index.js --no-strict > $@ +js/lib/id/operations.js: modules/ + node_modules/.bin/rollup -f umd -n iD.operations modules/operations/index.js --no-strict > $@ + dist/iD.js: \ js/lib/bootstrap-tooltip.js \ js/lib/d3.v3.js \ @@ -111,18 +115,6 @@ dist/iD.js: \ js/id/behavior/paste.js \ js/id/behavior/select.js \ js/id/behavior/tail.js \ - js/id/operations.js \ - js/id/operations/circularize.js \ - js/id/operations/continue.js \ - js/id/operations/delete.js \ - js/id/operations/disconnect.js \ - js/id/operations/merge.js \ - js/id/operations/move.js \ - js/id/operations/orthogonalize.js \ - js/id/operations/reverse.js \ - js/id/operations/rotate.js \ - js/id/operations/split.js \ - js/id/operations/straighten.js \ js/id/core/connection.js \ js/id/core/difference.js \ js/id/core/entity.js \ diff --git a/index.html b/index.html index c5cec0878..16256a922 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,7 @@ + @@ -158,19 +159,6 @@ - - - - - - - - - - - - - diff --git a/js/id/operations.js b/js/id/operations.js deleted file mode 100644 index a72fe1d82..000000000 --- a/js/id/operations.js +++ /dev/null @@ -1 +0,0 @@ -iD.operations = {}; diff --git a/js/lib/id/operations.js b/js/lib/id/operations.js new file mode 100644 index 000000000..03f0ee699 --- /dev/null +++ b/js/lib/id/operations.js @@ -0,0 +1,540 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.iD = global.iD || {}, global.iD.operations = global.iD.operations || {}))); +}(this, function (exports) { 'use strict'; + + function Circularize(selectedIDs, context) { + var entityId = selectedIDs[0], + entity = context.entity(entityId), + extent = entity.extent(context.graph()), + geometry = context.geometry(entityId), + action = iD.actions.Circularize(entityId, context.projection); + + var operation = function() { + var annotation = t('operations.circularize.annotation.' + geometry); + context.perform(action, annotation); + }; + + operation.available = function() { + return selectedIDs.length === 1 && + entity.type === 'way' && + _.uniq(entity.nodes).length > 1; + }; + + operation.disabled = function() { + var reason; + if (extent.percentContainedIn(context.extent()) < 0.8) { + reason = 'too_large'; + } else if (context.hasHiddenConnections(entityId)) { + reason = 'connected_to_hidden'; + } + return action.disabled(context.graph()) || reason; + }; + + operation.tooltip = function() { + var disable = operation.disabled(); + return disable ? + t('operations.circularize.' + disable) : + t('operations.circularize.description.' + geometry); + }; + + operation.id = 'circularize'; + operation.keys = [t('operations.circularize.key')]; + operation.title = t('operations.circularize.title'); + + return operation; + } + + function Continue(selectedIDs, context) { + var graph = context.graph(), + entities = selectedIDs.map(function(id) { return graph.entity(id); }), + geometries = _.extend({line: [], vertex: []}, + _.groupBy(entities, function(entity) { return entity.geometry(graph); })), + vertex = geometries.vertex[0]; + + function candidateWays() { + return graph.parentWays(vertex).filter(function(parent) { + return parent.geometry(graph) === 'line' && + parent.affix(vertex.id) && + (geometries.line.length === 0 || geometries.line[0] === parent); + }); + } + + var operation = function() { + var candidate = candidateWays()[0]; + context.enter(iD.modes.DrawLine( + context, + candidate.id, + context.graph(), + candidate.affix(vertex.id))); + }; + + operation.available = function() { + return geometries.vertex.length === 1 && geometries.line.length <= 1 && + !context.features().hasHiddenConnections(vertex, context.graph()); + }; + + operation.disabled = function() { + var candidates = candidateWays(); + if (candidates.length === 0) + return 'not_eligible'; + if (candidates.length > 1) + return 'multiple'; + }; + + operation.tooltip = function() { + var disable = operation.disabled(); + return disable ? + t('operations.continue.' + disable) : + t('operations.continue.description'); + }; + + operation.id = 'continue'; + operation.keys = [t('operations.continue.key')]; + operation.title = t('operations.continue.title'); + + return operation; + } + + function Delete(selectedIDs, context) { + var action = iD.actions.DeleteMultiple(selectedIDs); + + var operation = function() { + var annotation, + nextSelectedID; + + if (selectedIDs.length > 1) { + annotation = t('operations.delete.annotation.multiple', {n: selectedIDs.length}); + + } else { + var id = selectedIDs[0], + entity = context.entity(id), + geometry = context.geometry(id), + parents = context.graph().parentWays(entity), + parent = parents[0]; + + annotation = t('operations.delete.annotation.' + geometry); + + // Select the next closest node in the way. + if (geometry === 'vertex' && parents.length === 1 && parent.nodes.length > 2) { + var nodes = parent.nodes, + i = nodes.indexOf(id); + + if (i === 0) { + i++; + } else if (i === nodes.length - 1) { + i--; + } else { + var a = iD.geo.sphericalDistance(entity.loc, context.entity(nodes[i - 1]).loc), + b = iD.geo.sphericalDistance(entity.loc, context.entity(nodes[i + 1]).loc); + i = a < b ? i - 1 : i + 1; + } + + nextSelectedID = nodes[i]; + } + } + + if (nextSelectedID && context.hasEntity(nextSelectedID)) { + context.enter(iD.modes.Select(context, [nextSelectedID])); + } else { + context.enter(iD.modes.Browse(context)); + } + + context.perform( + action, + annotation); + }; + + operation.available = function() { + return true; + }; + + operation.disabled = function() { + var reason; + if (_.some(selectedIDs, context.hasHiddenConnections)) { + reason = 'connected_to_hidden'; + } + return action.disabled(context.graph()) || reason; + }; + + operation.tooltip = function() { + var disable = operation.disabled(); + return disable ? + t('operations.delete.' + disable) : + t('operations.delete.description'); + }; + + operation.id = 'delete'; + operation.keys = [iD.ui.cmd('⌘⌫'), iD.ui.cmd('⌘⌦')]; + operation.title = t('operations.delete.title'); + + return operation; + } + + function Disconnect(selectedIDs, context) { + var vertices = _.filter(selectedIDs, function vertex(entityId) { + return context.geometry(entityId) === 'vertex'; + }); + + var entityId = vertices[0], + action = iD.actions.Disconnect(entityId); + + if (selectedIDs.length > 1) { + action.limitWays(_.without(selectedIDs, entityId)); + } + + var operation = function() { + context.perform(action, t('operations.disconnect.annotation')); + }; + + operation.available = function() { + return vertices.length === 1; + }; + + operation.disabled = function() { + var reason; + if (_.some(selectedIDs, context.hasHiddenConnections)) { + reason = 'connected_to_hidden'; + } + return action.disabled(context.graph()) || reason; + }; + + operation.tooltip = function() { + var disable = operation.disabled(); + return disable ? + t('operations.disconnect.' + disable) : + t('operations.disconnect.description'); + }; + + operation.id = 'disconnect'; + operation.keys = [t('operations.disconnect.key')]; + operation.title = t('operations.disconnect.title'); + + return operation; + } + + function Merge(selectedIDs, context) { + var join = iD.actions.Join(selectedIDs), + merge = iD.actions.Merge(selectedIDs), + mergePolygon = iD.actions.MergePolygon(selectedIDs); + + var operation = function() { + var annotation = t('operations.merge.annotation', {n: selectedIDs.length}), + action; + + if (!join.disabled(context.graph())) { + action = join; + } else if (!merge.disabled(context.graph())) { + action = merge; + } else { + action = mergePolygon; + } + + context.perform(action, annotation); + context.enter(iD.modes.Select(context, selectedIDs.filter(function(id) { return context.hasEntity(id); })) + .suppressMenu(true)); + }; + + operation.available = function() { + return selectedIDs.length >= 2; + }; + + operation.disabled = function() { + return join.disabled(context.graph()) && + merge.disabled(context.graph()) && + mergePolygon.disabled(context.graph()); + }; + + operation.tooltip = function() { + var j = join.disabled(context.graph()), + m = merge.disabled(context.graph()), + p = mergePolygon.disabled(context.graph()); + + if (j === 'restriction' && m && p) + return t('operations.merge.restriction', {relation: context.presets().item('type/restriction').name()}); + + if (p === 'incomplete_relation' && j && m) + return t('operations.merge.incomplete_relation'); + + if (j && m && p) + return t('operations.merge.' + j); + + return t('operations.merge.description'); + }; + + operation.id = 'merge'; + operation.keys = [t('operations.merge.key')]; + operation.title = t('operations.merge.title'); + + return operation; + } + + function Move(selectedIDs, context) { + var extent = selectedIDs.reduce(function(extent, id) { + return extent.extend(context.entity(id).extent(context.graph())); + }, iD.geo.Extent()); + + var operation = function() { + context.enter(iD.modes.Move(context, selectedIDs)); + }; + + operation.available = function() { + return selectedIDs.length > 1 || + context.entity(selectedIDs[0]).type !== 'node'; + }; + + operation.disabled = function() { + var reason; + if (extent.area() && extent.percentContainedIn(context.extent()) < 0.8) { + reason = 'too_large'; + } else if (_.some(selectedIDs, context.hasHiddenConnections)) { + reason = 'connected_to_hidden'; + } + return iD.actions.Move(selectedIDs).disabled(context.graph()) || reason; + }; + + operation.tooltip = function() { + var disable = operation.disabled(); + return disable ? + t('operations.move.' + disable) : + t('operations.move.description'); + }; + + operation.id = 'move'; + operation.keys = [t('operations.move.key')]; + operation.title = t('operations.move.title'); + + return operation; + } + + function Orthogonalize(selectedIDs, context) { + var entityId = selectedIDs[0], + entity = context.entity(entityId), + extent = entity.extent(context.graph()), + geometry = context.geometry(entityId), + action = iD.actions.Orthogonalize(entityId, context.projection); + + var operation = function() { + var annotation = t('operations.orthogonalize.annotation.' + geometry); + context.perform(action, annotation); + }; + + operation.available = function() { + return selectedIDs.length === 1 && + entity.type === 'way' && + entity.isClosed() && + _.uniq(entity.nodes).length > 2; + }; + + operation.disabled = function() { + var reason; + if (extent.percentContainedIn(context.extent()) < 0.8) { + reason = 'too_large'; + } else if (context.hasHiddenConnections(entityId)) { + reason = 'connected_to_hidden'; + } + return action.disabled(context.graph()) || reason; + }; + + operation.tooltip = function() { + var disable = operation.disabled(); + return disable ? + t('operations.orthogonalize.' + disable) : + t('operations.orthogonalize.description.' + geometry); + }; + + operation.id = 'orthogonalize'; + operation.keys = [t('operations.orthogonalize.key')]; + operation.title = t('operations.orthogonalize.title'); + + return operation; + } + + function Reverse(selectedIDs, context) { + var entityId = selectedIDs[0]; + + var operation = function() { + context.perform( + iD.actions.Reverse(entityId), + t('operations.reverse.annotation')); + }; + + operation.available = function() { + return selectedIDs.length === 1 && + context.geometry(entityId) === 'line'; + }; + + operation.disabled = function() { + return false; + }; + + operation.tooltip = function() { + return t('operations.reverse.description'); + }; + + operation.id = 'reverse'; + operation.keys = [t('operations.reverse.key')]; + operation.title = t('operations.reverse.title'); + + return operation; + } + + function Rotate(selectedIDs, context) { + var entityId = selectedIDs[0], + entity = context.entity(entityId), + extent = entity.extent(context.graph()), + geometry = context.geometry(entityId); + + var operation = function() { + context.enter(iD.modes.RotateWay(context, entityId)); + }; + + 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; + }; + + 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; + } + }; + + operation.tooltip = function() { + var disable = operation.disabled(); + return disable ? + t('operations.rotate.' + disable) : + t('operations.rotate.description'); + }; + + operation.id = 'rotate'; + operation.keys = [t('operations.rotate.key')]; + operation.title = t('operations.rotate.title'); + + return operation; + } + + function Split(selectedIDs, context) { + var vertices = _.filter(selectedIDs, function vertex(entityId) { + return context.geometry(entityId) === 'vertex'; + }); + + var entityId = vertices[0], + action = iD.actions.Split(entityId); + + if (selectedIDs.length > 1) { + action.limitWays(_.without(selectedIDs, entityId)); + } + + var operation = function() { + var annotation; + + var ways = action.ways(context.graph()); + if (ways.length === 1) { + annotation = t('operations.split.annotation.' + context.geometry(ways[0].id)); + } else { + annotation = t('operations.split.annotation.multiple', {n: ways.length}); + } + + var difference = context.perform(action, annotation); + context.enter(iD.modes.Select(context, difference.extantIDs())); + }; + + operation.available = function() { + return vertices.length === 1; + }; + + operation.disabled = function() { + var reason; + if (_.some(selectedIDs, context.hasHiddenConnections)) { + reason = 'connected_to_hidden'; + } + return action.disabled(context.graph()) || reason; + }; + + operation.tooltip = function() { + var disable = operation.disabled(); + if (disable) { + return t('operations.split.' + disable); + } + + var ways = action.ways(context.graph()); + if (ways.length === 1) { + return t('operations.split.description.' + context.geometry(ways[0].id)); + } else { + return t('operations.split.description.multiple'); + } + }; + + operation.id = 'split'; + operation.keys = [t('operations.split.key')]; + operation.title = t('operations.split.title'); + + return operation; + } + + function Straighten(selectedIDs, context) { + var entityId = selectedIDs[0], + action = iD.actions.Straighten(entityId, context.projection); + + function operation() { + var annotation = t('operations.straighten.annotation'); + context.perform(action, annotation); + } + + operation.available = function() { + var entity = context.entity(entityId); + return selectedIDs.length === 1 && + entity.type === 'way' && + !entity.isClosed() && + _.uniq(entity.nodes).length > 2; + }; + + operation.disabled = function() { + var reason; + if (context.hasHiddenConnections(entityId)) { + reason = 'connected_to_hidden'; + } + return action.disabled(context.graph()) || reason; + }; + + operation.tooltip = function() { + var disable = operation.disabled(); + return disable ? + t('operations.straighten.' + disable) : + t('operations.straighten.description'); + }; + + operation.id = 'straighten'; + operation.keys = [t('operations.straighten.key')]; + operation.title = t('operations.straighten.title'); + + return operation; + } + + exports.Circularize = Circularize; + exports.Continue = Continue; + exports.Delete = Delete; + exports.Disconnect = Disconnect; + exports.Merge = Merge; + exports.Move = Move; + exports.Orthogonalize = Orthogonalize; + exports.Reverse = Reverse; + exports.Rotate = Rotate; + exports.Split = Split; + exports.Straighten = Straighten; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); \ No newline at end of file diff --git a/js/id/operations/circularize.js b/modules/operations/circularize.js similarity index 95% rename from js/id/operations/circularize.js rename to modules/operations/circularize.js index f15a11d8f..e614fbafd 100644 --- a/js/id/operations/circularize.js +++ b/modules/operations/circularize.js @@ -1,4 +1,4 @@ -iD.operations.Circularize = function(selectedIDs, context) { +export function Circularize(selectedIDs, context) { var entityId = selectedIDs[0], entity = context.entity(entityId), extent = entity.extent(context.graph()), @@ -38,4 +38,4 @@ iD.operations.Circularize = function(selectedIDs, context) { operation.title = t('operations.circularize.title'); return operation; -}; +} diff --git a/js/id/operations/continue.js b/modules/operations/continue.js similarity index 96% rename from js/id/operations/continue.js rename to modules/operations/continue.js index 642458721..bd412eedc 100644 --- a/js/id/operations/continue.js +++ b/modules/operations/continue.js @@ -1,4 +1,4 @@ -iD.operations.Continue = function(selectedIDs, context) { +export function Continue(selectedIDs, context) { var graph = context.graph(), entities = selectedIDs.map(function(id) { return graph.entity(id); }), geometries = _.extend({line: [], vertex: []}, @@ -47,4 +47,4 @@ iD.operations.Continue = function(selectedIDs, context) { operation.title = t('operations.continue.title'); return operation; -}; +} diff --git a/js/id/operations/delete.js b/modules/operations/delete.js similarity index 97% rename from js/id/operations/delete.js rename to modules/operations/delete.js index 6ead83914..0eb33e201 100644 --- a/js/id/operations/delete.js +++ b/modules/operations/delete.js @@ -1,4 +1,4 @@ -iD.operations.Delete = function(selectedIDs, context) { +export function Delete(selectedIDs, context) { var action = iD.actions.DeleteMultiple(selectedIDs); var operation = function() { @@ -71,4 +71,4 @@ iD.operations.Delete = function(selectedIDs, context) { operation.title = t('operations.delete.title'); return operation; -}; +} diff --git a/js/id/operations/disconnect.js b/modules/operations/disconnect.js similarity index 94% rename from js/id/operations/disconnect.js rename to modules/operations/disconnect.js index f63b42c72..46b3db69a 100644 --- a/js/id/operations/disconnect.js +++ b/modules/operations/disconnect.js @@ -1,4 +1,4 @@ -iD.operations.Disconnect = function(selectedIDs, context) { +export function Disconnect(selectedIDs, context) { var vertices = _.filter(selectedIDs, function vertex(entityId) { return context.geometry(entityId) === 'vertex'; }); @@ -38,4 +38,4 @@ iD.operations.Disconnect = function(selectedIDs, context) { operation.title = t('operations.disconnect.title'); return operation; -}; +} diff --git a/modules/operations/index.js b/modules/operations/index.js new file mode 100644 index 000000000..58190dbea --- /dev/null +++ b/modules/operations/index.js @@ -0,0 +1,11 @@ +export { Circularize } from './circularize'; +export { Continue } from './continue'; +export { Delete } from './delete'; +export { Disconnect } from './disconnect'; +export { Merge } from './merge'; +export { Move } from './move'; +export { Orthogonalize } from './orthogonalize'; +export { Reverse } from './reverse'; +export { Rotate } from './rotate'; +export { Split } from './split'; +export { Straighten } from './straighten'; diff --git a/js/id/operations/merge.js b/modules/operations/merge.js similarity index 96% rename from js/id/operations/merge.js rename to modules/operations/merge.js index c8bc60b11..6f5370ccd 100644 --- a/js/id/operations/merge.js +++ b/modules/operations/merge.js @@ -1,4 +1,4 @@ -iD.operations.Merge = function(selectedIDs, context) { +export function Merge(selectedIDs, context) { var join = iD.actions.Join(selectedIDs), merge = iD.actions.Merge(selectedIDs), mergePolygon = iD.actions.MergePolygon(selectedIDs); @@ -52,4 +52,4 @@ iD.operations.Merge = function(selectedIDs, context) { operation.title = t('operations.merge.title'); return operation; -}; +} diff --git a/js/id/operations/move.js b/modules/operations/move.js similarity index 95% rename from js/id/operations/move.js rename to modules/operations/move.js index 8225d0f45..b65b1b891 100644 --- a/js/id/operations/move.js +++ b/modules/operations/move.js @@ -1,4 +1,4 @@ -iD.operations.Move = function(selectedIDs, context) { +export function Move(selectedIDs, context) { var extent = selectedIDs.reduce(function(extent, id) { return extent.extend(context.entity(id).extent(context.graph())); }, iD.geo.Extent()); @@ -34,4 +34,4 @@ iD.operations.Move = function(selectedIDs, context) { operation.title = t('operations.move.title'); return operation; -}; +} diff --git a/js/id/operations/orthogonalize.js b/modules/operations/orthogonalize.js similarity index 95% rename from js/id/operations/orthogonalize.js rename to modules/operations/orthogonalize.js index 851f3b7a9..7b1aa1a58 100644 --- a/js/id/operations/orthogonalize.js +++ b/modules/operations/orthogonalize.js @@ -1,4 +1,4 @@ -iD.operations.Orthogonalize = function(selectedIDs, context) { +export function Orthogonalize(selectedIDs, context) { var entityId = selectedIDs[0], entity = context.entity(entityId), extent = entity.extent(context.graph()), @@ -39,4 +39,4 @@ iD.operations.Orthogonalize = function(selectedIDs, context) { operation.title = t('operations.orthogonalize.title'); return operation; -}; +} diff --git a/js/id/operations/reverse.js b/modules/operations/reverse.js similarity index 91% rename from js/id/operations/reverse.js rename to modules/operations/reverse.js index afb926215..9197d7249 100644 --- a/js/id/operations/reverse.js +++ b/modules/operations/reverse.js @@ -1,4 +1,4 @@ -iD.operations.Reverse = function(selectedIDs, context) { +export function Reverse(selectedIDs, context) { var entityId = selectedIDs[0]; var operation = function() { @@ -25,4 +25,4 @@ iD.operations.Reverse = function(selectedIDs, context) { operation.title = t('operations.reverse.title'); return operation; -}; +} diff --git a/js/id/operations/rotate.js b/modules/operations/rotate.js similarity index 95% rename from js/id/operations/rotate.js rename to modules/operations/rotate.js index 485d05d24..ea50d128c 100644 --- a/js/id/operations/rotate.js +++ b/modules/operations/rotate.js @@ -1,4 +1,4 @@ -iD.operations.Rotate = function(selectedIDs, context) { +export function Rotate(selectedIDs, context) { var entityId = selectedIDs[0], entity = context.entity(entityId), extent = entity.extent(context.graph()), @@ -41,4 +41,4 @@ iD.operations.Rotate = function(selectedIDs, context) { operation.title = t('operations.rotate.title'); return operation; -}; +} diff --git a/js/id/operations/split.js b/modules/operations/split.js similarity index 96% rename from js/id/operations/split.js rename to modules/operations/split.js index e40222a8e..a978245ea 100644 --- a/js/id/operations/split.js +++ b/modules/operations/split.js @@ -1,4 +1,4 @@ -iD.operations.Split = function(selectedIDs, context) { +export function Split(selectedIDs, context) { var vertices = _.filter(selectedIDs, function vertex(entityId) { return context.geometry(entityId) === 'vertex'; }); @@ -55,4 +55,4 @@ iD.operations.Split = function(selectedIDs, context) { operation.title = t('operations.split.title'); return operation; -}; +} diff --git a/js/id/operations/straighten.js b/modules/operations/straighten.js similarity index 94% rename from js/id/operations/straighten.js rename to modules/operations/straighten.js index fd0a5e823..9a64d8ac1 100644 --- a/js/id/operations/straighten.js +++ b/modules/operations/straighten.js @@ -1,4 +1,4 @@ -iD.operations.Straighten = function(selectedIDs, context) { +export function Straighten(selectedIDs, context) { var entityId = selectedIDs[0], action = iD.actions.Straighten(entityId, context.projection); @@ -35,4 +35,4 @@ iD.operations.Straighten = function(selectedIDs, context) { operation.title = t('operations.straighten.title'); return operation; -}; +} diff --git a/test/index.html b/test/index.html index 84a434dd4..01c41b4f3 100644 --- a/test/index.html +++ b/test/index.html @@ -47,6 +47,7 @@ + @@ -143,19 +144,6 @@ - - - - - - - - - - - - -