diff --git a/Makefile b/Makefile
index 92f988eb1..ec12b7a05 100644
--- a/Makefile
+++ b/Makefile
@@ -41,6 +41,15 @@ BUILDJS_SOURCES = \
$(BUILDJS_TARGETS): $(BUILDJS_SOURCES) build.js
node build.js
+
+MODULE_TARGETS = \
+ js/lib/id/actions.js
+
+ACTIONS = $(shell ./node_modules/.bin/browserify --list modules/actions/index.js)
+js/lib/id/actions.js: $(ACTIONS)
+ node_modules/.bin/browserify modules/actions/index.js -s iD.actions > $@
+
+
dist/iD.js: \
js/lib/bootstrap-tooltip.js \
js/lib/d3.v3.js \
@@ -63,6 +72,7 @@ dist/iD.js: \
js/lib/marked.js \
js/id/start.js \
js/id/id.js \
+ $(MODULE_TARGETS) \
js/id/services.js \
js/id/services/mapillary.js \
js/id/services/nominatim.js \
@@ -77,7 +87,6 @@ dist/iD.js: \
js/id/geo/intersection.js \
js/id/geo/multipolygon.js \
js/id/geo/raw_mercator.js \
- dist/modules/actions.js \
js/id/behavior.js \
js/id/behavior/add_way.js \
js/id/behavior/breathe.js \
@@ -245,9 +254,6 @@ node_modules/.install: package.json
npm install
touch node_modules/.install
-clean:
- rm -f $(BUILDJS_TARGETS) data/feature-icons.json dist/iD*.js dist/iD.css dist/img/*.svg
-
translations:
node data/update_locales
@@ -285,9 +291,8 @@ d3:
node_modules/.bin/smash $(D3_FILES) > js/lib/d3.v3.js
@echo 'd3 rebuilt. Please reapply 7e2485d, 4da529f, 223974d and 71a3d3e'
-ACTIONS = $(shell ./node_modules/.bin/browserify --list modules/actions/index.js)
-dist/modules/actions.js: $(ACTIONS)
- node_modules/.bin/browserify modules/actions/index.js -s iD.actions > dist/modules/actions.js
-
lodash:
node_modules/.bin/lodash --development --output js/lib/lodash.js include="includes,toPairs,assign,bind,chunk,clone,compact,debounce,difference,each,every,extend,filter,find,first,forEach,forOwn,groupBy,indexOf,intersection,isEmpty,isEqual,isFunction,keys,last,map,omit,reject,some,throttle,union,uniq,values,without,flatten,value,chain,cloneDeep,merge,pick,reduce" exports="global,node"
+
+clean:
+ rm -f $(BUILDJS_TARGETS) $(MODULE_TARGETS) data/feature-icons.json dist/iD*.js dist/iD.css dist/img/*.svg
diff --git a/index.html b/index.html
index 57dfc6161..aadbb355c 100644
--- a/index.html
+++ b/index.html
@@ -32,7 +32,9 @@
+
+
@@ -146,8 +148,6 @@
-
-
diff --git a/dist/modules/.gitkeep b/js/lib/id/.gitkeep
similarity index 100%
rename from dist/modules/.gitkeep
rename to js/lib/id/.gitkeep
diff --git a/js/lib/id/actions.js b/js/lib/id/actions.js
new file mode 100644
index 000000000..60ba923de
--- /dev/null
+++ b/js/lib/id/actions.js
@@ -0,0 +1,2236 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}(g.iD || (g.iD = {})).actions = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 2; j++) {
+ if (segment[j] !== member)
+ continue;
+
+ if (j === 0) {
+ memberIndex = segment[j + 1].index;
+ } else if (j === segment.length - 1) {
+ memberIndex = segment[j - 1].index + 1;
+ } else {
+ memberIndex = Math.min(segment[j - 1].index + 1, segment[j + 1].index + 1);
+ }
+ }
+ }
+ }
+
+ return graph.replace(relation.addMember(member, memberIndex));
+ };
+};
+
+},{}],3:[function(require,module,exports){
+module.exports = function(midpoint, node) {
+ return function(graph) {
+ graph = graph.replace(node.move(midpoint.loc));
+
+ var parents = _.intersection(
+ graph.parentWays(graph.entity(midpoint.edge[0])),
+ graph.parentWays(graph.entity(midpoint.edge[1])));
+
+ parents.forEach(function(way) {
+ for (var i = 0; i < way.nodes.length - 1; i++) {
+ if (iD.geo.edgeEqual([way.nodes[i], way.nodes[i + 1]], midpoint.edge)) {
+ graph = graph.replace(graph.entity(way.id).addNode(node.id, i + 1));
+
+ // Add only one midpoint on doubled-back segments,
+ // turning them into self-intersections.
+ return;
+ }
+ }
+ });
+
+ return graph;
+ };
+};
+
+},{}],4:[function(require,module,exports){
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as
+module.exports = function(wayId, nodeId, index) {
+ return function(graph) {
+ return graph.replace(graph.entity(wayId).addNode(nodeId, index));
+ };
+};
+
+},{}],5:[function(require,module,exports){
+module.exports = function(relationId, member, memberIndex) {
+ return function(graph) {
+ return graph.replace(graph.entity(relationId).updateMember(member, memberIndex));
+ };
+};
+
+},{}],6:[function(require,module,exports){
+module.exports = function(entityId, oldPreset, newPreset) {
+ return function(graph) {
+ var entity = graph.entity(entityId),
+ geometry = entity.geometry(graph),
+ tags = entity.tags;
+
+ if (oldPreset) tags = oldPreset.removeTags(tags, geometry);
+ if (newPreset) tags = newPreset.applyTags(tags, geometry);
+
+ return graph.replace(entity.update({tags: tags}));
+ };
+};
+
+},{}],7:[function(require,module,exports){
+module.exports = function(entityId, tags) {
+ return function(graph) {
+ var entity = graph.entity(entityId);
+ return graph.replace(entity.update({tags: tags}));
+ };
+};
+
+},{}],8:[function(require,module,exports){
+module.exports = function(wayId, projection, maxAngle) {
+ maxAngle = (maxAngle || 20) * Math.PI / 180;
+
+ var action = function(graph) {
+ var way = graph.entity(wayId);
+
+ if (!way.isConvex(graph)) {
+ graph = action.makeConvex(graph);
+ }
+
+ var nodes = _.uniq(graph.childNodes(way)),
+ keyNodes = nodes.filter(function(n) { return graph.parentWays(n).length !== 1; }),
+ points = nodes.map(function(n) { return projection(n.loc); }),
+ keyPoints = keyNodes.map(function(n) { return projection(n.loc); }),
+ centroid = (points.length === 2) ? iD.geo.interp(points[0], points[1], 0.5) : d3.geom.polygon(points).centroid(),
+ radius = d3.median(points, function(p) { return iD.geo.euclideanDistance(centroid, p); }),
+ sign = d3.geom.polygon(points).area() > 0 ? 1 : -1,
+ ids;
+
+ // we need atleast two key nodes for the algorithm to work
+ if (!keyNodes.length) {
+ keyNodes = [nodes[0]];
+ keyPoints = [points[0]];
+ }
+
+ if (keyNodes.length === 1) {
+ var index = nodes.indexOf(keyNodes[0]),
+ oppositeIndex = Math.floor((index + nodes.length / 2) % nodes.length);
+
+ keyNodes.push(nodes[oppositeIndex]);
+ keyPoints.push(points[oppositeIndex]);
+ }
+
+ // key points and nodes are those connected to the ways,
+ // they are projected onto the circle, inbetween nodes are moved
+ // to constant intervals between key nodes, extra inbetween nodes are
+ // added if necessary.
+ for (var i = 0; i < keyPoints.length; i++) {
+ var nextKeyNodeIndex = (i + 1) % keyNodes.length,
+ startNode = keyNodes[i],
+ endNode = keyNodes[nextKeyNodeIndex],
+ startNodeIndex = nodes.indexOf(startNode),
+ endNodeIndex = nodes.indexOf(endNode),
+ numberNewPoints = -1,
+ indexRange = endNodeIndex - startNodeIndex,
+ distance, totalAngle, eachAngle, startAngle, endAngle,
+ angle, loc, node, j,
+ inBetweenNodes = [];
+
+ if (indexRange < 0) {
+ indexRange += nodes.length;
+ }
+
+ // position this key node
+ distance = iD.geo.euclideanDistance(centroid, keyPoints[i]);
+ if (distance === 0) { distance = 1e-4; }
+ keyPoints[i] = [
+ centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius,
+ centroid[1] + (keyPoints[i][1] - centroid[1]) / distance * radius];
+ graph = graph.replace(keyNodes[i].move(projection.invert(keyPoints[i])));
+
+ // figure out the between delta angle we want to match to
+ startAngle = Math.atan2(keyPoints[i][1] - centroid[1], keyPoints[i][0] - centroid[0]);
+ endAngle = Math.atan2(keyPoints[nextKeyNodeIndex][1] - centroid[1], keyPoints[nextKeyNodeIndex][0] - centroid[0]);
+ totalAngle = endAngle - startAngle;
+
+ // detects looping around -pi/pi
+ if (totalAngle * sign > 0) {
+ totalAngle = -sign * (2 * Math.PI - Math.abs(totalAngle));
+ }
+
+ do {
+ numberNewPoints++;
+ eachAngle = totalAngle / (indexRange + numberNewPoints);
+ } while (Math.abs(eachAngle) > maxAngle);
+
+ // move existing points
+ for (j = 1; j < indexRange; j++) {
+ angle = startAngle + j * eachAngle;
+ loc = projection.invert([
+ centroid[0] + Math.cos(angle)*radius,
+ centroid[1] + Math.sin(angle)*radius]);
+
+ node = nodes[(j + startNodeIndex) % nodes.length].move(loc);
+ graph = graph.replace(node);
+ }
+
+ // add new inbetween nodes if necessary
+ for (j = 0; j < numberNewPoints; j++) {
+ angle = startAngle + (indexRange + j) * eachAngle;
+ loc = projection.invert([
+ centroid[0] + Math.cos(angle) * radius,
+ centroid[1] + Math.sin(angle) * radius]);
+
+ node = iD.Node({loc: loc});
+ graph = graph.replace(node);
+
+ nodes.splice(endNodeIndex + j, 0, node);
+ inBetweenNodes.push(node.id);
+ }
+
+ // Check for other ways that share these keyNodes..
+ // If keyNodes are adjacent in both ways,
+ // we can add inBetween nodes to that shared way too..
+ if (indexRange === 1 && inBetweenNodes.length) {
+ var startIndex1 = way.nodes.lastIndexOf(startNode.id),
+ endIndex1 = way.nodes.lastIndexOf(endNode.id),
+ wayDirection1 = (endIndex1 - startIndex1);
+ if (wayDirection1 < -1) { wayDirection1 = 1; }
+
+ /* eslint-disable no-loop-func */
+ _.each(_.without(graph.parentWays(keyNodes[i]), way), function(sharedWay) {
+ if (sharedWay.areAdjacent(startNode.id, endNode.id)) {
+ var startIndex2 = sharedWay.nodes.lastIndexOf(startNode.id),
+ endIndex2 = sharedWay.nodes.lastIndexOf(endNode.id),
+ wayDirection2 = (endIndex2 - startIndex2),
+ insertAt = endIndex2;
+ if (wayDirection2 < -1) { wayDirection2 = 1; }
+
+ if (wayDirection1 !== wayDirection2) {
+ inBetweenNodes.reverse();
+ insertAt = startIndex2;
+ }
+ for (j = 0; j < inBetweenNodes.length; j++) {
+ sharedWay = sharedWay.addNode(inBetweenNodes[j], insertAt + j);
+ }
+ graph = graph.replace(sharedWay);
+ }
+ });
+ /* eslint-enable no-loop-func */
+ }
+
+ }
+
+ // update the way to have all the new nodes
+ ids = nodes.map(function(n) { return n.id; });
+ ids.push(ids[0]);
+
+ way = way.update({nodes: ids});
+ graph = graph.replace(way);
+
+ return graph;
+ };
+
+ action.makeConvex = function(graph) {
+ var way = graph.entity(wayId),
+ nodes = _.uniq(graph.childNodes(way)),
+ points = nodes.map(function(n) { return projection(n.loc); }),
+ sign = d3.geom.polygon(points).area() > 0 ? 1 : -1,
+ hull = d3.geom.hull(points);
+
+ // D3 convex hulls go counterclockwise..
+ if (sign === -1) {
+ nodes.reverse();
+ points.reverse();
+ }
+
+ for (var i = 0; i < hull.length - 1; i++) {
+ var startIndex = points.indexOf(hull[i]),
+ endIndex = points.indexOf(hull[i+1]),
+ indexRange = (endIndex - startIndex);
+
+ if (indexRange < 0) {
+ indexRange += nodes.length;
+ }
+
+ // move interior nodes to the surface of the convex hull..
+ for (var j = 1; j < indexRange; j++) {
+ var point = iD.geo.interp(hull[i], hull[i+1], j / indexRange),
+ node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point));
+ graph = graph.replace(node);
+ }
+ }
+ return graph;
+ };
+
+ action.disabled = function(graph) {
+ if (!graph.entity(wayId).isClosed())
+ return 'not_closed';
+ };
+
+ return action;
+};
+
+},{}],9:[function(require,module,exports){
+// Connect the ways at the given nodes.
+//
+// The last node will survive. All other nodes will be replaced with
+// the surviving node in parent ways, and then removed.
+//
+// Tags and relation memberships of of non-surviving nodes are merged
+// to the survivor.
+//
+// This is the inverse of `iD.actions.Disconnect`.
+//
+// Reference:
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeNodesAction.as
+// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/MergeNodesAction.java
+//
+module.exports = function(nodeIds) {
+ return function(graph) {
+ var survivor = graph.entity(_.last(nodeIds));
+
+ for (var i = 0; i < nodeIds.length - 1; i++) {
+ var node = graph.entity(nodeIds[i]);
+
+ /* eslint-disable no-loop-func */
+ graph.parentWays(node).forEach(function(parent) {
+ if (!parent.areAdjacent(node.id, survivor.id)) {
+ graph = graph.replace(parent.replaceNode(node.id, survivor.id));
+ }
+ });
+
+ graph.parentRelations(node).forEach(function(parent) {
+ graph = graph.replace(parent.replaceMember(node, survivor));
+ });
+ /* eslint-enable no-loop-func */
+
+ survivor = survivor.mergeTags(node.tags);
+ graph = iD.actions.DeleteNode(node.id)(graph);
+ }
+
+ graph = graph.replace(survivor);
+
+ return graph;
+ };
+};
+
+},{}],10:[function(require,module,exports){
+module.exports = function(ids, fromGraph) {
+ var copies = {};
+
+ var action = function(graph) {
+ ids.forEach(function(id) {
+ fromGraph.entity(id).copy(fromGraph, copies);
+ });
+
+ for (var id in copies) {
+ graph = graph.replace(copies[id]);
+ }
+
+ return graph;
+ };
+
+ action.copies = function() {
+ return copies;
+ };
+
+ return action;
+};
+
+},{}],11:[function(require,module,exports){
+module.exports = function(relationId, memberIndex) {
+ return function(graph) {
+ var relation = graph.entity(relationId)
+ .removeMember(memberIndex);
+
+ graph = graph.replace(relation);
+
+ if (relation.isDegenerate())
+ graph = iD.actions.DeleteRelation(relation.id)(graph);
+
+ return graph;
+ };
+};
+
+},{}],12:[function(require,module,exports){
+module.exports = function(ids) {
+ var actions = {
+ way: iD.actions.DeleteWay,
+ node: iD.actions.DeleteNode,
+ relation: iD.actions.DeleteRelation
+ };
+
+ var action = function(graph) {
+ ids.forEach(function(id) {
+ if (graph.hasEntity(id)) { // It may have been deleted aready.
+ graph = actions[graph.entity(id).type](id)(graph);
+ }
+ });
+
+ return graph;
+ };
+
+ 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;
+};
+
+},{}],13:[function(require,module,exports){
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteNodeAction.as
+module.exports = function(nodeId) {
+ var action = function(graph) {
+ var node = graph.entity(nodeId);
+
+ graph.parentWays(node)
+ .forEach(function(parent) {
+ parent = parent.removeNode(nodeId);
+ graph = graph.replace(parent);
+
+ if (parent.isDegenerate()) {
+ graph = iD.actions.DeleteWay(parent.id)(graph);
+ }
+ });
+
+ graph.parentRelations(node)
+ .forEach(function(parent) {
+ parent = parent.removeMembersWithID(nodeId);
+ graph = graph.replace(parent);
+
+ if (parent.isDegenerate()) {
+ graph = iD.actions.DeleteRelation(parent.id)(graph);
+ }
+ });
+
+ return graph.remove(node);
+ };
+
+ action.disabled = function() {
+ return false;
+ };
+
+ return action;
+};
+
+},{}],14:[function(require,module,exports){
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteRelationAction.as
+module.exports = function(relationId) {
+ function deleteEntity(entity, graph) {
+ return !graph.parentWays(entity).length &&
+ !graph.parentRelations(entity).length &&
+ !entity.hasInterestingTags();
+ }
+
+ var action = function(graph) {
+ var relation = graph.entity(relationId);
+
+ graph.parentRelations(relation)
+ .forEach(function(parent) {
+ parent = parent.removeMembersWithID(relationId);
+ graph = graph.replace(parent);
+
+ if (parent.isDegenerate()) {
+ graph = iD.actions.DeleteRelation(parent.id)(graph);
+ }
+ });
+
+ _.uniq(_.map(relation.members, 'id')).forEach(function(memberId) {
+ graph = graph.replace(relation.removeMembersWithID(memberId));
+
+ var entity = graph.entity(memberId);
+ if (deleteEntity(entity, graph)) {
+ graph = iD.actions.DeleteMultiple([memberId])(graph);
+ }
+ });
+
+ return graph.remove(relation);
+ };
+
+ action.disabled = function(graph) {
+ if (!graph.entity(relationId).isComplete(graph))
+ return 'incomplete_relation';
+ };
+
+ return action;
+};
+
+},{}],15:[function(require,module,exports){
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteWayAction.as
+module.exports = function(wayId) {
+ function deleteNode(node, graph) {
+ return !graph.parentWays(node).length &&
+ !graph.parentRelations(node).length &&
+ !node.hasInterestingTags();
+ }
+
+ var action = function(graph) {
+ var way = graph.entity(wayId);
+
+ graph.parentRelations(way)
+ .forEach(function(parent) {
+ parent = parent.removeMembersWithID(wayId);
+ graph = graph.replace(parent);
+
+ if (parent.isDegenerate()) {
+ graph = iD.actions.DeleteRelation(parent.id)(graph);
+ }
+ });
+
+ _.uniq(way.nodes).forEach(function(nodeId) {
+ graph = graph.replace(way.removeNode(nodeId));
+
+ var node = graph.entity(nodeId);
+ if (deleteNode(node, graph)) {
+ graph = graph.remove(node);
+ }
+ });
+
+ return graph.remove(way);
+ };
+
+ 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;
+};
+
+},{}],16:[function(require,module,exports){
+module.exports = function(entityId) {
+ return function(graph) {
+ var entity = graph.entity(entityId),
+ newtags = _.clone(entity.tags),
+ change = false,
+ rule;
+
+ // This handles deprecated tags with a single condition
+ for (var i = 0; i < iD.data.deprecated.length; i++) {
+
+ rule = iD.data.deprecated[i];
+ var match = _.toPairs(rule.old)[0],
+ replacements = rule.replace ? _.toPairs(rule.replace) : null;
+
+ if (entity.tags[match[0]] && match[1] === '*') {
+
+ var value = entity.tags[match[0]];
+ if (replacements && !newtags[replacements[0][0]]) {
+ newtags[replacements[0][0]] = value;
+ }
+ delete newtags[match[0]];
+ change = true;
+
+ } else if (entity.tags[match[0]] === match[1]) {
+ newtags = _.assign({}, rule.replace || {}, _.omit(newtags, match[0]));
+ change = true;
+ }
+ }
+
+ if (change) {
+ return graph.replace(entity.update({tags: newtags}));
+ } else {
+ return graph;
+ }
+ };
+};
+
+},{}],17:[function(require,module,exports){
+module.exports = function(difference) {
+ return function(graph) {
+ function discardTags(entity) {
+ if (!_.isEmpty(entity.tags)) {
+ var tags = {};
+ _.each(entity.tags, function(v, k) {
+ if (v) tags[k] = v;
+ });
+
+ graph = graph.replace(entity.update({
+ tags: _.omit(tags, iD.data.discarded)
+ }));
+ }
+ }
+
+ difference.modified().forEach(discardTags);
+ difference.created().forEach(discardTags);
+
+ return graph;
+ };
+};
+
+},{}],18:[function(require,module,exports){
+// Disconect the ways at the given node.
+//
+// Optionally, disconnect only the given ways.
+//
+// For testing convenience, accepts an ID to assign to the (first) new node.
+// Normally, this will be undefined and the way will automatically
+// be assigned a new ID.
+//
+// This is the inverse of `iD.actions.Connect`.
+//
+// Reference:
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/UnjoinNodeAction.as
+// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/UnGlueAction.java
+//
+module.exports = function(nodeId, newNodeId) {
+ var wayIds;
+
+ var action = function(graph) {
+ var node = graph.entity(nodeId),
+ connections = action.connections(graph);
+
+ connections.forEach(function(connection) {
+ var way = graph.entity(connection.wayID),
+ newNode = iD.Node({id: newNodeId, loc: node.loc, tags: node.tags});
+
+ graph = graph.replace(newNode);
+ if (connection.index === 0 && way.isArea()) {
+ // replace shared node with shared node..
+ graph = graph.replace(way.replaceNode(way.nodes[0], newNode.id));
+ } else {
+ // replace shared node with multiple new nodes..
+ graph = graph.replace(way.updateNode(newNode.id, connection.index));
+ }
+ });
+
+ return graph;
+ };
+
+ action.connections = function(graph) {
+ var candidates = [],
+ keeping = false,
+ parentWays = graph.parentWays(graph.entity(nodeId));
+
+ parentWays.forEach(function(way) {
+ if (wayIds && wayIds.indexOf(way.id) === -1) {
+ keeping = true;
+ return;
+ }
+ if (way.isArea() && (way.nodes[0] === nodeId)) {
+ candidates.push({wayID: way.id, index: 0});
+ } else {
+ way.nodes.forEach(function(waynode, index) {
+ if (waynode === nodeId) {
+ candidates.push({wayID: way.id, index: index});
+ }
+ });
+ }
+ });
+
+ return keeping ? candidates : candidates.slice(1);
+ };
+
+ action.disabled = function(graph) {
+ var connections = action.connections(graph);
+ if (connections.length === 0 || (wayIds && wayIds.length !== connections.length))
+ return 'not_connected';
+
+ var parentWays = graph.parentWays(graph.entity(nodeId)),
+ seenRelationIds = {},
+ sharedRelation;
+
+ parentWays.forEach(function(way) {
+ if (wayIds && wayIds.indexOf(way.id) === -1)
+ return;
+
+ var relations = graph.parentRelations(way);
+ relations.forEach(function(relation) {
+ if (relation.id in seenRelationIds) {
+ sharedRelation = relation;
+ } else {
+ seenRelationIds[relation.id] = true;
+ }
+ });
+ });
+
+ if (sharedRelation)
+ return 'relation';
+ };
+
+ action.limitWays = function(_) {
+ if (!arguments.length) return wayIds;
+ wayIds = _;
+ return action;
+ };
+
+ return action;
+};
+
+},{}],19:[function(require,module,exports){
+module.exports.AddEntity = require('./add_entity');
+module.exports.AddMember = require('./add_member');
+module.exports.AddMidpoint = require('./add_midpoint');
+module.exports.AddVertex = require('./add_vertex');
+module.exports.ChangeMember = require('./change_member');
+module.exports.ChangePreset = require('./change_preset');
+module.exports.ChangeTags = require('./change_tags');
+module.exports.Circularize = require('./circularize');
+module.exports.Connect = require('./connect');
+module.exports.CopyEntities = require('./copy_entities');
+module.exports.DeleteMember = require('./delete_member');
+module.exports.DeleteMultiple = require('./delete_multiple');
+module.exports.DeleteNode = require('./delete_node');
+module.exports.DeleteRelation = require('./delete_relation');
+module.exports.DeleteWay = require('./delete_way');
+module.exports.DeprecateTags = require('./deprecate_tags');
+module.exports.DiscardTags = require('./discard_tags');
+module.exports.Disconnect = require('./disconnect');
+module.exports.Join = require('./join');
+module.exports.Merge = require('./merge');
+module.exports.MergePolygon = require('./merge_polygon');
+module.exports.MergeRemoteChanges = require('./merge_remote_changes');
+module.exports.Move = require('./move');
+module.exports.MoveNode = require('./move_node');
+module.exports.Noop = require('./noop');
+module.exports.Orthogonalize = require('./orthogonalize');
+module.exports.RestrictTurn = require('./restrict_turn');
+module.exports.Reverse = require('./reverse');
+module.exports.Revert = require('./revert');
+module.exports.RotateWay = require('./rotate_way');
+module.exports.Split = require('./split');
+module.exports.Straighten = require('./straighten');
+module.exports.UnrestrictTurn = require('./unrestrict_turn');
+
+},{"./add_entity":1,"./add_member":2,"./add_midpoint":3,"./add_vertex":4,"./change_member":5,"./change_preset":6,"./change_tags":7,"./circularize":8,"./connect":9,"./copy_entities":10,"./delete_member":11,"./delete_multiple":12,"./delete_node":13,"./delete_relation":14,"./delete_way":15,"./deprecate_tags":16,"./discard_tags":17,"./disconnect":18,"./join":20,"./merge":21,"./merge_polygon":22,"./merge_remote_changes":23,"./move":24,"./move_node":25,"./noop":26,"./orthogonalize":27,"./restrict_turn":28,"./reverse":29,"./revert":30,"./rotate_way":31,"./split":32,"./straighten":33,"./unrestrict_turn":34}],20:[function(require,module,exports){
+// Join ways at the end node they share.
+//
+// This is the inverse of `iD.actions.Split`.
+//
+// Reference:
+// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeWaysAction.as
+// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/CombineWayAction.java
+//
+module.exports = function(ids) {
+
+ function groupEntitiesByGeometry(graph) {
+ var entities = ids.map(function(id) { return graph.entity(id); });
+ return _.extend({line: []}, _.groupBy(entities, function(entity) { return entity.geometry(graph); }));
+ }
+
+ var action = function(graph) {
+ var ways = ids.map(graph.entity, graph),
+ survivor = ways[0];
+
+ // Prefer to keep an existing way.
+ for (var i = 0; i < ways.length; i++) {
+ if (!ways[i].isNew()) {
+ survivor = ways[i];
+ break;
+ }
+ }
+
+ var joined = iD.geo.joinWays(ways, graph)[0];
+
+ survivor = survivor.update({nodes: _.map(joined.nodes, 'id')});
+ graph = graph.replace(survivor);
+
+ joined.forEach(function(way) {
+ if (way.id === survivor.id)
+ return;
+
+ graph.parentRelations(way).forEach(function(parent) {
+ graph = graph.replace(parent.replaceMember(way, survivor));
+ });
+
+ survivor = survivor.mergeTags(way.tags);
+
+ graph = graph.replace(survivor);
+ graph = iD.actions.DeleteWay(way.id)(graph);
+ });
+
+ return graph;
+ };
+
+ action.disabled = function(graph) {
+ var geometries = groupEntitiesByGeometry(graph);
+ if (ids.length < 2 || ids.length !== geometries.line.length)
+ return 'not_eligible';
+
+ var joined = iD.geo.joinWays(ids.map(graph.entity, graph), graph);
+ if (joined.length > 1)
+ return 'not_adjacent';
+
+ var nodeIds = _.map(joined[0].nodes, 'id').slice(1, -1),
+ relation,
+ tags = {},
+ conflicting = false;
+
+ joined[0].forEach(function(way) {
+ var parents = graph.parentRelations(way);
+ parents.forEach(function(parent) {
+ if (parent.isRestriction() && parent.members.some(function(m) { return nodeIds.indexOf(m.id) >= 0; }))
+ relation = parent;
+ });
+
+ for (var k in way.tags) {
+ if (!(k in tags)) {
+ tags[k] = way.tags[k];
+ } else if (tags[k] && iD.interestingTag(k) && tags[k] !== way.tags[k]) {
+ conflicting = true;
+ }
+ }
+ });
+
+ if (relation)
+ return 'restriction';
+
+ if (conflicting)
+ return 'conflicting_tags';
+ };
+
+ return action;
+};
+
+},{}],21:[function(require,module,exports){
+module.exports = function(ids) {
+ function groupEntitiesByGeometry(graph) {
+ var entities = ids.map(function(id) { return graph.entity(id); });
+ return _.extend({point: [], area: [], line: [], relation: []},
+ _.groupBy(entities, function(entity) { return entity.geometry(graph); }));
+ }
+
+ var action = function(graph) {
+ var geometries = groupEntitiesByGeometry(graph),
+ target = geometries.area[0] || geometries.line[0],
+ points = geometries.point;
+
+ points.forEach(function(point) {
+ target = target.mergeTags(point.tags);
+
+ graph.parentRelations(point).forEach(function(parent) {
+ graph = graph.replace(parent.replaceMember(point, target));
+ });
+
+ graph = graph.remove(point);
+ });
+
+ graph = graph.replace(target);
+
+ return graph;
+ };
+
+ action.disabled = function(graph) {
+ var geometries = groupEntitiesByGeometry(graph);
+ if (geometries.point.length === 0 ||
+ (geometries.area.length + geometries.line.length) !== 1 ||
+ geometries.relation.length !== 0)
+ return 'not_eligible';
+ };
+
+ return action;
+};
+
+},{}],22:[function(require,module,exports){
+module.exports = function(ids, newRelationId) {
+
+ function groupEntities(graph) {
+ var entities = ids.map(function (id) { return graph.entity(id); });
+ return _.extend({
+ closedWay: [],
+ multipolygon: [],
+ other: []
+ }, _.groupBy(entities, function(entity) {
+ if (entity.type === 'way' && entity.isClosed()) {
+ return 'closedWay';
+ } else if (entity.type === 'relation' && entity.isMultipolygon()) {
+ return 'multipolygon';
+ } else {
+ return 'other';
+ }
+ }));
+ }
+
+ var action = function(graph) {
+ var entities = groupEntities(graph);
+
+ // An array representing all the polygons that are part of the multipolygon.
+ //
+ // Each element is itself an array of objects with an id property, and has a
+ // locs property which is an array of the locations forming the polygon.
+ var polygons = entities.multipolygon.reduce(function(polygons, m) {
+ return polygons.concat(iD.geo.joinWays(m.members, graph));
+ }, []).concat(entities.closedWay.map(function(d) {
+ var member = [{id: d.id}];
+ member.nodes = graph.childNodes(d);
+ return member;
+ }));
+
+ // contained is an array of arrays of boolean values,
+ // where contained[j][k] is true iff the jth way is
+ // contained by the kth way.
+ var contained = polygons.map(function(w, i) {
+ return polygons.map(function(d, n) {
+ if (i === n) return null;
+ return iD.geo.polygonContainsPolygon(
+ _.map(d.nodes, 'loc'),
+ _.map(w.nodes, 'loc'));
+ });
+ });
+
+ // Sort all polygons as either outer or inner ways
+ var members = [],
+ outer = true;
+
+ while (polygons.length) {
+ extractUncontained(polygons);
+ polygons = polygons.filter(isContained);
+ contained = contained.filter(isContained).map(filterContained);
+ }
+
+ function isContained(d, i) {
+ return _.some(contained[i]);
+ }
+
+ function filterContained(d) {
+ return d.filter(isContained);
+ }
+
+ function extractUncontained(polygons) {
+ polygons.forEach(function(d, i) {
+ if (!isContained(d, i)) {
+ d.forEach(function(member) {
+ members.push({
+ type: 'way',
+ id: member.id,
+ role: outer ? 'outer' : 'inner'
+ });
+ });
+ }
+ });
+ outer = !outer;
+ }
+
+ // Move all tags to one relation
+ var relation = entities.multipolygon[0] ||
+ iD.Relation({ id: newRelationId, tags: { type: 'multipolygon' }});
+
+ entities.multipolygon.slice(1).forEach(function(m) {
+ relation = relation.mergeTags(m.tags);
+ graph = graph.remove(m);
+ });
+
+ entities.closedWay.forEach(function(way) {
+ function isThisOuter(m) {
+ return m.id === way.id && m.role !== 'inner';
+ }
+ if (members.some(isThisOuter)) {
+ relation = relation.mergeTags(way.tags);
+ graph = graph.replace(way.update({ tags: {} }));
+ }
+ });
+
+ return graph.replace(relation.update({
+ members: members,
+ tags: _.omit(relation.tags, 'area')
+ }));
+ };
+
+ action.disabled = function(graph) {
+ var entities = groupEntities(graph);
+ if (entities.other.length > 0 ||
+ entities.closedWay.length + entities.multipolygon.length < 2)
+ return 'not_eligible';
+ if (!entities.multipolygon.every(function(r) { return r.isComplete(graph); }))
+ return 'incomplete_relation';
+ };
+
+ return action;
+};
+
+},{}],23:[function(require,module,exports){
+module.exports = function(id, localGraph, remoteGraph, formatUser) {
+ var option = 'safe', // 'safe', 'force_local', 'force_remote'
+ conflicts = [];
+
+ function user(d) {
+ return _.isFunction(formatUser) ? formatUser(d) : d;
+ }
+
+
+ function mergeLocation(remote, target) {
+ function pointEqual(a, b) {
+ var epsilon = 1e-6;
+ return (Math.abs(a[0] - b[0]) < epsilon) && (Math.abs(a[1] - b[1]) < epsilon);
+ }
+
+ if (option === 'force_local' || pointEqual(target.loc, remote.loc)) {
+ return target;
+ }
+ if (option === 'force_remote') {
+ return target.update({loc: remote.loc});
+ }
+
+ conflicts.push(t('merge_remote_changes.conflict.location', { user: user(remote.user) }));
+ return target;
+ }
+
+
+ function mergeNodes(base, remote, target) {
+ if (option === 'force_local' || _.isEqual(target.nodes, remote.nodes)) {
+ return target;
+ }
+ if (option === 'force_remote') {
+ return target.update({nodes: remote.nodes});
+ }
+
+ var ccount = conflicts.length,
+ o = base.nodes || [],
+ a = target.nodes || [],
+ b = remote.nodes || [],
+ nodes = [],
+ hunks = Diff3.diff3_merge(a, o, b, true);
+
+ for (var i = 0; i < hunks.length; i++) {
+ var hunk = hunks[i];
+ if (hunk.ok) {
+ nodes.push.apply(nodes, hunk.ok);
+ } else {
+ // for all conflicts, we can assume c.a !== c.b
+ // because `diff3_merge` called with `true` option to exclude false conflicts..
+ var c = hunk.conflict;
+ if (_.isEqual(c.o, c.a)) { // only changed remotely
+ nodes.push.apply(nodes, c.b);
+ } else if (_.isEqual(c.o, c.b)) { // only changed locally
+ nodes.push.apply(nodes, c.a);
+ } else { // changed both locally and remotely
+ conflicts.push(t('merge_remote_changes.conflict.nodelist', { user: user(remote.user) }));
+ break;
+ }
+ }
+ }
+
+ return (conflicts.length === ccount) ? target.update({nodes: nodes}) : target;
+ }
+
+
+ function mergeChildren(targetWay, children, updates, graph) {
+ function isUsed(node, targetWay) {
+ var parentWays = _.map(graph.parentWays(node), 'id');
+ return node.hasInterestingTags() ||
+ _.without(parentWays, targetWay.id).length > 0 ||
+ graph.parentRelations(node).length > 0;
+ }
+
+ var ccount = conflicts.length;
+
+ for (var i = 0; i < children.length; i++) {
+ var id = children[i],
+ node = graph.hasEntity(id);
+
+ // remove unused childNodes..
+ if (targetWay.nodes.indexOf(id) === -1) {
+ if (node && !isUsed(node, targetWay)) {
+ updates.removeIds.push(id);
+ }
+ continue;
+ }
+
+ // restore used childNodes..
+ var local = localGraph.hasEntity(id),
+ remote = remoteGraph.hasEntity(id),
+ target;
+
+ if (option === 'force_remote' && remote && remote.visible) {
+ updates.replacements.push(remote);
+
+ } else if (option === 'force_local' && local) {
+ target = iD.Entity(local);
+ if (remote) {
+ target = target.update({ version: remote.version });
+ }
+ updates.replacements.push(target);
+
+ } else if (option === 'safe' && local && remote && local.version !== remote.version) {
+ target = iD.Entity(local, { version: remote.version });
+ if (remote.visible) {
+ target = mergeLocation(remote, target);
+ } else {
+ conflicts.push(t('merge_remote_changes.conflict.deleted', { user: user(remote.user) }));
+ }
+
+ if (conflicts.length !== ccount) break;
+ updates.replacements.push(target);
+ }
+ }
+
+ return targetWay;
+ }
+
+
+ function updateChildren(updates, graph) {
+ for (var i = 0; i < updates.replacements.length; i++) {
+ graph = graph.replace(updates.replacements[i]);
+ }
+ if (updates.removeIds.length) {
+ graph = iD.actions.DeleteMultiple(updates.removeIds)(graph);
+ }
+ return graph;
+ }
+
+
+ function mergeMembers(remote, target) {
+ if (option === 'force_local' || _.isEqual(target.members, remote.members)) {
+ return target;
+ }
+ if (option === 'force_remote') {
+ return target.update({members: remote.members});
+ }
+
+ conflicts.push(t('merge_remote_changes.conflict.memberlist', { user: user(remote.user) }));
+ return target;
+ }
+
+
+ function mergeTags(base, remote, target) {
+ function ignoreKey(k) {
+ return _.includes(iD.data.discarded, k);
+ }
+
+ if (option === 'force_local' || _.isEqual(target.tags, remote.tags)) {
+ return target;
+ }
+ if (option === 'force_remote') {
+ return target.update({tags: remote.tags});
+ }
+
+ var ccount = conflicts.length,
+ o = base.tags || {},
+ a = target.tags || {},
+ b = remote.tags || {},
+ keys = _.reject(_.union(_.keys(o), _.keys(a), _.keys(b)), ignoreKey),
+ tags = _.clone(a),
+ changed = false;
+
+ for (var i = 0; i < keys.length; i++) {
+ var k = keys[i];
+
+ if (o[k] !== b[k] && a[k] !== b[k]) { // changed remotely..
+ if (o[k] !== a[k]) { // changed locally..
+ conflicts.push(t('merge_remote_changes.conflict.tags',
+ { tag: k, local: a[k], remote: b[k], user: user(remote.user) }));
+
+ } else { // unchanged locally, accept remote change..
+ if (b.hasOwnProperty(k)) {
+ tags[k] = b[k];
+ } else {
+ delete tags[k];
+ }
+ changed = true;
+ }
+ }
+ }
+
+ return (changed && conflicts.length === ccount) ? target.update({tags: tags}) : target;
+ }
+
+
+ // `graph.base()` is the common ancestor of the two graphs.
+ // `localGraph` contains user's edits up to saving
+ // `remoteGraph` contains remote edits to modified nodes
+ // `graph` must be a descendent of `localGraph` and may include
+ // some conflict resolution actions performed on it.
+ //
+ // --- ... --- `localGraph` -- ... -- `graph`
+ // /
+ // `graph.base()` --- ... --- `remoteGraph`
+ //
+ var action = function(graph) {
+ var updates = { replacements: [], removeIds: [] },
+ base = graph.base().entities[id],
+ local = localGraph.entity(id),
+ remote = remoteGraph.entity(id),
+ target = iD.Entity(local, { version: remote.version });
+
+ // delete/undelete
+ if (!remote.visible) {
+ if (option === 'force_remote') {
+ return iD.actions.DeleteMultiple([id])(graph);
+
+ } else if (option === 'force_local') {
+ if (target.type === 'way') {
+ target = mergeChildren(target, _.uniq(local.nodes), updates, graph);
+ graph = updateChildren(updates, graph);
+ }
+ return graph.replace(target);
+
+ } else {
+ conflicts.push(t('merge_remote_changes.conflict.deleted', { user: user(remote.user) }));
+ return graph; // do nothing
+ }
+ }
+
+ // merge
+ if (target.type === 'node') {
+ target = mergeLocation(remote, target);
+
+ } else if (target.type === 'way') {
+ // pull in any child nodes that may not be present locally..
+ graph.rebase(remoteGraph.childNodes(remote), [graph], false);
+ target = mergeNodes(base, remote, target);
+ target = mergeChildren(target, _.union(local.nodes, remote.nodes), updates, graph);
+
+ } else if (target.type === 'relation') {
+ target = mergeMembers(remote, target);
+ }
+
+ target = mergeTags(base, remote, target);
+
+ if (!conflicts.length) {
+ graph = updateChildren(updates, graph).replace(target);
+ }
+
+ return graph;
+ };
+
+ action.withOption = function(opt) {
+ option = opt;
+ return action;
+ };
+
+ action.conflicts = function() {
+ return conflicts;
+ };
+
+ return action;
+};
+
+},{}],24:[function(require,module,exports){
+// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as
+module.exports = function(moveIds, tryDelta, projection, cache) {
+ var delta = tryDelta;
+
+ function vecAdd(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 setupCache(graph) {
+ function canMove(nodeId) {
+ var parents = _.map(graph.parentWays(graph.entity(nodeId)), 'id');
+ if (parents.length < 3) return true;
+
+ // Don't move a vertex where >2 ways meet, unless all parentWays are moving too..
+ var parentsMoving = _.all(parents, function(id) { return cache.moving[id]; });
+ if (!parentsMoving) delete cache.moving[nodeId];
+
+ return parentsMoving;
+ }
+
+ function cacheEntities(ids) {
+ _.each(ids, function(id) {
+ if (cache.moving[id]) return;
+ cache.moving[id] = true;
+
+ var entity = graph.hasEntity(id);
+ if (!entity) return;
+
+ if (entity.type === 'node') {
+ cache.nodes.push(id);
+ cache.startLoc[id] = entity.loc;
+ } else if (entity.type === 'way') {
+ cache.ways.push(id);
+ cacheEntities(entity.nodes);
+ } else {
+ cacheEntities(_.map(entity.members, 'id'));
+ }
+ });
+ }
+
+ function cacheIntersections(ids) {
+ function isEndpoint(way, id) { return !way.isClosed() && !!way.affix(id); }
+
+ _.each(ids, function(id) {
+ // consider only intersections with 1 moved and 1 unmoved way.
+ _.each(graph.childNodes(graph.entity(id)), function(node) {
+ var parents = graph.parentWays(node);
+ if (parents.length !== 2) return;
+
+ var moved = graph.entity(id),
+ unmoved = _.find(parents, function(way) { return !cache.moving[way.id]; });
+ if (!unmoved) return;
+
+ // exclude ways that are overly connected..
+ if (_.intersection(moved.nodes, unmoved.nodes).length > 2) return;
+
+ if (moved.isArea() || unmoved.isArea()) return;
+
+ cache.intersection[node.id] = {
+ nodeId: node.id,
+ movedId: moved.id,
+ unmovedId: unmoved.id,
+ movedIsEP: isEndpoint(moved, node.id),
+ unmovedIsEP: isEndpoint(unmoved, node.id)
+ };
+ });
+ });
+ }
+
+
+ if (!cache) {
+ cache = {};
+ }
+ if (!cache.ok) {
+ cache.moving = {};
+ cache.intersection = {};
+ cache.replacedVertex = {};
+ cache.startLoc = {};
+ cache.nodes = [];
+ cache.ways = [];
+
+ cacheEntities(moveIds);
+ cacheIntersections(cache.ways);
+ cache.nodes = _.filter(cache.nodes, canMove);
+
+ cache.ok = true;
+ }
+ }
+
+
+ // Place a vertex where the moved vertex used to be, to preserve way shape..
+ function replaceMovedVertex(nodeId, wayId, graph, delta) {
+ var way = graph.entity(wayId),
+ moved = graph.entity(nodeId),
+ movedIndex = way.nodes.indexOf(nodeId),
+ len, prevIndex, nextIndex;
+
+ if (way.isClosed()) {
+ len = way.nodes.length - 1;
+ prevIndex = (movedIndex + len - 1) % len;
+ nextIndex = (movedIndex + len + 1) % len;
+ } else {
+ len = way.nodes.length;
+ prevIndex = movedIndex - 1;
+ nextIndex = movedIndex + 1;
+ }
+
+ var prev = graph.hasEntity(way.nodes[prevIndex]),
+ next = graph.hasEntity(way.nodes[nextIndex]);
+
+ // Don't add orig vertex at endpoint..
+ if (!prev || !next) return graph;
+
+ var key = wayId + '_' + nodeId,
+ orig = cache.replacedVertex[key];
+ if (!orig) {
+ orig = iD.Node();
+ cache.replacedVertex[key] = orig;
+ cache.startLoc[orig.id] = cache.startLoc[nodeId];
+ }
+
+ var start, end;
+ if (delta) {
+ start = projection(cache.startLoc[nodeId]);
+ end = projection.invert(vecAdd(start, delta));
+ } else {
+ end = cache.startLoc[nodeId];
+ }
+ orig = orig.move(end);
+
+ var angle = Math.abs(iD.geo.angle(orig, prev, projection) -
+ iD.geo.angle(orig, next, projection)) * 180 / Math.PI;
+
+ // Don't add orig vertex if it would just make a straight line..
+ if (angle > 175 && angle < 185) return graph;
+
+ // Don't add orig vertex if another point is already nearby (within 10m)
+ if (iD.geo.sphericalDistance(prev.loc, orig.loc) < 10 ||
+ iD.geo.sphericalDistance(orig.loc, next.loc) < 10) return graph;
+
+ // moving forward or backward along way?
+ var p1 = [prev.loc, orig.loc, moved.loc, next.loc].map(projection),
+ p2 = [prev.loc, moved.loc, orig.loc, next.loc].map(projection),
+ d1 = iD.geo.pathLength(p1),
+ d2 = iD.geo.pathLength(p2),
+ insertAt = (d1 < d2) ? movedIndex : nextIndex;
+
+ // moving around closed loop?
+ if (way.isClosed() && insertAt === 0) insertAt = len;
+
+ way = way.addNode(orig.id, insertAt);
+ return graph.replace(orig).replace(way);
+ }
+
+ // Reorder nodes around intersections that have moved..
+ function unZorroIntersection(intersection, graph) {
+ var vertex = graph.entity(intersection.nodeId),
+ way1 = graph.entity(intersection.movedId),
+ way2 = graph.entity(intersection.unmovedId),
+ isEP1 = intersection.movedIsEP,
+ isEP2 = intersection.unmovedIsEP;
+
+ // don't move the vertex if it is the endpoint of both ways.
+ if (isEP1 && isEP2) return graph;
+
+ var nodes1 = _.without(graph.childNodes(way1), vertex),
+ nodes2 = _.without(graph.childNodes(way2), vertex);
+
+ if (way1.isClosed() && way1.first() === vertex.id) nodes1.push(nodes1[0]);
+ if (way2.isClosed() && way2.first() === vertex.id) nodes2.push(nodes2[0]);
+
+ var edge1 = !isEP1 && iD.geo.chooseEdge(nodes1, projection(vertex.loc), projection),
+ edge2 = !isEP2 && iD.geo.chooseEdge(nodes2, projection(vertex.loc), projection),
+ loc;
+
+ // snap vertex to nearest edge (or some point between them)..
+ if (!isEP1 && !isEP2) {
+ var epsilon = 1e-4, maxIter = 10;
+ for (var i = 0; i < maxIter; i++) {
+ loc = iD.geo.interp(edge1.loc, edge2.loc, 0.5);
+ edge1 = iD.geo.chooseEdge(nodes1, projection(loc), projection);
+ edge2 = iD.geo.chooseEdge(nodes2, projection(loc), projection);
+ if (Math.abs(edge1.distance - edge2.distance) < epsilon) break;
+ }
+ } else if (!isEP1) {
+ loc = edge1.loc;
+ } else {
+ loc = edge2.loc;
+ }
+
+ graph = graph.replace(vertex.move(loc));
+
+ // if zorro happened, reorder nodes..
+ if (!isEP1 && edge1.index !== way1.nodes.indexOf(vertex.id)) {
+ way1 = way1.removeNode(vertex.id).addNode(vertex.id, edge1.index);
+ graph = graph.replace(way1);
+ }
+ if (!isEP2 && edge2.index !== way2.nodes.indexOf(vertex.id)) {
+ way2 = way2.removeNode(vertex.id).addNode(vertex.id, edge2.index);
+ graph = graph.replace(way2);
+ }
+
+ return graph;
+ }
+
+
+ function cleanupIntersections(graph) {
+ _.each(cache.intersection, function(obj) {
+ graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, delta);
+ graph = replaceMovedVertex(obj.nodeId, obj.unmovedId, graph, null);
+ graph = unZorroIntersection(obj, graph);
+ });
+
+ return graph;
+ }
+
+ // check if moving way endpoint can cross an unmoved way, if so limit delta..
+ function limitDelta(graph) {
+ _.each(cache.intersection, function(obj) {
+ if (!obj.movedIsEP) return;
+
+ var node = graph.entity(obj.nodeId),
+ start = projection(node.loc),
+ end = vecAdd(start, delta),
+ movedNodes = graph.childNodes(graph.entity(obj.movedId)),
+ movedPath = _.map(_.map(movedNodes, 'loc'),
+ function(loc) { return vecAdd(projection(loc), delta); }),
+ unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)),
+ unmovedPath = _.map(_.map(unmovedNodes, 'loc'), projection),
+ hits = iD.geo.pathIntersections(movedPath, unmovedPath);
+
+ for (var i = 0; i < hits.length; i++) {
+ if (_.isEqual(hits[i], end)) continue;
+ var edge = iD.geo.chooseEdge(unmovedNodes, end, projection);
+ delta = vecSub(projection(edge.loc), start);
+ }
+ });
+ }
+
+
+ var action = function(graph) {
+ if (delta[0] === 0 && delta[1] === 0) return graph;
+
+ setupCache(graph);
+
+ if (!_.isEmpty(cache.intersection)) {
+ limitDelta(graph);
+ }
+
+ _.each(cache.nodes, function(id) {
+ var node = graph.entity(id),
+ start = projection(node.loc),
+ end = vecAdd(start, delta);
+ graph = graph.replace(node.move(projection.invert(end)));
+ });
+
+ if (!_.isEmpty(cache.intersection)) {
+ graph = cleanupIntersections(graph);
+ }
+
+ return graph;
+ };
+
+ 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;
+ };
+
+ return action;
+};
+
+},{}],25:[function(require,module,exports){
+// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as
+module.exports = function(nodeId, loc) {
+ return function(graph) {
+ return graph.replace(graph.entity(nodeId).move(loc));
+ };
+};
+
+},{}],26:[function(require,module,exports){
+module.exports = function() {
+ return function(graph) {
+ return graph;
+ };
+};
+
+},{}],27:[function(require,module,exports){
+/*
+ * Based on https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/potlatch2/tools/Quadrilateralise.as
+ */
+
+module.exports = function(wayId, projection) {
+ var threshold = 12, // degrees within right or straight to alter
+ lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180),
+ upperThreshold = Math.cos(threshold * Math.PI / 180);
+
+ var action = function(graph) {
+ var way = graph.entity(wayId),
+ nodes = graph.childNodes(way),
+ points = _.uniq(nodes).map(function(n) { return projection(n.loc); }),
+ corner = {i: 0, dotp: 1},
+ epsilon = 1e-4,
+ i, j, score, motions;
+
+ if (nodes.length === 4) {
+ for (i = 0; i < 1000; i++) {
+ motions = points.map(calcMotion);
+ points[corner.i] = addPoints(points[corner.i],motions[corner.i]);
+ score = corner.dotp;
+ if (score < epsilon) {
+ break;
+ }
+ }
+
+ graph = graph.replace(graph.entity(nodes[corner.i].id)
+ .move(projection.invert(points[corner.i])));
+ } else {
+ var best,
+ originalPoints = _.clone(points);
+ score = Infinity;
+
+ for (i = 0; i < 1000; i++) {
+ motions = points.map(calcMotion);
+ for (j = 0; j < motions.length; j++) {
+ points[j] = addPoints(points[j],motions[j]);
+ }
+ var newScore = squareness(points);
+ if (newScore < score) {
+ best = _.clone(points);
+ score = newScore;
+ }
+ if (score < epsilon) {
+ break;
+ }
+ }
+
+ points = best;
+
+ for (i = 0; i < points.length; i++) {
+ // only move the points that actually moved
+ if (originalPoints[i][0] !== points[i][0] || originalPoints[i][1] !== points[i][1]) {
+ graph = graph.replace(graph.entity(nodes[i].id)
+ .move(projection.invert(points[i])));
+ }
+ }
+
+ // remove empty nodes on straight sections
+ for (i = 0; i < points.length; i++) {
+ var node = nodes[i];
+
+ if (graph.parentWays(node).length > 1 ||
+ graph.parentRelations(node).length ||
+ node.hasInterestingTags()) {
+
+ continue;
+ }
+
+ var dotp = normalizedDotProduct(i, points);
+ if (dotp < -1 + epsilon) {
+ graph = iD.actions.DeleteNode(nodes[i].id)(graph);
+ }
+ }
+ }
+
+ return graph;
+
+ function calcMotion(b, i, array) {
+ var a = array[(i - 1 + array.length) % array.length],
+ c = array[(i + 1) % array.length],
+ p = subtractPoints(a, b),
+ q = subtractPoints(c, b),
+ scale, dotp;
+
+ scale = 2 * Math.min(iD.geo.euclideanDistance(p, [0, 0]), iD.geo.euclideanDistance(q, [0, 0]));
+ p = normalizePoint(p, 1.0);
+ q = normalizePoint(q, 1.0);
+
+ dotp = filterDotProduct(p[0] * q[0] + p[1] * q[1]);
+
+ // nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270).
+ if (array.length > 3) {
+ if (dotp < -0.707106781186547) {
+ dotp += 1.0;
+ }
+ } else if (dotp && Math.abs(dotp) < corner.dotp) {
+ corner.i = i;
+ corner.dotp = Math.abs(dotp);
+ }
+
+ return normalizePoint(addPoints(p, q), 0.1 * dotp * scale);
+ }
+ };
+
+ function squareness(points) {
+ return points.reduce(function(sum, val, i, array) {
+ var dotp = normalizedDotProduct(i, array);
+
+ dotp = filterDotProduct(dotp);
+ return sum + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)));
+ }, 0);
+ }
+
+ function normalizedDotProduct(i, points) {
+ var a = points[(i - 1 + points.length) % points.length],
+ b = points[i],
+ c = points[(i + 1) % points.length],
+ p = subtractPoints(a, b),
+ q = subtractPoints(c, b);
+
+ p = normalizePoint(p, 1.0);
+ q = normalizePoint(q, 1.0);
+
+ return p[0] * q[0] + p[1] * q[1];
+ }
+
+ function subtractPoints(a, b) {
+ return [a[0] - b[0], a[1] - b[1]];
+ }
+
+ function addPoints(a, b) {
+ return [a[0] + b[0], a[1] + b[1]];
+ }
+
+ function normalizePoint(point, scale) {
+ var vector = [0, 0];
+ var length = Math.sqrt(point[0] * point[0] + point[1] * point[1]);
+ if (length !== 0) {
+ vector[0] = point[0] / length;
+ vector[1] = point[1] / length;
+ }
+
+ vector[0] *= scale;
+ vector[1] *= scale;
+
+ return vector;
+ }
+
+ function filterDotProduct(dotp) {
+ if (lowerThreshold > Math.abs(dotp) || Math.abs(dotp) > upperThreshold) {
+ return dotp;
+ }
+
+ return 0;
+ }
+
+ action.disabled = function(graph) {
+ var way = graph.entity(wayId),
+ nodes = graph.childNodes(way),
+ points = _.uniq(nodes).map(function(n) { return projection(n.loc); });
+
+ if (squareness(points)) {
+ return false;
+ }
+
+ return 'not_squarish';
+ };
+
+ return action;
+};
+
+},{}],28:[function(require,module,exports){
+// Create a restriction relation for `turn`, which must have the following structure:
+//
+// {
+// from: { node: , way: },
+// via: { node: },
+// to: { node: , way: },
+// restriction: <'no_right_turn', 'no_left_turn', etc.>
+// }
+//
+// This specifies a restriction of type `restriction` when traveling from
+// `from.node` in `from.way` toward `to.node` in `to.way` via `via.node`.
+// (The action does not check that these entities form a valid intersection.)
+//
+// If `restriction` is not provided, it is automatically determined by
+// iD.geo.inferRestriction.
+//
+// If necessary, the `from` and `to` ways are split. In these cases, `from.node`
+// and `to.node` are used to determine which portion of the split ways become
+// members of the restriction.
+//
+// For testing convenience, accepts an ID to assign to the new relation.
+// Normally, this will be undefined and the relation will automatically
+// be assigned a new ID.
+//
+module.exports = function(turn, projection, restrictionId) {
+ return function(graph) {
+ var from = graph.entity(turn.from.way),
+ via = graph.entity(turn.via.node),
+ to = graph.entity(turn.to.way);
+
+ function isClosingNode(way, nodeId) {
+ return nodeId === way.first() && nodeId === way.last();
+ }
+
+ function split(toOrFrom) {
+ var newID = toOrFrom.newID || iD.Way().id;
+ graph = iD.actions.Split(via.id, [newID])
+ .limitWays([toOrFrom.way])(graph);
+
+ var a = graph.entity(newID),
+ b = graph.entity(toOrFrom.way);
+
+ if (a.nodes.indexOf(toOrFrom.node) !== -1) {
+ return [a, b];
+ } else {
+ return [b, a];
+ }
+ }
+
+ if (!from.affix(via.id) || isClosingNode(from, via.id)) {
+ if (turn.from.node === turn.to.node) {
+ // U-turn
+ from = to = split(turn.from)[0];
+ } else if (turn.from.way === turn.to.way) {
+ // Straight-on or circular
+ var s = split(turn.from);
+ from = s[0];
+ to = s[1];
+ } else {
+ // Other
+ from = split(turn.from)[0];
+ }
+ }
+
+ if (!to.affix(via.id) || isClosingNode(to, via.id)) {
+ to = split(turn.to)[0];
+ }
+
+ return graph.replace(iD.Relation({
+ id: restrictionId,
+ tags: {
+ type: 'restriction',
+ restriction: turn.restriction ||
+ iD.geo.inferRestriction(
+ graph,
+ turn.from,
+ turn.via,
+ turn.to,
+ projection)
+ },
+ members: [
+ {id: from.id, type: 'way', role: 'from'},
+ {id: via.id, type: 'node', role: 'via'},
+ {id: to.id, type: 'way', role: 'to'}
+ ]
+ }));
+ };
+};
+
+},{}],29:[function(require,module,exports){
+/*
+ Order the nodes of a way in reverse order and reverse any direction dependent tags
+ other than `oneway`. (We assume that correcting a backwards oneway is the primary
+ reason for reversing a way.)
+
+ The following transforms are performed:
+
+ Keys:
+ *:right=* ⟺ *:left=*
+ *:forward=* ⟺ *:backward=*
+ direction=up ⟺ direction=down
+ incline=up ⟺ incline=down
+ *=right ⟺ *=left
+
+ Relation members:
+ role=forward ⟺ role=backward
+ role=north ⟺ role=south
+ role=east ⟺ role=west
+
+ In addition, numeric-valued `incline` tags are negated.
+
+ The JOSM implementation was used as a guide, but transformations that were of unclear benefit
+ or adjusted tags that don't seem to be used in practice were omitted.
+
+ References:
+ http://wiki.openstreetmap.org/wiki/Forward_%26_backward,_left_%26_right
+ http://wiki.openstreetmap.org/wiki/Key:direction#Steps
+ http://wiki.openstreetmap.org/wiki/Key:incline
+ http://wiki.openstreetmap.org/wiki/Route#Members
+ http://josm.openstreetmap.de/browser/josm/trunk/src/org/openstreetmap/josm/corrector/ReverseWayTagCorrector.java
+ */
+module.exports = function(wayId, options) {
+ var replacements = [
+ [/:right$/, ':left'], [/:left$/, ':right'],
+ [/:forward$/, ':backward'], [/:backward$/, ':forward']
+ ],
+ numeric = /^([+\-]?)(?=[\d.])/,
+ roleReversals = {
+ forward: 'backward',
+ backward: 'forward',
+ north: 'south',
+ south: 'north',
+ east: 'west',
+ west: 'east'
+ };
+
+ function reverseKey(key) {
+ for (var i = 0; i < replacements.length; ++i) {
+ var replacement = replacements[i];
+ if (replacement[0].test(key)) {
+ return key.replace(replacement[0], replacement[1]);
+ }
+ }
+ return key;
+ }
+
+ function reverseValue(key, value) {
+ if (key === 'incline' && numeric.test(value)) {
+ return value.replace(numeric, function(_, sign) { return sign === '-' ? '' : '-'; });
+ } else if (key === 'incline' || key === 'direction') {
+ return {up: 'down', down: 'up'}[value] || value;
+ } else if (options && options.reverseOneway && key === 'oneway') {
+ return {yes: '-1', '1': '-1', '-1': 'yes'}[value] || value;
+ } else {
+ return {left: 'right', right: 'left'}[value] || value;
+ }
+ }
+
+ return function(graph) {
+ var way = graph.entity(wayId),
+ nodes = way.nodes.slice().reverse(),
+ tags = {}, key, role;
+
+ for (key in way.tags) {
+ tags[reverseKey(key)] = reverseValue(key, way.tags[key]);
+ }
+
+ graph.parentRelations(way).forEach(function(relation) {
+ relation.members.forEach(function(member, index) {
+ if (member.id === way.id && (role = roleReversals[member.role])) {
+ relation = relation.updateMember({role: role}, index);
+ graph = graph.replace(relation);
+ }
+ });
+ });
+
+ return graph.replace(way.update({nodes: nodes, tags: tags}));
+ };
+};
+
+},{}],30:[function(require,module,exports){
+module.exports = function(id) {
+
+ var action = function(graph) {
+ var entity = graph.hasEntity(id),
+ base = graph.base().entities[id];
+
+ if (entity && !base) { // entity will be removed..
+ if (entity.type === 'node') {
+ graph.parentWays(entity)
+ .forEach(function(parent) {
+ parent = parent.removeNode(id);
+ graph = graph.replace(parent);
+
+ if (parent.isDegenerate()) {
+ graph = iD.actions.DeleteWay(parent.id)(graph);
+ }
+ });
+ }
+
+ graph.parentRelations(entity)
+ .forEach(function(parent) {
+ parent = parent.removeMembersWithID(id);
+ graph = graph.replace(parent);
+
+ if (parent.isDegenerate()) {
+ graph = iD.actions.DeleteRelation(parent.id)(graph);
+ }
+ });
+ }
+
+ return graph.revert(id);
+ };
+
+ return action;
+};
+
+},{}],31:[function(require,module,exports){
+module.exports = function(wayId, pivot, angle, projection) {
+ return 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]
+ ];
+
+ graph = graph.replace(node.move(projection.invert(point)));
+
+ });
+
+ });
+ };
+};
+
+},{}],32:[function(require,module,exports){
+// Split a way at the given node.
+//
+// Optionally, split only the given ways, if multiple ways share
+// the given node.
+//
+// This is the inverse of `iD.actions.Join`.
+//
+// For testing convenience, accepts an ID to assign to the new way.
+// Normally, this will be undefined and the way will automatically
+// be assigned a new ID.
+//
+// Reference:
+// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as
+//
+module.exports = function(nodeId, newWayIds) {
+ var wayIds;
+
+ // if the way is closed, we need to search for a partner node
+ // to split the way at.
+ //
+ // The following looks for a node that is both far away from
+ // the initial node in terms of way segment length and nearby
+ // in terms of beeline-distance. This assures that areas get
+ // split on the most "natural" points (independent of the number
+ // of nodes).
+ // For example: bone-shaped areas get split across their waist
+ // line, circles across the diameter.
+ function splitArea(nodes, idxA, graph) {
+ var lengths = new Array(nodes.length),
+ length,
+ i,
+ best = 0,
+ idxB;
+
+ function wrap(index) {
+ return iD.util.wrap(index, nodes.length);
+ }
+
+ function dist(nA, nB) {
+ return iD.geo.sphericalDistance(graph.entity(nA).loc, graph.entity(nB).loc);
+ }
+
+ // calculate lengths
+ length = 0;
+ for (i = wrap(idxA+1); i !== idxA; i = wrap(i+1)) {
+ length += dist(nodes[i], nodes[wrap(i-1)]);
+ lengths[i] = length;
+ }
+
+ length = 0;
+ for (i = wrap(idxA-1); i !== idxA; i = wrap(i-1)) {
+ length += dist(nodes[i], nodes[wrap(i+1)]);
+ if (length < lengths[i])
+ lengths[i] = length;
+ }
+
+ // determine best opposite node to split
+ for (i = 0; i < nodes.length; i++) {
+ var cost = lengths[i] / dist(nodes[idxA], nodes[i]);
+ if (cost > best) {
+ idxB = i;
+ best = cost;
+ }
+ }
+
+ return idxB;
+ }
+
+ function split(graph, wayA, newWayId) {
+ var wayB = iD.Way({id: newWayId, tags: wayA.tags}),
+ nodesA,
+ nodesB,
+ isArea = wayA.isArea(),
+ isOuter = iD.geo.isSimpleMultipolygonOuterMember(wayA, graph);
+
+ if (wayA.isClosed()) {
+ var nodes = wayA.nodes.slice(0, -1),
+ idxA = _.indexOf(nodes, nodeId),
+ idxB = splitArea(nodes, idxA, graph);
+
+ if (idxB < idxA) {
+ nodesA = nodes.slice(idxA).concat(nodes.slice(0, idxB + 1));
+ nodesB = nodes.slice(idxB, idxA + 1);
+ } else {
+ nodesA = nodes.slice(idxA, idxB + 1);
+ nodesB = nodes.slice(idxB).concat(nodes.slice(0, idxA + 1));
+ }
+ } else {
+ var idx = _.indexOf(wayA.nodes, nodeId, 1);
+ nodesA = wayA.nodes.slice(0, idx + 1);
+ nodesB = wayA.nodes.slice(idx);
+ }
+
+ wayA = wayA.update({nodes: nodesA});
+ wayB = wayB.update({nodes: nodesB});
+
+ graph = graph.replace(wayA);
+ graph = graph.replace(wayB);
+
+ graph.parentRelations(wayA).forEach(function(relation) {
+ if (relation.isRestriction()) {
+ var via = relation.memberByRole('via');
+ if (via && wayB.contains(via.id)) {
+ relation = relation.updateMember({id: wayB.id}, relation.memberById(wayA.id).index);
+ graph = graph.replace(relation);
+ }
+ } else {
+ if (relation === isOuter) {
+ graph = graph.replace(relation.mergeTags(wayA.tags));
+ graph = graph.replace(wayA.update({tags: {}}));
+ graph = graph.replace(wayB.update({tags: {}}));
+ }
+
+ var member = {
+ id: wayB.id,
+ type: 'way',
+ role: relation.memberById(wayA.id).role
+ };
+
+ graph = iD.actions.AddMember(relation.id, member)(graph);
+ }
+ });
+
+ if (!isOuter && isArea) {
+ var multipolygon = iD.Relation({
+ tags: _.extend({}, wayA.tags, {type: 'multipolygon'}),
+ members: [
+ {id: wayA.id, role: 'outer', type: 'way'},
+ {id: wayB.id, role: 'outer', type: 'way'}
+ ]});
+
+ graph = graph.replace(multipolygon);
+ graph = graph.replace(wayA.update({tags: {}}));
+ graph = graph.replace(wayB.update({tags: {}}));
+ }
+
+ return graph;
+ }
+
+ var action = function(graph) {
+ var candidates = action.ways(graph);
+ for (var i = 0; i < candidates.length; i++) {
+ graph = split(graph, candidates[i], newWayIds && newWayIds[i]);
+ }
+ return graph;
+ };
+
+ action.ways = function(graph) {
+ var node = graph.entity(nodeId),
+ parents = graph.parentWays(node),
+ hasLines = _.some(parents, function(parent) { return parent.geometry(graph) === 'line'; });
+
+ return parents.filter(function(parent) {
+ if (wayIds && wayIds.indexOf(parent.id) === -1)
+ return false;
+
+ if (!wayIds && hasLines && parent.geometry(graph) !== 'line')
+ return false;
+
+ if (parent.isClosed()) {
+ return true;
+ }
+
+ for (var i = 1; i < parent.nodes.length - 1; i++) {
+ if (parent.nodes[i] === nodeId) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+ };
+
+ action.disabled = function(graph) {
+ var candidates = action.ways(graph);
+ if (candidates.length === 0 || (wayIds && wayIds.length !== candidates.length))
+ return 'not_eligible';
+ };
+
+ action.limitWays = function(_) {
+ if (!arguments.length) return wayIds;
+ wayIds = _;
+ return action;
+ };
+
+ return action;
+};
+
+},{}],33:[function(require,module,exports){
+/*
+ * Based on https://github.com/openstreetmap/potlatch2/net/systemeD/potlatch2/tools/Straighten.as
+ */
+
+module.exports = function(wayId, projection) {
+ function positionAlongWay(n, s, e) {
+ return ((n[0] - s[0]) * (e[0] - s[0]) + (n[1] - s[1]) * (e[1] - s[1]))/
+ (Math.pow(e[0] - s[0], 2) + Math.pow(e[1] - s[1], 2));
+ }
+
+ var action = function(graph) {
+ var way = graph.entity(wayId),
+ nodes = graph.childNodes(way),
+ points = nodes.map(function(n) { return projection(n.loc); }),
+ startPoint = points[0],
+ endPoint = points[points.length-1],
+ toDelete = [],
+ i;
+
+ for (i = 1; i < points.length-1; i++) {
+ var node = nodes[i],
+ point = points[i];
+
+ if (graph.parentWays(node).length > 1 ||
+ graph.parentRelations(node).length ||
+ node.hasInterestingTags()) {
+
+ var u = positionAlongWay(point, startPoint, endPoint),
+ p0 = startPoint[0] + u * (endPoint[0] - startPoint[0]),
+ p1 = startPoint[1] + u * (endPoint[1] - startPoint[1]);
+
+ graph = graph.replace(graph.entity(node.id)
+ .move(projection.invert([p0, p1])));
+ } else {
+ // safe to delete
+ if (toDelete.indexOf(node) === -1) {
+ toDelete.push(node);
+ }
+ }
+ }
+
+ for (i = 0; i < toDelete.length; i++) {
+ graph = iD.actions.DeleteNode(toDelete[i].id)(graph);
+ }
+
+ return graph;
+ };
+
+ action.disabled = function(graph) {
+ // check way isn't too bendy
+ var way = graph.entity(wayId),
+ nodes = graph.childNodes(way),
+ points = nodes.map(function(n) { return projection(n.loc); }),
+ startPoint = points[0],
+ endPoint = points[points.length-1],
+ threshold = 0.2 * Math.sqrt(Math.pow(startPoint[0] - endPoint[0], 2) + Math.pow(startPoint[1] - endPoint[1], 2)),
+ i;
+
+ if (threshold === 0) {
+ return 'too_bendy';
+ }
+
+ for (i = 1; i < points.length-1; i++) {
+ var point = points[i],
+ u = positionAlongWay(point, startPoint, endPoint),
+ p0 = startPoint[0] + u * (endPoint[0] - startPoint[0]),
+ p1 = startPoint[1] + u * (endPoint[1] - startPoint[1]),
+ dist = Math.sqrt(Math.pow(p0 - point[0], 2) + Math.pow(p1 - point[1], 2));
+
+ // to bendy if point is off by 20% of total start/end distance in projected space
+ if (isNaN(dist) || dist > threshold) {
+ return 'too_bendy';
+ }
+ }
+ };
+
+ return action;
+};
+
+},{}],34:[function(require,module,exports){
+// Remove the effects of `turn.restriction` on `turn`, which must have the
+// following structure:
+//
+// {
+// from: { node: , way: },
+// via: { node: },
+// to: { node: , way: },
+// restriction:
+// }
+//
+// In the simple case, `restriction` is a reference to a `no_*` restriction
+// on the turn itself. In this case, it is simply deleted.
+//
+// The more complex case is where `restriction` references an `only_*`
+// restriction on a different turn in the same intersection. In that case,
+// that restriction is also deleted, but at the same time restrictions on
+// the turns other than the first two are created.
+//
+module.exports = function(turn) {
+ return function(graph) {
+ return iD.actions.DeleteRelation(turn.restriction)(graph);
+ };
+};
+
+},{}]},{},[19])(19)
+});
\ No newline at end of file
diff --git a/test/index.html b/test/index.html
index e6cdd5c23..206a4039a 100644
--- a/test/index.html
+++ b/test/index.html
@@ -42,6 +42,8 @@
+
+
@@ -132,8 +134,6 @@
-
-