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 @@ - -