diff --git a/Makefile b/Makefile
index b079b12d5..4a4084631 100644
--- a/Makefile
+++ b/Makefile
@@ -43,10 +43,8 @@ $(BUILDJS_TARGETS): $(BUILDJS_SOURCES) build.js
MODULE_TARGETS = \
- js/lib/id/actions.js \
+ js/lib/id/index.js \
js/lib/id/behavior.js \
- js/lib/id/core.js \
- js/lib/id/geo.js \
js/lib/id/modes.js \
js/lib/id/operations.js \
js/lib/id/presets.js \
@@ -61,22 +59,14 @@ MODULE_TARGETS = \
js/lib/id/util.js \
js/lib/id/validations.js
-js/lib/id/actions.js: $(shell find modules/actions -type f)
+js/lib/id/index.js: $(shell find modules/index.js -type f)
@rm -f $@
- node_modules/.bin/rollup -f umd -n iD.actions modules/actions/index.js --no-strict -o $@
+ node_modules/.bin/rollup -f umd -n iD modules/index.js --no-strict -o $@
js/lib/id/behavior.js: $(shell find modules/behavior -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD.behavior modules/behavior/index.js --no-strict -o $@
-js/lib/id/core.js: $(shell find modules/core -type f)
- @rm -f $@
- node_modules/.bin/rollup -f umd -n iD modules/core/index.js --no-strict -o $@
-
-js/lib/id/geo.js: $(shell find modules/geo -type f)
- @rm -f $@
- node_modules/.bin/rollup -f umd -n iD.geo modules/geo/index.js --no-strict -o $@
-
js/lib/id/modes.js: $(shell find modules/modes -type f)
@rm -f $@
node_modules/.bin/rollup -f umd -n iD.modes modules/modes/index.js --no-strict -o $@
diff --git a/index.html b/index.html
index 64befdbcb..f937272c7 100644
--- a/index.html
+++ b/index.html
@@ -34,10 +34,9 @@
-
+
+
-
-
diff --git a/js/lib/id/geo.js b/js/lib/id/geo.js
index be373b0cc..000fb46de 100644
--- a/js/lib/id/geo.js
+++ b/js/lib/id/geo.js
@@ -115,6 +115,798 @@
});
+ function interestingTag(key) {
+ return key !== 'attribution' &&
+ key !== 'created_by' &&
+ key !== 'source' &&
+ key !== 'odbl' &&
+ key.indexOf('tiger:') !== 0;
+
+ }
+
+ var oneWayTags = {
+ 'aerialway': {
+ 'chair_lift': true,
+ 'mixed_lift': true,
+ 't-bar': true,
+ 'j-bar': true,
+ 'platter': true,
+ 'rope_tow': true,
+ 'magic_carpet': true,
+ 'yes': true
+ },
+ 'highway': {
+ 'motorway': true,
+ 'motorway_link': true
+ },
+ 'junction': {
+ 'roundabout': true
+ },
+ 'man_made': {
+ 'piste:halfpipe': true
+ },
+ 'piste:type': {
+ 'downhill': true,
+ 'sled': true,
+ 'yes': true
+ },
+ 'waterway': {
+ 'river': true,
+ 'stream': true
+ }
+ };
+
+ function Entity(attrs) {
+ // For prototypal inheritance.
+ if (this instanceof Entity) return;
+
+ // Create the appropriate subtype.
+ if (attrs && attrs.type) {
+ return Entity[attrs.type].apply(this, arguments);
+ } else if (attrs && attrs.id) {
+ return Entity[Entity.id.type(attrs.id)].apply(this, arguments);
+ }
+
+ // Initialize a generic Entity (used only in tests).
+ return (new Entity()).initialize(arguments);
+ }
+
+ Entity.id = function(type) {
+ return Entity.id.fromOSM(type, Entity.id.next[type]--);
+ };
+
+ Entity.id.next = {node: -1, way: -1, relation: -1};
+
+ Entity.id.fromOSM = function(type, id) {
+ return type[0] + id;
+ };
+
+ Entity.id.toOSM = function(id) {
+ return id.slice(1);
+ };
+
+ Entity.id.type = function(id) {
+ return {'n': 'node', 'w': 'way', 'r': 'relation'}[id[0]];
+ };
+
+ // A function suitable for use as the second argument to d3.selection#data().
+ Entity.key = function(entity) {
+ return entity.id + 'v' + (entity.v || 0);
+ };
+
+ Entity.prototype = {
+ tags: {},
+
+ initialize: function(sources) {
+ for (var i = 0; i < sources.length; ++i) {
+ var source = sources[i];
+ for (var prop in source) {
+ if (Object.prototype.hasOwnProperty.call(source, prop)) {
+ if (source[prop] === undefined) {
+ delete this[prop];
+ } else {
+ this[prop] = source[prop];
+ }
+ }
+ }
+ }
+
+ if (!this.id && this.type) {
+ this.id = Entity.id(this.type);
+ }
+ if (!this.hasOwnProperty('visible')) {
+ this.visible = true;
+ }
+
+ if (iD.debug) {
+ Object.freeze(this);
+ Object.freeze(this.tags);
+
+ if (this.loc) Object.freeze(this.loc);
+ if (this.nodes) Object.freeze(this.nodes);
+ if (this.members) Object.freeze(this.members);
+ }
+
+ return this;
+ },
+
+ copy: function(resolver, copies) {
+ if (copies[this.id])
+ return copies[this.id];
+
+ var copy = Entity(this, {id: undefined, user: undefined, version: undefined});
+ copies[this.id] = copy;
+
+ return copy;
+ },
+
+ osmId: function() {
+ return Entity.id.toOSM(this.id);
+ },
+
+ isNew: function() {
+ return this.osmId() < 0;
+ },
+
+ update: function(attrs) {
+ return Entity(this, attrs, {v: 1 + (this.v || 0)});
+ },
+
+ mergeTags: function(tags) {
+ var merged = _.clone(this.tags), changed = false;
+ for (var k in tags) {
+ var t1 = merged[k],
+ t2 = tags[k];
+ if (!t1) {
+ changed = true;
+ merged[k] = t2;
+ } else if (t1 !== t2) {
+ changed = true;
+ merged[k] = _.union(t1.split(/;\s*/), t2.split(/;\s*/)).join(';');
+ }
+ }
+ return changed ? this.update({tags: merged}) : this;
+ },
+
+ intersects: function(extent, resolver) {
+ return this.extent(resolver).intersects(extent);
+ },
+
+ isUsed: function(resolver) {
+ return _.without(Object.keys(this.tags), 'area').length > 0 ||
+ resolver.parentRelations(this).length > 0;
+ },
+
+ hasInterestingTags: function() {
+ return _.keys(this.tags).some(interestingTag);
+ },
+
+ isHighwayIntersection: function() {
+ return false;
+ },
+
+ deprecatedTags: function() {
+ var tags = _.toPairs(this.tags);
+ var deprecated = {};
+
+ iD.data.deprecated.forEach(function(d) {
+ var match = _.toPairs(d.old)[0];
+ tags.forEach(function(t) {
+ if (t[0] === match[0] &&
+ (t[1] === match[1] || match[1] === '*')) {
+ deprecated[t[0]] = t[1];
+ }
+ });
+ });
+
+ return deprecated;
+ }
+ };
+
+ function Way() {
+ if (!(this instanceof Way)) {
+ return (new Way()).initialize(arguments);
+ } else if (arguments.length) {
+ this.initialize(arguments);
+ }
+ }
+
+ Entity.way = Way;
+
+ Way.prototype = Object.create(Entity.prototype);
+
+ _.extend(Way.prototype, {
+ type: 'way',
+ nodes: [],
+
+ copy: function(resolver, copies) {
+ if (copies[this.id])
+ return copies[this.id];
+
+ var copy = Entity.prototype.copy.call(this, resolver, copies);
+
+ var nodes = this.nodes.map(function(id) {
+ return resolver.entity(id).copy(resolver, copies).id;
+ });
+
+ copy = copy.update({nodes: nodes});
+ copies[this.id] = copy;
+
+ return copy;
+ },
+
+ extent: function(resolver) {
+ return resolver.transient(this, 'extent', function() {
+ var extent = Extent();
+ for (var i = 0; i < this.nodes.length; i++) {
+ var node = resolver.hasEntity(this.nodes[i]);
+ if (node) {
+ extent._extend(node.extent());
+ }
+ }
+ return extent;
+ });
+ },
+
+ first: function() {
+ return this.nodes[0];
+ },
+
+ last: function() {
+ return this.nodes[this.nodes.length - 1];
+ },
+
+ contains: function(node) {
+ return this.nodes.indexOf(node) >= 0;
+ },
+
+ affix: function(node) {
+ if (this.nodes[0] === node) return 'prefix';
+ if (this.nodes[this.nodes.length - 1] === node) return 'suffix';
+ },
+
+ layer: function() {
+ // explicit layer tag, clamp between -10, 10..
+ if (this.tags.layer !== undefined) {
+ return Math.max(-10, Math.min(+(this.tags.layer), 10));
+ }
+
+ // implied layer tag..
+ if (this.tags.location === 'overground') return 1;
+ if (this.tags.location === 'underground') return -1;
+ if (this.tags.location === 'underwater') return -10;
+
+ if (this.tags.power === 'line') return 10;
+ if (this.tags.power === 'minor_line') return 10;
+ if (this.tags.aerialway) return 10;
+ if (this.tags.bridge) return 1;
+ if (this.tags.cutting) return -1;
+ if (this.tags.tunnel) return -1;
+ if (this.tags.waterway) return -1;
+ if (this.tags.man_made === 'pipeline') return -10;
+ if (this.tags.boundary) return -10;
+ return 0;
+ },
+
+ isOneWay: function() {
+ // explicit oneway tag..
+ if (['yes', '1', '-1'].indexOf(this.tags.oneway) !== -1) { return true; }
+ if (['no', '0'].indexOf(this.tags.oneway) !== -1) { return false; }
+
+ // implied oneway tag..
+ for (var key in this.tags) {
+ if (key in oneWayTags && (this.tags[key] in oneWayTags[key]))
+ return true;
+ }
+ return false;
+ },
+
+ isClosed: function() {
+ return this.nodes.length > 0 && this.first() === this.last();
+ },
+
+ isConvex: function(resolver) {
+ if (!this.isClosed() || this.isDegenerate()) return null;
+
+ var nodes = _.uniq(resolver.childNodes(this)),
+ coords = _.map(nodes, 'loc'),
+ curr = 0, prev = 0;
+
+ for (var i = 0; i < coords.length; i++) {
+ var o = coords[(i+1) % coords.length],
+ a = coords[i],
+ b = coords[(i+2) % coords.length],
+ res = cross(o, a, b);
+
+ curr = (res > 0) ? 1 : (res < 0) ? -1 : 0;
+ if (curr === 0) {
+ continue;
+ } else if (prev && curr !== prev) {
+ return false;
+ }
+ prev = curr;
+ }
+ return true;
+ },
+
+ isArea: function() {
+ if (this.tags.area === 'yes')
+ return true;
+ if (!this.isClosed() || this.tags.area === 'no')
+ return false;
+ for (var key in this.tags)
+ if (key in iD.areaKeys && !(this.tags[key] in iD.areaKeys[key]))
+ return true;
+ return false;
+ },
+
+ isDegenerate: function() {
+ return _.uniq(this.nodes).length < (this.isArea() ? 3 : 2);
+ },
+
+ areAdjacent: function(n1, n2) {
+ for (var i = 0; i < this.nodes.length; i++) {
+ if (this.nodes[i] === n1) {
+ if (this.nodes[i - 1] === n2) return true;
+ if (this.nodes[i + 1] === n2) return true;
+ }
+ }
+ return false;
+ },
+
+ geometry: function(graph) {
+ return graph.transient(this, 'geometry', function() {
+ return this.isArea() ? 'area' : 'line';
+ });
+ },
+
+ addNode: function(id, index) {
+ var nodes = this.nodes.slice();
+ nodes.splice(index === undefined ? nodes.length : index, 0, id);
+ return this.update({nodes: nodes});
+ },
+
+ updateNode: function(id, index) {
+ var nodes = this.nodes.slice();
+ nodes.splice(index, 1, id);
+ return this.update({nodes: nodes});
+ },
+
+ replaceNode: function(needle, replacement) {
+ if (this.nodes.indexOf(needle) < 0)
+ return this;
+
+ var nodes = this.nodes.slice();
+ for (var i = 0; i < nodes.length; i++) {
+ if (nodes[i] === needle) {
+ nodes[i] = replacement;
+ }
+ }
+ return this.update({nodes: nodes});
+ },
+
+ removeNode: function(id) {
+ var nodes = [];
+
+ for (var i = 0; i < this.nodes.length; i++) {
+ var node = this.nodes[i];
+ if (node !== id && nodes[nodes.length - 1] !== node) {
+ nodes.push(node);
+ }
+ }
+
+ // Preserve circularity
+ if (this.nodes.length > 1 && this.first() === id && this.last() === id && nodes[nodes.length - 1] !== nodes[0]) {
+ nodes.push(nodes[0]);
+ }
+
+ return this.update({nodes: nodes});
+ },
+
+ asJXON: function(changeset_id) {
+ var r = {
+ way: {
+ '@id': this.osmId(),
+ '@version': this.version || 0,
+ nd: _.map(this.nodes, function(id) {
+ return { keyAttributes: { ref: Entity.id.toOSM(id) } };
+ }),
+ tag: _.map(this.tags, function(v, k) {
+ return { keyAttributes: { k: k, v: v } };
+ })
+ }
+ };
+ if (changeset_id) r.way['@changeset'] = changeset_id;
+ return r;
+ },
+
+ asGeoJSON: function(resolver) {
+ return resolver.transient(this, 'GeoJSON', function() {
+ var coordinates = _.map(resolver.childNodes(this), 'loc');
+ if (this.isArea() && this.isClosed()) {
+ return {
+ type: 'Polygon',
+ coordinates: [coordinates]
+ };
+ } else {
+ return {
+ type: 'LineString',
+ coordinates: coordinates
+ };
+ }
+ });
+ },
+
+ area: function(resolver) {
+ return resolver.transient(this, 'area', function() {
+ var nodes = resolver.childNodes(this);
+
+ var json = {
+ type: 'Polygon',
+ coordinates: [_.map(nodes, 'loc')]
+ };
+
+ if (!this.isClosed() && nodes.length) {
+ json.coordinates[0].push(nodes[0].loc);
+ }
+
+ var area = d3.geo.area(json);
+
+ // Heuristic for detecting counterclockwise winding order. Assumes
+ // that OpenStreetMap polygons are not hemisphere-spanning.
+ if (area > 2 * Math.PI) {
+ json.coordinates[0] = json.coordinates[0].reverse();
+ area = d3.geo.area(json);
+ }
+
+ return isNaN(area) ? 0 : area;
+ });
+ }
+ });
+
+ function Relation() {
+ if (!(this instanceof Relation)) {
+ return (new Relation()).initialize(arguments);
+ } else if (arguments.length) {
+ this.initialize(arguments);
+ }
+ }
+ Entity.relation = Relation;
+
+ Relation.prototype = Object.create(Entity.prototype);
+
+ Relation.creationOrder = function(a, b) {
+ var aId = parseInt(Entity.id.toOSM(a.id), 10);
+ var bId = parseInt(Entity.id.toOSM(b.id), 10);
+
+ if (aId < 0 || bId < 0) return aId - bId;
+ return bId - aId;
+ };
+
+ _.extend(Relation.prototype, {
+ type: 'relation',
+ members: [],
+
+ copy: function(resolver, copies) {
+ if (copies[this.id])
+ return copies[this.id];
+
+ var copy = Entity.prototype.copy.call(this, resolver, copies);
+
+ var members = this.members.map(function(member) {
+ return _.extend({}, member, {id: resolver.entity(member.id).copy(resolver, copies).id});
+ });
+
+ copy = copy.update({members: members});
+ copies[this.id] = copy;
+
+ return copy;
+ },
+
+ extent: function(resolver, memo) {
+ return resolver.transient(this, 'extent', function() {
+ if (memo && memo[this.id]) return Extent();
+ memo = memo || {};
+ memo[this.id] = true;
+
+ var extent = Extent();
+ for (var i = 0; i < this.members.length; i++) {
+ var member = resolver.hasEntity(this.members[i].id);
+ if (member) {
+ extent._extend(member.extent(resolver, memo));
+ }
+ }
+ return extent;
+ });
+ },
+
+ geometry: function(graph) {
+ return graph.transient(this, 'geometry', function() {
+ return this.isMultipolygon() ? 'area' : 'relation';
+ });
+ },
+
+ isDegenerate: function() {
+ return this.members.length === 0;
+ },
+
+ // Return an array of members, each extended with an 'index' property whose value
+ // is the member index.
+ indexedMembers: function() {
+ var result = new Array(this.members.length);
+ for (var i = 0; i < this.members.length; i++) {
+ result[i] = _.extend({}, this.members[i], {index: i});
+ }
+ return result;
+ },
+
+ // Return the first member with the given role. A copy of the member object
+ // is returned, extended with an 'index' property whose value is the member index.
+ memberByRole: function(role) {
+ for (var i = 0; i < this.members.length; i++) {
+ if (this.members[i].role === role) {
+ return _.extend({}, this.members[i], {index: i});
+ }
+ }
+ },
+
+ // Return the first member with the given id. A copy of the member object
+ // is returned, extended with an 'index' property whose value is the member index.
+ memberById: function(id) {
+ for (var i = 0; i < this.members.length; i++) {
+ if (this.members[i].id === id) {
+ return _.extend({}, this.members[i], {index: i});
+ }
+ }
+ },
+
+ // Return the first member with the given id and role. A copy of the member object
+ // is returned, extended with an 'index' property whose value is the member index.
+ memberByIdAndRole: function(id, role) {
+ for (var i = 0; i < this.members.length; i++) {
+ if (this.members[i].id === id && this.members[i].role === role) {
+ return _.extend({}, this.members[i], {index: i});
+ }
+ }
+ },
+
+ addMember: function(member, index) {
+ var members = this.members.slice();
+ members.splice(index === undefined ? members.length : index, 0, member);
+ return this.update({members: members});
+ },
+
+ updateMember: function(member, index) {
+ var members = this.members.slice();
+ members.splice(index, 1, _.extend({}, members[index], member));
+ return this.update({members: members});
+ },
+
+ removeMember: function(index) {
+ var members = this.members.slice();
+ members.splice(index, 1);
+ return this.update({members: members});
+ },
+
+ removeMembersWithID: function(id) {
+ var members = _.reject(this.members, function(m) { return m.id === id; });
+ return this.update({members: members});
+ },
+
+ // Wherever a member appears with id `needle.id`, replace it with a member
+ // with id `replacement.id`, type `replacement.type`, and the original role,
+ // unless a member already exists with that id and role. Return an updated
+ // relation.
+ replaceMember: function(needle, replacement) {
+ if (!this.memberById(needle.id))
+ return this;
+
+ var members = [];
+
+ for (var i = 0; i < this.members.length; i++) {
+ var member = this.members[i];
+ if (member.id !== needle.id) {
+ members.push(member);
+ } else if (!this.memberByIdAndRole(replacement.id, member.role)) {
+ members.push({id: replacement.id, type: replacement.type, role: member.role});
+ }
+ }
+
+ return this.update({members: members});
+ },
+
+ asJXON: function(changeset_id) {
+ var r = {
+ relation: {
+ '@id': this.osmId(),
+ '@version': this.version || 0,
+ member: _.map(this.members, function(member) {
+ return { keyAttributes: { type: member.type, role: member.role, ref: Entity.id.toOSM(member.id) } };
+ }),
+ tag: _.map(this.tags, function(v, k) {
+ return { keyAttributes: { k: k, v: v } };
+ })
+ }
+ };
+ if (changeset_id) r.relation['@changeset'] = changeset_id;
+ return r;
+ },
+
+ asGeoJSON: function(resolver) {
+ return resolver.transient(this, 'GeoJSON', function () {
+ if (this.isMultipolygon()) {
+ return {
+ type: 'MultiPolygon',
+ coordinates: this.multipolygon(resolver)
+ };
+ } else {
+ return {
+ type: 'FeatureCollection',
+ properties: this.tags,
+ features: this.members.map(function (member) {
+ return _.extend({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver));
+ })
+ };
+ }
+ });
+ },
+
+ area: function(resolver) {
+ return resolver.transient(this, 'area', function() {
+ return d3.geo.area(this.asGeoJSON(resolver));
+ });
+ },
+
+ isMultipolygon: function() {
+ return this.tags.type === 'multipolygon';
+ },
+
+ isComplete: function(resolver) {
+ for (var i = 0; i < this.members.length; i++) {
+ if (!resolver.hasEntity(this.members[i].id)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ isRestriction: function() {
+ return !!(this.tags.type && this.tags.type.match(/^restriction:?/));
+ },
+
+ // Returns an array [A0, ... An], each Ai being an array of node arrays [Nds0, ... Ndsm],
+ // where Nds0 is an outer ring and subsequent Ndsi's (if any i > 0) being inner rings.
+ //
+ // This corresponds to the structure needed for rendering a multipolygon path using a
+ // `evenodd` fill rule, as well as the structure of a GeoJSON MultiPolygon geometry.
+ //
+ // In the case of invalid geometries, this function will still return a result which
+ // includes the nodes of all way members, but some Nds may be unclosed and some inner
+ // rings not matched with the intended outer ring.
+ //
+ multipolygon: function(resolver) {
+ var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); }),
+ inners = this.members.filter(function(m) { return 'inner' === m.role; });
+
+ outers = joinWays(outers, resolver);
+ inners = joinWays(inners, resolver);
+
+ outers = outers.map(function(outer) { return _.map(outer.nodes, 'loc'); });
+ inners = inners.map(function(inner) { return _.map(inner.nodes, 'loc'); });
+
+ var result = outers.map(function(o) {
+ // Heuristic for detecting counterclockwise winding order. Assumes
+ // that OpenStreetMap polygons are not hemisphere-spanning.
+ return [d3.geo.area({type: 'Polygon', coordinates: [o]}) > 2 * Math.PI ? o.reverse() : o];
+ });
+
+ function findOuter(inner) {
+ var o, outer;
+
+ for (o = 0; o < outers.length; o++) {
+ outer = outers[o];
+ if (polygonContainsPolygon(outer, inner))
+ return o;
+ }
+
+ for (o = 0; o < outers.length; o++) {
+ outer = outers[o];
+ if (polygonIntersectsPolygon(outer, inner))
+ return o;
+ }
+ }
+
+ for (var i = 0; i < inners.length; i++) {
+ var inner = inners[i];
+
+ if (d3.geo.area({type: 'Polygon', coordinates: [inner]}) < 2 * Math.PI) {
+ inner = inner.reverse();
+ }
+
+ var o = findOuter(inners[i]);
+ if (o !== undefined)
+ result[o].push(inners[i]);
+ else
+ result.push([inners[i]]); // Invalid geometry
+ }
+
+ return result;
+ }
+ });
+
+ function Node() {
+ if (!(this instanceof Node)) {
+ return (new Node()).initialize(arguments);
+ } else if (arguments.length) {
+ this.initialize(arguments);
+ }
+ }
+
+ Entity.node = Node;
+
+ Node.prototype = Object.create(Entity.prototype);
+
+ _.extend(Node.prototype, {
+ type: 'node',
+
+ extent: function() {
+ return new Extent(this.loc);
+ },
+
+ geometry: function(graph) {
+ return graph.transient(this, 'geometry', function() {
+ return graph.isPoi(this) ? 'point' : 'vertex';
+ });
+ },
+
+ move: function(loc) {
+ return this.update({loc: loc});
+ },
+
+ isIntersection: function(resolver) {
+ return resolver.transient(this, 'isIntersection', function() {
+ return resolver.parentWays(this).filter(function(parent) {
+ return (parent.tags.highway ||
+ parent.tags.waterway ||
+ parent.tags.railway ||
+ parent.tags.aeroway) &&
+ parent.geometry(resolver) === 'line';
+ }).length > 1;
+ });
+ },
+
+ isHighwayIntersection: function(resolver) {
+ return resolver.transient(this, 'isHighwayIntersection', function() {
+ return resolver.parentWays(this).filter(function(parent) {
+ return parent.tags.highway && parent.geometry(resolver) === 'line';
+ }).length > 1;
+ });
+ },
+
+ asJXON: function(changeset_id) {
+ var r = {
+ node: {
+ '@id': this.osmId(),
+ '@lon': this.loc[0],
+ '@lat': this.loc[1],
+ '@version': (this.version || 0),
+ tag: _.map(this.tags, function(v, k) {
+ return { keyAttributes: { k: k, v: v } };
+ })
+ }
+ };
+ if (changeset_id) r.node['@changeset'] = changeset_id;
+ return r;
+ },
+
+ asGeoJSON: function() {
+ return {
+ type: 'Point',
+ coordinates: this.loc
+ };
+ }
+ });
+
function Turn(turn) {
if (!(this instanceof Turn))
return new Turn(turn);
@@ -155,14 +947,14 @@
var splitIndex, wayA, wayB, indexA, indexB;
if (isClosingNode) {
splitIndex = Math.ceil(way.nodes.length / 2); // split at midpoint
- wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)});
- wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
+ wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)});
+ wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
indexA = 1;
indexB = way.nodes.length - 2;
} else {
splitIndex = _.indexOf(way.nodes, vertex.id, 1); // split at vertexid
- wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)});
- wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
+ wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)});
+ wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
indexA = splitIndex - 1;
indexB = splitIndex + 1;
}
diff --git a/js/lib/id/index.js b/js/lib/id/index.js
new file mode 100644
index 000000000..949055cba
--- /dev/null
+++ b/js/lib/id/index.js
@@ -0,0 +1,5368 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ (factory((global.iD = global.iD || {})));
+}(this, function (exports) { 'use strict';
+
+ function AddEntity(way) {
+ return function(graph) {
+ return graph.replace(way);
+ };
+ }
+
+ function Extent(min, max) {
+ if (!(this instanceof Extent)) return new Extent(min, max);
+ if (min instanceof Extent) {
+ return min;
+ } else if (min && min.length === 2 && min[0].length === 2 && min[1].length === 2) {
+ this[0] = min[0];
+ this[1] = min[1];
+ } else {
+ this[0] = min || [ Infinity, Infinity];
+ this[1] = max || min || [-Infinity, -Infinity];
+ }
+ }
+
+ Extent.prototype = new Array(2);
+
+ _.extend(Extent.prototype, {
+ equals: function (obj) {
+ return this[0][0] === obj[0][0] &&
+ this[0][1] === obj[0][1] &&
+ this[1][0] === obj[1][0] &&
+ this[1][1] === obj[1][1];
+ },
+
+ extend: function(obj) {
+ if (!(obj instanceof Extent)) obj = new Extent(obj);
+ return Extent([Math.min(obj[0][0], this[0][0]),
+ Math.min(obj[0][1], this[0][1])],
+ [Math.max(obj[1][0], this[1][0]),
+ Math.max(obj[1][1], this[1][1])]);
+ },
+
+ _extend: function(extent) {
+ this[0][0] = Math.min(extent[0][0], this[0][0]);
+ this[0][1] = Math.min(extent[0][1], this[0][1]);
+ this[1][0] = Math.max(extent[1][0], this[1][0]);
+ this[1][1] = Math.max(extent[1][1], this[1][1]);
+ },
+
+ area: function() {
+ return Math.abs((this[1][0] - this[0][0]) * (this[1][1] - this[0][1]));
+ },
+
+ center: function() {
+ return [(this[0][0] + this[1][0]) / 2,
+ (this[0][1] + this[1][1]) / 2];
+ },
+
+ rectangle: function() {
+ return [this[0][0], this[0][1], this[1][0], this[1][1]];
+ },
+
+ polygon: function() {
+ return [
+ [this[0][0], this[0][1]],
+ [this[0][0], this[1][1]],
+ [this[1][0], this[1][1]],
+ [this[1][0], this[0][1]],
+ [this[0][0], this[0][1]]
+ ];
+ },
+
+ contains: function(obj) {
+ if (!(obj instanceof Extent)) obj = new Extent(obj);
+ return obj[0][0] >= this[0][0] &&
+ obj[0][1] >= this[0][1] &&
+ obj[1][0] <= this[1][0] &&
+ obj[1][1] <= this[1][1];
+ },
+
+ intersects: function(obj) {
+ if (!(obj instanceof Extent)) obj = new Extent(obj);
+ return obj[0][0] <= this[1][0] &&
+ obj[0][1] <= this[1][1] &&
+ obj[1][0] >= this[0][0] &&
+ obj[1][1] >= this[0][1];
+ },
+
+ intersection: function(obj) {
+ if (!this.intersects(obj)) return new Extent();
+ return new Extent([Math.max(obj[0][0], this[0][0]),
+ Math.max(obj[0][1], this[0][1])],
+ [Math.min(obj[1][0], this[1][0]),
+ Math.min(obj[1][1], this[1][1])]);
+ },
+
+ percentContainedIn: function(obj) {
+ if (!(obj instanceof Extent)) obj = new Extent(obj);
+ var a1 = this.intersection(obj).area(),
+ a2 = this.area();
+
+ if (a1 === Infinity || a2 === Infinity || a1 === 0 || a2 === 0) {
+ return 0;
+ } else {
+ return a1 / a2;
+ }
+ },
+
+ padByMeters: function(meters) {
+ var dLat = metersToLat(meters),
+ dLon = metersToLon(meters, this.center()[1]);
+ return Extent(
+ [this[0][0] - dLon, this[0][1] - dLat],
+ [this[1][0] + dLon, this[1][1] + dLat]);
+ },
+
+ toParam: function() {
+ return this.rectangle().join(',');
+ }
+
+ });
+
+ function interestingTag(key) {
+ return key !== 'attribution' &&
+ key !== 'created_by' &&
+ key !== 'source' &&
+ key !== 'odbl' &&
+ key.indexOf('tiger:') !== 0;
+
+ }
+
+ var oneWayTags = {
+ 'aerialway': {
+ 'chair_lift': true,
+ 'mixed_lift': true,
+ 't-bar': true,
+ 'j-bar': true,
+ 'platter': true,
+ 'rope_tow': true,
+ 'magic_carpet': true,
+ 'yes': true
+ },
+ 'highway': {
+ 'motorway': true,
+ 'motorway_link': true
+ },
+ 'junction': {
+ 'roundabout': true
+ },
+ 'man_made': {
+ 'piste:halfpipe': true
+ },
+ 'piste:type': {
+ 'downhill': true,
+ 'sled': true,
+ 'yes': true
+ },
+ 'waterway': {
+ 'river': true,
+ 'stream': true
+ }
+ };
+
+ var pavedTags = {
+ 'surface': {
+ 'paved': true,
+ 'asphalt': true,
+ 'concrete': true
+ },
+ 'tracktype': {
+ 'grade1': true
+ }
+ };
+
+ function Entity(attrs) {
+ // For prototypal inheritance.
+ if (this instanceof Entity) return;
+
+ // Create the appropriate subtype.
+ if (attrs && attrs.type) {
+ return Entity[attrs.type].apply(this, arguments);
+ } else if (attrs && attrs.id) {
+ return Entity[Entity.id.type(attrs.id)].apply(this, arguments);
+ }
+
+ // Initialize a generic Entity (used only in tests).
+ return (new Entity()).initialize(arguments);
+ }
+
+ Entity.id = function(type) {
+ return Entity.id.fromOSM(type, Entity.id.next[type]--);
+ };
+
+ Entity.id.next = {node: -1, way: -1, relation: -1};
+
+ Entity.id.fromOSM = function(type, id) {
+ return type[0] + id;
+ };
+
+ Entity.id.toOSM = function(id) {
+ return id.slice(1);
+ };
+
+ Entity.id.type = function(id) {
+ return {'n': 'node', 'w': 'way', 'r': 'relation'}[id[0]];
+ };
+
+ // A function suitable for use as the second argument to d3.selection#data().
+ Entity.key = function(entity) {
+ return entity.id + 'v' + (entity.v || 0);
+ };
+
+ Entity.prototype = {
+ tags: {},
+
+ initialize: function(sources) {
+ for (var i = 0; i < sources.length; ++i) {
+ var source = sources[i];
+ for (var prop in source) {
+ if (Object.prototype.hasOwnProperty.call(source, prop)) {
+ if (source[prop] === undefined) {
+ delete this[prop];
+ } else {
+ this[prop] = source[prop];
+ }
+ }
+ }
+ }
+
+ if (!this.id && this.type) {
+ this.id = Entity.id(this.type);
+ }
+ if (!this.hasOwnProperty('visible')) {
+ this.visible = true;
+ }
+
+ if (iD.debug) {
+ Object.freeze(this);
+ Object.freeze(this.tags);
+
+ if (this.loc) Object.freeze(this.loc);
+ if (this.nodes) Object.freeze(this.nodes);
+ if (this.members) Object.freeze(this.members);
+ }
+
+ return this;
+ },
+
+ copy: function(resolver, copies) {
+ if (copies[this.id])
+ return copies[this.id];
+
+ var copy = Entity(this, {id: undefined, user: undefined, version: undefined});
+ copies[this.id] = copy;
+
+ return copy;
+ },
+
+ osmId: function() {
+ return Entity.id.toOSM(this.id);
+ },
+
+ isNew: function() {
+ return this.osmId() < 0;
+ },
+
+ update: function(attrs) {
+ return Entity(this, attrs, {v: 1 + (this.v || 0)});
+ },
+
+ mergeTags: function(tags) {
+ var merged = _.clone(this.tags), changed = false;
+ for (var k in tags) {
+ var t1 = merged[k],
+ t2 = tags[k];
+ if (!t1) {
+ changed = true;
+ merged[k] = t2;
+ } else if (t1 !== t2) {
+ changed = true;
+ merged[k] = _.union(t1.split(/;\s*/), t2.split(/;\s*/)).join(';');
+ }
+ }
+ return changed ? this.update({tags: merged}) : this;
+ },
+
+ intersects: function(extent, resolver) {
+ return this.extent(resolver).intersects(extent);
+ },
+
+ isUsed: function(resolver) {
+ return _.without(Object.keys(this.tags), 'area').length > 0 ||
+ resolver.parentRelations(this).length > 0;
+ },
+
+ hasInterestingTags: function() {
+ return _.keys(this.tags).some(interestingTag);
+ },
+
+ isHighwayIntersection: function() {
+ return false;
+ },
+
+ deprecatedTags: function() {
+ var tags = _.toPairs(this.tags);
+ var deprecated = {};
+
+ iD.data.deprecated.forEach(function(d) {
+ var match = _.toPairs(d.old)[0];
+ tags.forEach(function(t) {
+ if (t[0] === match[0] &&
+ (t[1] === match[1] || match[1] === '*')) {
+ deprecated[t[0]] = t[1];
+ }
+ });
+ });
+
+ return deprecated;
+ }
+ };
+
+ function Way() {
+ if (!(this instanceof Way)) {
+ return (new Way()).initialize(arguments);
+ } else if (arguments.length) {
+ this.initialize(arguments);
+ }
+ }
+
+ Entity.way = Way;
+
+ Way.prototype = Object.create(Entity.prototype);
+
+ _.extend(Way.prototype, {
+ type: 'way',
+ nodes: [],
+
+ copy: function(resolver, copies) {
+ if (copies[this.id])
+ return copies[this.id];
+
+ var copy = Entity.prototype.copy.call(this, resolver, copies);
+
+ var nodes = this.nodes.map(function(id) {
+ return resolver.entity(id).copy(resolver, copies).id;
+ });
+
+ copy = copy.update({nodes: nodes});
+ copies[this.id] = copy;
+
+ return copy;
+ },
+
+ extent: function(resolver) {
+ return resolver.transient(this, 'extent', function() {
+ var extent = Extent();
+ for (var i = 0; i < this.nodes.length; i++) {
+ var node = resolver.hasEntity(this.nodes[i]);
+ if (node) {
+ extent._extend(node.extent());
+ }
+ }
+ return extent;
+ });
+ },
+
+ first: function() {
+ return this.nodes[0];
+ },
+
+ last: function() {
+ return this.nodes[this.nodes.length - 1];
+ },
+
+ contains: function(node) {
+ return this.nodes.indexOf(node) >= 0;
+ },
+
+ affix: function(node) {
+ if (this.nodes[0] === node) return 'prefix';
+ if (this.nodes[this.nodes.length - 1] === node) return 'suffix';
+ },
+
+ layer: function() {
+ // explicit layer tag, clamp between -10, 10..
+ if (this.tags.layer !== undefined) {
+ return Math.max(-10, Math.min(+(this.tags.layer), 10));
+ }
+
+ // implied layer tag..
+ if (this.tags.location === 'overground') return 1;
+ if (this.tags.location === 'underground') return -1;
+ if (this.tags.location === 'underwater') return -10;
+
+ if (this.tags.power === 'line') return 10;
+ if (this.tags.power === 'minor_line') return 10;
+ if (this.tags.aerialway) return 10;
+ if (this.tags.bridge) return 1;
+ if (this.tags.cutting) return -1;
+ if (this.tags.tunnel) return -1;
+ if (this.tags.waterway) return -1;
+ if (this.tags.man_made === 'pipeline') return -10;
+ if (this.tags.boundary) return -10;
+ return 0;
+ },
+
+ isOneWay: function() {
+ // explicit oneway tag..
+ if (['yes', '1', '-1'].indexOf(this.tags.oneway) !== -1) { return true; }
+ if (['no', '0'].indexOf(this.tags.oneway) !== -1) { return false; }
+
+ // implied oneway tag..
+ for (var key in this.tags) {
+ if (key in oneWayTags && (this.tags[key] in oneWayTags[key]))
+ return true;
+ }
+ return false;
+ },
+
+ isClosed: function() {
+ return this.nodes.length > 0 && this.first() === this.last();
+ },
+
+ isConvex: function(resolver) {
+ if (!this.isClosed() || this.isDegenerate()) return null;
+
+ var nodes = _.uniq(resolver.childNodes(this)),
+ coords = _.map(nodes, 'loc'),
+ curr = 0, prev = 0;
+
+ for (var i = 0; i < coords.length; i++) {
+ var o = coords[(i+1) % coords.length],
+ a = coords[i],
+ b = coords[(i+2) % coords.length],
+ res = cross(o, a, b);
+
+ curr = (res > 0) ? 1 : (res < 0) ? -1 : 0;
+ if (curr === 0) {
+ continue;
+ } else if (prev && curr !== prev) {
+ return false;
+ }
+ prev = curr;
+ }
+ return true;
+ },
+
+ isArea: function() {
+ if (this.tags.area === 'yes')
+ return true;
+ if (!this.isClosed() || this.tags.area === 'no')
+ return false;
+ for (var key in this.tags)
+ if (key in iD.areaKeys && !(this.tags[key] in iD.areaKeys[key]))
+ return true;
+ return false;
+ },
+
+ isDegenerate: function() {
+ return _.uniq(this.nodes).length < (this.isArea() ? 3 : 2);
+ },
+
+ areAdjacent: function(n1, n2) {
+ for (var i = 0; i < this.nodes.length; i++) {
+ if (this.nodes[i] === n1) {
+ if (this.nodes[i - 1] === n2) return true;
+ if (this.nodes[i + 1] === n2) return true;
+ }
+ }
+ return false;
+ },
+
+ geometry: function(graph) {
+ return graph.transient(this, 'geometry', function() {
+ return this.isArea() ? 'area' : 'line';
+ });
+ },
+
+ addNode: function(id, index) {
+ var nodes = this.nodes.slice();
+ nodes.splice(index === undefined ? nodes.length : index, 0, id);
+ return this.update({nodes: nodes});
+ },
+
+ updateNode: function(id, index) {
+ var nodes = this.nodes.slice();
+ nodes.splice(index, 1, id);
+ return this.update({nodes: nodes});
+ },
+
+ replaceNode: function(needle, replacement) {
+ if (this.nodes.indexOf(needle) < 0)
+ return this;
+
+ var nodes = this.nodes.slice();
+ for (var i = 0; i < nodes.length; i++) {
+ if (nodes[i] === needle) {
+ nodes[i] = replacement;
+ }
+ }
+ return this.update({nodes: nodes});
+ },
+
+ removeNode: function(id) {
+ var nodes = [];
+
+ for (var i = 0; i < this.nodes.length; i++) {
+ var node = this.nodes[i];
+ if (node !== id && nodes[nodes.length - 1] !== node) {
+ nodes.push(node);
+ }
+ }
+
+ // Preserve circularity
+ if (this.nodes.length > 1 && this.first() === id && this.last() === id && nodes[nodes.length - 1] !== nodes[0]) {
+ nodes.push(nodes[0]);
+ }
+
+ return this.update({nodes: nodes});
+ },
+
+ asJXON: function(changeset_id) {
+ var r = {
+ way: {
+ '@id': this.osmId(),
+ '@version': this.version || 0,
+ nd: _.map(this.nodes, function(id) {
+ return { keyAttributes: { ref: Entity.id.toOSM(id) } };
+ }),
+ tag: _.map(this.tags, function(v, k) {
+ return { keyAttributes: { k: k, v: v } };
+ })
+ }
+ };
+ if (changeset_id) r.way['@changeset'] = changeset_id;
+ return r;
+ },
+
+ asGeoJSON: function(resolver) {
+ return resolver.transient(this, 'GeoJSON', function() {
+ var coordinates = _.map(resolver.childNodes(this), 'loc');
+ if (this.isArea() && this.isClosed()) {
+ return {
+ type: 'Polygon',
+ coordinates: [coordinates]
+ };
+ } else {
+ return {
+ type: 'LineString',
+ coordinates: coordinates
+ };
+ }
+ });
+ },
+
+ area: function(resolver) {
+ return resolver.transient(this, 'area', function() {
+ var nodes = resolver.childNodes(this);
+
+ var json = {
+ type: 'Polygon',
+ coordinates: [_.map(nodes, 'loc')]
+ };
+
+ if (!this.isClosed() && nodes.length) {
+ json.coordinates[0].push(nodes[0].loc);
+ }
+
+ var area = d3.geo.area(json);
+
+ // Heuristic for detecting counterclockwise winding order. Assumes
+ // that OpenStreetMap polygons are not hemisphere-spanning.
+ if (area > 2 * Math.PI) {
+ json.coordinates[0] = json.coordinates[0].reverse();
+ area = d3.geo.area(json);
+ }
+
+ return isNaN(area) ? 0 : area;
+ });
+ }
+ });
+
+ function Relation() {
+ if (!(this instanceof Relation)) {
+ return (new Relation()).initialize(arguments);
+ } else if (arguments.length) {
+ this.initialize(arguments);
+ }
+ }
+ Entity.relation = Relation;
+
+ Relation.prototype = Object.create(Entity.prototype);
+
+ Relation.creationOrder = function(a, b) {
+ var aId = parseInt(Entity.id.toOSM(a.id), 10);
+ var bId = parseInt(Entity.id.toOSM(b.id), 10);
+
+ if (aId < 0 || bId < 0) return aId - bId;
+ return bId - aId;
+ };
+
+ _.extend(Relation.prototype, {
+ type: 'relation',
+ members: [],
+
+ copy: function(resolver, copies) {
+ if (copies[this.id])
+ return copies[this.id];
+
+ var copy = Entity.prototype.copy.call(this, resolver, copies);
+
+ var members = this.members.map(function(member) {
+ return _.extend({}, member, {id: resolver.entity(member.id).copy(resolver, copies).id});
+ });
+
+ copy = copy.update({members: members});
+ copies[this.id] = copy;
+
+ return copy;
+ },
+
+ extent: function(resolver, memo) {
+ return resolver.transient(this, 'extent', function() {
+ if (memo && memo[this.id]) return Extent();
+ memo = memo || {};
+ memo[this.id] = true;
+
+ var extent = Extent();
+ for (var i = 0; i < this.members.length; i++) {
+ var member = resolver.hasEntity(this.members[i].id);
+ if (member) {
+ extent._extend(member.extent(resolver, memo));
+ }
+ }
+ return extent;
+ });
+ },
+
+ geometry: function(graph) {
+ return graph.transient(this, 'geometry', function() {
+ return this.isMultipolygon() ? 'area' : 'relation';
+ });
+ },
+
+ isDegenerate: function() {
+ return this.members.length === 0;
+ },
+
+ // Return an array of members, each extended with an 'index' property whose value
+ // is the member index.
+ indexedMembers: function() {
+ var result = new Array(this.members.length);
+ for (var i = 0; i < this.members.length; i++) {
+ result[i] = _.extend({}, this.members[i], {index: i});
+ }
+ return result;
+ },
+
+ // Return the first member with the given role. A copy of the member object
+ // is returned, extended with an 'index' property whose value is the member index.
+ memberByRole: function(role) {
+ for (var i = 0; i < this.members.length; i++) {
+ if (this.members[i].role === role) {
+ return _.extend({}, this.members[i], {index: i});
+ }
+ }
+ },
+
+ // Return the first member with the given id. A copy of the member object
+ // is returned, extended with an 'index' property whose value is the member index.
+ memberById: function(id) {
+ for (var i = 0; i < this.members.length; i++) {
+ if (this.members[i].id === id) {
+ return _.extend({}, this.members[i], {index: i});
+ }
+ }
+ },
+
+ // Return the first member with the given id and role. A copy of the member object
+ // is returned, extended with an 'index' property whose value is the member index.
+ memberByIdAndRole: function(id, role) {
+ for (var i = 0; i < this.members.length; i++) {
+ if (this.members[i].id === id && this.members[i].role === role) {
+ return _.extend({}, this.members[i], {index: i});
+ }
+ }
+ },
+
+ addMember: function(member, index) {
+ var members = this.members.slice();
+ members.splice(index === undefined ? members.length : index, 0, member);
+ return this.update({members: members});
+ },
+
+ updateMember: function(member, index) {
+ var members = this.members.slice();
+ members.splice(index, 1, _.extend({}, members[index], member));
+ return this.update({members: members});
+ },
+
+ removeMember: function(index) {
+ var members = this.members.slice();
+ members.splice(index, 1);
+ return this.update({members: members});
+ },
+
+ removeMembersWithID: function(id) {
+ var members = _.reject(this.members, function(m) { return m.id === id; });
+ return this.update({members: members});
+ },
+
+ // Wherever a member appears with id `needle.id`, replace it with a member
+ // with id `replacement.id`, type `replacement.type`, and the original role,
+ // unless a member already exists with that id and role. Return an updated
+ // relation.
+ replaceMember: function(needle, replacement) {
+ if (!this.memberById(needle.id))
+ return this;
+
+ var members = [];
+
+ for (var i = 0; i < this.members.length; i++) {
+ var member = this.members[i];
+ if (member.id !== needle.id) {
+ members.push(member);
+ } else if (!this.memberByIdAndRole(replacement.id, member.role)) {
+ members.push({id: replacement.id, type: replacement.type, role: member.role});
+ }
+ }
+
+ return this.update({members: members});
+ },
+
+ asJXON: function(changeset_id) {
+ var r = {
+ relation: {
+ '@id': this.osmId(),
+ '@version': this.version || 0,
+ member: _.map(this.members, function(member) {
+ return { keyAttributes: { type: member.type, role: member.role, ref: Entity.id.toOSM(member.id) } };
+ }),
+ tag: _.map(this.tags, function(v, k) {
+ return { keyAttributes: { k: k, v: v } };
+ })
+ }
+ };
+ if (changeset_id) r.relation['@changeset'] = changeset_id;
+ return r;
+ },
+
+ asGeoJSON: function(resolver) {
+ return resolver.transient(this, 'GeoJSON', function () {
+ if (this.isMultipolygon()) {
+ return {
+ type: 'MultiPolygon',
+ coordinates: this.multipolygon(resolver)
+ };
+ } else {
+ return {
+ type: 'FeatureCollection',
+ properties: this.tags,
+ features: this.members.map(function (member) {
+ return _.extend({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver));
+ })
+ };
+ }
+ });
+ },
+
+ area: function(resolver) {
+ return resolver.transient(this, 'area', function() {
+ return d3.geo.area(this.asGeoJSON(resolver));
+ });
+ },
+
+ isMultipolygon: function() {
+ return this.tags.type === 'multipolygon';
+ },
+
+ isComplete: function(resolver) {
+ for (var i = 0; i < this.members.length; i++) {
+ if (!resolver.hasEntity(this.members[i].id)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ isRestriction: function() {
+ return !!(this.tags.type && this.tags.type.match(/^restriction:?/));
+ },
+
+ // Returns an array [A0, ... An], each Ai being an array of node arrays [Nds0, ... Ndsm],
+ // where Nds0 is an outer ring and subsequent Ndsi's (if any i > 0) being inner rings.
+ //
+ // This corresponds to the structure needed for rendering a multipolygon path using a
+ // `evenodd` fill rule, as well as the structure of a GeoJSON MultiPolygon geometry.
+ //
+ // In the case of invalid geometries, this function will still return a result which
+ // includes the nodes of all way members, but some Nds may be unclosed and some inner
+ // rings not matched with the intended outer ring.
+ //
+ multipolygon: function(resolver) {
+ var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); }),
+ inners = this.members.filter(function(m) { return 'inner' === m.role; });
+
+ outers = joinWays(outers, resolver);
+ inners = joinWays(inners, resolver);
+
+ outers = outers.map(function(outer) { return _.map(outer.nodes, 'loc'); });
+ inners = inners.map(function(inner) { return _.map(inner.nodes, 'loc'); });
+
+ var result = outers.map(function(o) {
+ // Heuristic for detecting counterclockwise winding order. Assumes
+ // that OpenStreetMap polygons are not hemisphere-spanning.
+ return [d3.geo.area({type: 'Polygon', coordinates: [o]}) > 2 * Math.PI ? o.reverse() : o];
+ });
+
+ function findOuter(inner) {
+ var o, outer;
+
+ for (o = 0; o < outers.length; o++) {
+ outer = outers[o];
+ if (polygonContainsPolygon(outer, inner))
+ return o;
+ }
+
+ for (o = 0; o < outers.length; o++) {
+ outer = outers[o];
+ if (polygonIntersectsPolygon(outer, inner))
+ return o;
+ }
+ }
+
+ for (var i = 0; i < inners.length; i++) {
+ var inner = inners[i];
+
+ if (d3.geo.area({type: 'Polygon', coordinates: [inner]}) < 2 * Math.PI) {
+ inner = inner.reverse();
+ }
+
+ var o = findOuter(inners[i]);
+ if (o !== undefined)
+ result[o].push(inners[i]);
+ else
+ result.push([inners[i]]); // Invalid geometry
+ }
+
+ return result;
+ }
+ });
+
+ function Node() {
+ if (!(this instanceof Node)) {
+ return (new Node()).initialize(arguments);
+ } else if (arguments.length) {
+ this.initialize(arguments);
+ }
+ }
+
+ Entity.node = Node;
+
+ Node.prototype = Object.create(Entity.prototype);
+
+ _.extend(Node.prototype, {
+ type: 'node',
+
+ extent: function() {
+ return new Extent(this.loc);
+ },
+
+ geometry: function(graph) {
+ return graph.transient(this, 'geometry', function() {
+ return graph.isPoi(this) ? 'point' : 'vertex';
+ });
+ },
+
+ move: function(loc) {
+ return this.update({loc: loc});
+ },
+
+ isIntersection: function(resolver) {
+ return resolver.transient(this, 'isIntersection', function() {
+ return resolver.parentWays(this).filter(function(parent) {
+ return (parent.tags.highway ||
+ parent.tags.waterway ||
+ parent.tags.railway ||
+ parent.tags.aeroway) &&
+ parent.geometry(resolver) === 'line';
+ }).length > 1;
+ });
+ },
+
+ isHighwayIntersection: function(resolver) {
+ return resolver.transient(this, 'isHighwayIntersection', function() {
+ return resolver.parentWays(this).filter(function(parent) {
+ return parent.tags.highway && parent.geometry(resolver) === 'line';
+ }).length > 1;
+ });
+ },
+
+ asJXON: function(changeset_id) {
+ var r = {
+ node: {
+ '@id': this.osmId(),
+ '@lon': this.loc[0],
+ '@lat': this.loc[1],
+ '@version': (this.version || 0),
+ tag: _.map(this.tags, function(v, k) {
+ return { keyAttributes: { k: k, v: v } };
+ })
+ }
+ };
+ if (changeset_id) r.node['@changeset'] = changeset_id;
+ return r;
+ },
+
+ asGeoJSON: function() {
+ return {
+ type: 'Point',
+ coordinates: this.loc
+ };
+ }
+ });
+
+ function Connection(useHttps) {
+ if (typeof useHttps !== 'boolean') {
+ useHttps = window.location.protocol === 'https:';
+ }
+
+ var event = d3.dispatch('authenticating', 'authenticated', 'auth', 'loading', 'loaded'),
+ protocol = useHttps ? 'https:' : 'http:',
+ url = protocol + '//www.openstreetmap.org',
+ connection = {},
+ inflight = {},
+ loadedTiles = {},
+ tileZoom = 16,
+ oauth = osmAuth({
+ url: protocol + '//www.openstreetmap.org',
+ oauth_consumer_key: '5A043yRSEugj4DJ5TljuapfnrflWDte8jTOcWLlT',
+ oauth_secret: 'aB3jKq1TRsCOUrfOIZ6oQMEDmv2ptV76PA54NGLL',
+ loading: authenticating,
+ done: authenticated
+ }),
+ ndStr = 'nd',
+ tagStr = 'tag',
+ memberStr = 'member',
+ nodeStr = 'node',
+ wayStr = 'way',
+ relationStr = 'relation',
+ userDetails,
+ off;
+
+
+ connection.changesetURL = function(changesetId) {
+ return url + '/changeset/' + changesetId;
+ };
+
+ connection.changesetsURL = function(center, zoom) {
+ var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
+ return url + '/history#map=' +
+ Math.floor(zoom) + '/' +
+ center[1].toFixed(precision) + '/' +
+ center[0].toFixed(precision);
+ };
+
+ connection.entityURL = function(entity) {
+ return url + '/' + entity.type + '/' + entity.osmId();
+ };
+
+ connection.userURL = function(username) {
+ return url + '/user/' + username;
+ };
+
+ connection.loadFromURL = function(url, callback) {
+ function done(err, dom) {
+ return callback(err, parse(dom));
+ }
+ return d3.xml(url).get(done);
+ };
+
+ connection.loadEntity = function(id, callback) {
+ var type = Entity.id.type(id),
+ osmID = Entity.id.toOSM(id);
+
+ connection.loadFromURL(
+ url + '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : ''),
+ function(err, entities) {
+ if (callback) callback(err, {data: entities});
+ });
+ };
+
+ connection.loadEntityVersion = function(id, version, callback) {
+ var type = Entity.id.type(id),
+ osmID = Entity.id.toOSM(id);
+
+ connection.loadFromURL(
+ url + '/api/0.6/' + type + '/' + osmID + '/' + version,
+ function(err, entities) {
+ if (callback) callback(err, {data: entities});
+ });
+ };
+
+ connection.loadMultiple = function(ids, callback) {
+ _.each(_.groupBy(_.uniq(ids), Entity.id.type), function(v, k) {
+ var type = k + 's',
+ osmIDs = _.map(v, Entity.id.toOSM);
+
+ _.each(_.chunk(osmIDs, 150), function(arr) {
+ connection.loadFromURL(
+ url + '/api/0.6/' + type + '?' + type + '=' + arr.join(),
+ function(err, entities) {
+ if (callback) callback(err, {data: entities});
+ });
+ });
+ });
+ };
+
+ function authenticating() {
+ event.authenticating();
+ }
+
+ function authenticated() {
+ event.authenticated();
+ }
+
+ function getLoc(attrs) {
+ var lon = attrs.lon && attrs.lon.value,
+ lat = attrs.lat && attrs.lat.value;
+ return [parseFloat(lon), parseFloat(lat)];
+ }
+
+ function getNodes(obj) {
+ var elems = obj.getElementsByTagName(ndStr),
+ nodes = new Array(elems.length);
+ for (var i = 0, l = elems.length; i < l; i++) {
+ nodes[i] = 'n' + elems[i].attributes.ref.value;
+ }
+ return nodes;
+ }
+
+ function getTags(obj) {
+ var elems = obj.getElementsByTagName(tagStr),
+ tags = {};
+ for (var i = 0, l = elems.length; i < l; i++) {
+ var attrs = elems[i].attributes;
+ tags[attrs.k.value] = attrs.v.value;
+ }
+ return tags;
+ }
+
+ function getMembers(obj) {
+ var elems = obj.getElementsByTagName(memberStr),
+ members = new Array(elems.length);
+ for (var i = 0, l = elems.length; i < l; i++) {
+ var attrs = elems[i].attributes;
+ members[i] = {
+ id: attrs.type.value[0] + attrs.ref.value,
+ type: attrs.type.value,
+ role: attrs.role.value
+ };
+ }
+ return members;
+ }
+
+ function getVisible(attrs) {
+ return (!attrs.visible || attrs.visible.value !== 'false');
+ }
+
+ var parsers = {
+ node: function nodeData(obj) {
+ var attrs = obj.attributes;
+ return new Node({
+ id: Entity.id.fromOSM(nodeStr, attrs.id.value),
+ loc: getLoc(attrs),
+ version: attrs.version.value,
+ user: attrs.user && attrs.user.value,
+ tags: getTags(obj),
+ visible: getVisible(attrs)
+ });
+ },
+
+ way: function wayData(obj) {
+ var attrs = obj.attributes;
+ return new Way({
+ id: Entity.id.fromOSM(wayStr, attrs.id.value),
+ version: attrs.version.value,
+ user: attrs.user && attrs.user.value,
+ tags: getTags(obj),
+ nodes: getNodes(obj),
+ visible: getVisible(attrs)
+ });
+ },
+
+ relation: function relationData(obj) {
+ var attrs = obj.attributes;
+ return new Relation({
+ id: Entity.id.fromOSM(relationStr, attrs.id.value),
+ version: attrs.version.value,
+ user: attrs.user && attrs.user.value,
+ tags: getTags(obj),
+ members: getMembers(obj),
+ visible: getVisible(attrs)
+ });
+ }
+ };
+
+ function parse(dom) {
+ if (!dom || !dom.childNodes) return;
+
+ var root = dom.childNodes[0],
+ children = root.childNodes,
+ entities = [];
+
+ for (var i = 0, l = children.length; i < l; i++) {
+ var child = children[i],
+ parser = parsers[child.nodeName];
+ if (parser) {
+ entities.push(parser(child));
+ }
+ }
+
+ return entities;
+ }
+
+ connection.authenticated = function() {
+ return oauth.authenticated();
+ };
+
+ // Generate Changeset XML. Returns a string.
+ connection.changesetJXON = function(tags) {
+ return {
+ osm: {
+ changeset: {
+ tag: _.map(tags, function(value, key) {
+ return { '@k': key, '@v': value };
+ }),
+ '@version': 0.6,
+ '@generator': 'iD'
+ }
+ }
+ };
+ };
+
+ // Generate [osmChange](http://wiki.openstreetmap.org/wiki/OsmChange)
+ // XML. Returns a string.
+ connection.osmChangeJXON = function(changeset_id, changes) {
+ function nest(x, order) {
+ var groups = {};
+ for (var i = 0; i < x.length; i++) {
+ var tagName = Object.keys(x[i])[0];
+ if (!groups[tagName]) groups[tagName] = [];
+ groups[tagName].push(x[i][tagName]);
+ }
+ var ordered = {};
+ order.forEach(function(o) {
+ if (groups[o]) ordered[o] = groups[o];
+ });
+ return ordered;
+ }
+
+ function rep(entity) {
+ return entity.asJXON(changeset_id);
+ }
+
+ return {
+ osmChange: {
+ '@version': 0.6,
+ '@generator': 'iD',
+ 'create': nest(changes.created.map(rep), ['node', 'way', 'relation']),
+ 'modify': nest(changes.modified.map(rep), ['node', 'way', 'relation']),
+ 'delete': _.extend(nest(changes.deleted.map(rep), ['relation', 'way', 'node']), {'@if-unused': true})
+ }
+ };
+ };
+
+ connection.changesetTags = function(comment, imageryUsed) {
+ var detected = iD.detect(),
+ tags = {
+ created_by: 'iD ' + iD.version,
+ imagery_used: imageryUsed.join(';').substr(0, 255),
+ host: (window.location.origin + window.location.pathname).substr(0, 255),
+ locale: detected.locale
+ };
+
+ if (comment) {
+ tags.comment = comment.substr(0, 255);
+ }
+
+ return tags;
+ };
+
+ connection.putChangeset = function(changes, comment, imageryUsed, callback) {
+ oauth.xhr({
+ method: 'PUT',
+ path: '/api/0.6/changeset/create',
+ options: { header: { 'Content-Type': 'text/xml' } },
+ content: JXON.stringify(connection.changesetJXON(connection.changesetTags(comment, imageryUsed)))
+ }, function(err, changeset_id) {
+ if (err) return callback(err);
+ oauth.xhr({
+ method: 'POST',
+ path: '/api/0.6/changeset/' + changeset_id + '/upload',
+ options: { header: { 'Content-Type': 'text/xml' } },
+ content: JXON.stringify(connection.osmChangeJXON(changeset_id, changes))
+ }, function(err) {
+ if (err) return callback(err);
+ // POST was successful, safe to call the callback.
+ // Still attempt to close changeset, but ignore response because #2667
+ // Add delay to allow for postgres replication #1646 #2678
+ window.setTimeout(function() { callback(null, changeset_id); }, 2500);
+ oauth.xhr({
+ method: 'PUT',
+ path: '/api/0.6/changeset/' + changeset_id + '/close',
+ options: { header: { 'Content-Type': 'text/xml' } }
+ }, d3.functor(true));
+ });
+ });
+ };
+
+ connection.userDetails = function(callback) {
+ if (userDetails) {
+ callback(undefined, userDetails);
+ return;
+ }
+
+ function done(err, user_details) {
+ if (err) return callback(err);
+
+ var u = user_details.getElementsByTagName('user')[0],
+ img = u.getElementsByTagName('img'),
+ image_url = '';
+
+ if (img && img[0] && img[0].getAttribute('href')) {
+ image_url = img[0].getAttribute('href');
+ }
+
+ userDetails = {
+ display_name: u.attributes.display_name.value,
+ image_url: image_url,
+ id: u.attributes.id.value
+ };
+
+ callback(undefined, userDetails);
+ }
+
+ oauth.xhr({ method: 'GET', path: '/api/0.6/user/details' }, done);
+ };
+
+ connection.userChangesets = function(callback) {
+ connection.userDetails(function(err, user) {
+ if (err) return callback(err);
+
+ function done(changesets) {
+ callback(undefined, Array.prototype.map.call(changesets.getElementsByTagName('changeset'),
+ function (changeset) {
+ return { tags: getTags(changeset) };
+ }));
+ }
+
+ d3.xml(url + '/api/0.6/changesets?user=' + user.id).get()
+ .on('load', done)
+ .on('error', callback);
+ });
+ };
+
+ connection.status = function(callback) {
+ function done(capabilities) {
+ var apiStatus = capabilities.getElementsByTagName('status');
+ callback(undefined, apiStatus[0].getAttribute('api'));
+ }
+ d3.xml(url + '/api/capabilities').get()
+ .on('load', done)
+ .on('error', callback);
+ };
+
+ function abortRequest(i) { i.abort(); }
+
+ connection.tileZoom = function(_) {
+ if (!arguments.length) return tileZoom;
+ tileZoom = _;
+ return connection;
+ };
+
+ connection.loadTiles = function(projection, dimensions, callback) {
+
+ if (off) return;
+
+ var s = projection.scale() * 2 * Math.PI,
+ z = Math.max(Math.log(s) / Math.log(2) - 8, 0),
+ ts = 256 * Math.pow(2, z - tileZoom),
+ origin = [
+ s / 2 - projection.translate()[0],
+ s / 2 - projection.translate()[1]];
+
+ var tiles = d3.geo.tile()
+ .scaleExtent([tileZoom, tileZoom])
+ .scale(s)
+ .size(dimensions)
+ .translate(projection.translate())()
+ .map(function(tile) {
+ var x = tile[0] * ts - origin[0],
+ y = tile[1] * ts - origin[1];
+
+ return {
+ id: tile.toString(),
+ extent: Extent(
+ projection.invert([x, y + ts]),
+ projection.invert([x + ts, y]))
+ };
+ });
+
+ function bboxUrl(tile) {
+ return url + '/api/0.6/map?bbox=' + tile.extent.toParam();
+ }
+
+ _.filter(inflight, function(v, i) {
+ var wanted = _.find(tiles, function(tile) {
+ return i === tile.id;
+ });
+ if (!wanted) delete inflight[i];
+ return !wanted;
+ }).map(abortRequest);
+
+ tiles.forEach(function(tile) {
+ var id = tile.id;
+
+ if (loadedTiles[id] || inflight[id]) return;
+
+ if (_.isEmpty(inflight)) {
+ event.loading();
+ }
+
+ inflight[id] = connection.loadFromURL(bboxUrl(tile), function(err, parsed) {
+ loadedTiles[id] = true;
+ delete inflight[id];
+
+ if (callback) callback(err, _.extend({data: parsed}, tile));
+
+ if (_.isEmpty(inflight)) {
+ event.loaded();
+ }
+ });
+ });
+ };
+
+ connection.switch = function(options) {
+ url = options.url;
+ oauth.options(_.extend({
+ loading: authenticating,
+ done: authenticated
+ }, options));
+ event.auth();
+ connection.flush();
+ return connection;
+ };
+
+ connection.toggle = function(_) {
+ off = !_;
+ return connection;
+ };
+
+ connection.flush = function() {
+ userDetails = undefined;
+ _.forEach(inflight, abortRequest);
+ loadedTiles = {};
+ inflight = {};
+ return connection;
+ };
+
+ connection.loadedTiles = function(_) {
+ if (!arguments.length) return loadedTiles;
+ loadedTiles = _;
+ return connection;
+ };
+
+ connection.logout = function() {
+ userDetails = undefined;
+ oauth.logout();
+ event.auth();
+ return connection;
+ };
+
+ connection.authenticate = function(callback) {
+ userDetails = undefined;
+ function done(err, res) {
+ event.auth();
+ if (callback) callback(err, res);
+ }
+ return oauth.authenticate(done);
+ };
+
+ return d3.rebind(connection, event, 'on');
+ }
+
+ /*
+ iD.Difference represents the difference between two graphs.
+ It knows how to calculate the set of entities that were
+ created, modified, or deleted, and also contains the logic
+ for recursively extending a difference to the complete set
+ of entities that will require a redraw, taking into account
+ child and parent relationships.
+ */
+ function Difference(base, head) {
+ var changes = {}, length = 0;
+
+ function changed(h, b) {
+ return h !== b && !_.isEqual(_.omit(h, 'v'), _.omit(b, 'v'));
+ }
+
+ _.each(head.entities, function(h, id) {
+ var b = base.entities[id];
+ if (changed(h, b)) {
+ changes[id] = {base: b, head: h};
+ length++;
+ }
+ });
+
+ _.each(base.entities, function(b, id) {
+ var h = head.entities[id];
+ if (!changes[id] && changed(h, b)) {
+ changes[id] = {base: b, head: h};
+ length++;
+ }
+ });
+
+ function addParents(parents, result) {
+ for (var i = 0; i < parents.length; i++) {
+ var parent = parents[i];
+
+ if (parent.id in result)
+ continue;
+
+ result[parent.id] = parent;
+ addParents(head.parentRelations(parent), result);
+ }
+ }
+
+ var difference = {};
+
+ difference.length = function() {
+ return length;
+ };
+
+ difference.changes = function() {
+ return changes;
+ };
+
+ difference.extantIDs = function() {
+ var result = [];
+ _.each(changes, function(change, id) {
+ if (change.head) result.push(id);
+ });
+ return result;
+ };
+
+ difference.modified = function() {
+ var result = [];
+ _.each(changes, function(change) {
+ if (change.base && change.head) result.push(change.head);
+ });
+ return result;
+ };
+
+ difference.created = function() {
+ var result = [];
+ _.each(changes, function(change) {
+ if (!change.base && change.head) result.push(change.head);
+ });
+ return result;
+ };
+
+ difference.deleted = function() {
+ var result = [];
+ _.each(changes, function(change) {
+ if (change.base && !change.head) result.push(change.base);
+ });
+ return result;
+ };
+
+ difference.summary = function() {
+ var relevant = {};
+
+ function addEntity(entity, graph, changeType) {
+ relevant[entity.id] = {
+ entity: entity,
+ graph: graph,
+ changeType: changeType
+ };
+ }
+
+ function addParents(entity) {
+ var parents = head.parentWays(entity);
+ for (var j = parents.length - 1; j >= 0; j--) {
+ var parent = parents[j];
+ if (!(parent.id in relevant)) addEntity(parent, head, 'modified');
+ }
+ }
+
+ _.each(changes, function(change) {
+ if (change.head && change.head.geometry(head) !== 'vertex') {
+ addEntity(change.head, head, change.base ? 'modified' : 'created');
+
+ } else if (change.base && change.base.geometry(base) !== 'vertex') {
+ addEntity(change.base, base, 'deleted');
+
+ } else if (change.base && change.head) { // modified vertex
+ var moved = !_.isEqual(change.base.loc, change.head.loc),
+ retagged = !_.isEqual(change.base.tags, change.head.tags);
+
+ if (moved) {
+ addParents(change.head);
+ }
+
+ if (retagged || (moved && change.head.hasInterestingTags())) {
+ addEntity(change.head, head, 'modified');
+ }
+
+ } else if (change.head && change.head.hasInterestingTags()) { // created vertex
+ addEntity(change.head, head, 'created');
+
+ } else if (change.base && change.base.hasInterestingTags()) { // deleted vertex
+ addEntity(change.base, base, 'deleted');
+ }
+ });
+
+ return d3.values(relevant);
+ };
+
+ difference.complete = function(extent) {
+ var result = {}, id, change;
+
+ for (id in changes) {
+ change = changes[id];
+
+ var h = change.head,
+ b = change.base,
+ entity = h || b;
+
+ if (extent &&
+ (!h || !h.intersects(extent, head)) &&
+ (!b || !b.intersects(extent, base)))
+ continue;
+
+ result[id] = h;
+
+ if (entity.type === 'way') {
+ var nh = h ? h.nodes : [],
+ nb = b ? b.nodes : [],
+ diff, i;
+
+ diff = _.difference(nh, nb);
+ for (i = 0; i < diff.length; i++) {
+ result[diff[i]] = head.hasEntity(diff[i]);
+ }
+
+ diff = _.difference(nb, nh);
+ for (i = 0; i < diff.length; i++) {
+ result[diff[i]] = head.hasEntity(diff[i]);
+ }
+ }
+
+ addParents(head.parentWays(entity), result);
+ addParents(head.parentRelations(entity), result);
+ }
+
+ return result;
+ };
+
+ return difference;
+ }
+
+ /* eslint-disable no-proto */
+ var getPrototypeOf = Object.getPrototypeOf || function(obj) { return obj.__proto__; };
+ // wraps an index to an interval [0..length-1]
+ function Wrap(index, length) {
+ if (index < 0)
+ index += Math.ceil(-index/length)*length;
+ return index % length;
+ }
+
+ // A per-domain session mutex backed by a cookie and dead man's
+ // switch. If the session crashes, the mutex will auto-release
+ // after 5 seconds.
+
+ function SessionMutex(name) {
+ var mutex = {},
+ intervalID;
+
+ function renew() {
+ var expires = new Date();
+ expires.setSeconds(expires.getSeconds() + 5);
+ document.cookie = name + '=1; expires=' + expires.toUTCString();
+ }
+
+ mutex.lock = function() {
+ if (intervalID) return true;
+ var cookie = document.cookie.replace(new RegExp('(?:(?:^|.*;)\\s*' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1');
+ if (cookie) return false;
+ renew();
+ intervalID = window.setInterval(renew, 4000);
+ return true;
+ };
+
+ mutex.unlock = function() {
+ if (!intervalID) return;
+ document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
+ clearInterval(intervalID);
+ intervalID = null;
+ };
+
+ mutex.locked = function() {
+ return !!intervalID;
+ };
+
+ return mutex;
+ }
+
+ function Graph(other, mutable) {
+ if (!(this instanceof Graph)) return new Graph(other, mutable);
+
+ if (other instanceof Graph) {
+ var base = other.base();
+ this.entities = _.assign(Object.create(base.entities), other.entities);
+ this._parentWays = _.assign(Object.create(base.parentWays), other._parentWays);
+ this._parentRels = _.assign(Object.create(base.parentRels), other._parentRels);
+
+ } else {
+ this.entities = Object.create({});
+ this._parentWays = Object.create({});
+ this._parentRels = Object.create({});
+ this.rebase(other || [], [this]);
+ }
+
+ this.transients = {};
+ this._childNodes = {};
+ this.frozen = !mutable;
+ }
+
+ Graph.prototype = {
+ hasEntity: function(id) {
+ return this.entities[id];
+ },
+
+ entity: function(id) {
+ var entity = this.entities[id];
+ if (!entity) {
+ throw new Error('entity ' + id + ' not found');
+ }
+ return entity;
+ },
+
+ transient: function(entity, key, fn) {
+ var id = entity.id,
+ transients = this.transients[id] ||
+ (this.transients[id] = {});
+
+ if (transients[key] !== undefined) {
+ return transients[key];
+ }
+
+ transients[key] = fn.call(entity);
+
+ return transients[key];
+ },
+
+ parentWays: function(entity) {
+ var parents = this._parentWays[entity.id],
+ result = [];
+
+ if (parents) {
+ for (var i = 0; i < parents.length; i++) {
+ result.push(this.entity(parents[i]));
+ }
+ }
+ return result;
+ },
+
+ isPoi: function(entity) {
+ var parentWays = this._parentWays[entity.id];
+ return !parentWays || parentWays.length === 0;
+ },
+
+ isShared: function(entity) {
+ var parentWays = this._parentWays[entity.id];
+ return parentWays && parentWays.length > 1;
+ },
+
+ parentRelations: function(entity) {
+ var parents = this._parentRels[entity.id],
+ result = [];
+
+ if (parents) {
+ for (var i = 0; i < parents.length; i++) {
+ result.push(this.entity(parents[i]));
+ }
+ }
+ return result;
+ },
+
+ childNodes: function(entity) {
+ if (this._childNodes[entity.id]) return this._childNodes[entity.id];
+ if (!entity.nodes) return [];
+
+ var nodes = [];
+ for (var i = 0; i < entity.nodes.length; i++) {
+ nodes[i] = this.entity(entity.nodes[i]);
+ }
+
+ if (iD.debug) Object.freeze(nodes);
+
+ this._childNodes[entity.id] = nodes;
+ return this._childNodes[entity.id];
+ },
+
+ base: function() {
+ return {
+ 'entities': getPrototypeOf(this.entities),
+ 'parentWays': getPrototypeOf(this._parentWays),
+ 'parentRels': getPrototypeOf(this._parentRels)
+ };
+ },
+
+ // Unlike other graph methods, rebase mutates in place. This is because it
+ // is used only during the history operation that merges newly downloaded
+ // data into each state. To external consumers, it should appear as if the
+ // graph always contained the newly downloaded data.
+ rebase: function(entities, stack, force) {
+ var base = this.base(),
+ i, j, k, id;
+
+ for (i = 0; i < entities.length; i++) {
+ var entity = entities[i];
+
+ if (!entity.visible || (!force && base.entities[entity.id]))
+ continue;
+
+ // Merging data into the base graph
+ base.entities[entity.id] = entity;
+ this._updateCalculated(undefined, entity, base.parentWays, base.parentRels);
+
+ // Restore provisionally-deleted nodes that are discovered to have an extant parent
+ if (entity.type === 'way') {
+ for (j = 0; j < entity.nodes.length; j++) {
+ id = entity.nodes[j];
+ for (k = 1; k < stack.length; k++) {
+ var ents = stack[k].entities;
+ if (ents.hasOwnProperty(id) && ents[id] === undefined) {
+ delete ents[id];
+ }
+ }
+ }
+ }
+ }
+
+ for (i = 0; i < stack.length; i++) {
+ stack[i]._updateRebased();
+ }
+ },
+
+ _updateRebased: function() {
+ var base = this.base(),
+ i, k, child, id, keys;
+
+ keys = Object.keys(this._parentWays);
+ for (i = 0; i < keys.length; i++) {
+ child = keys[i];
+ if (base.parentWays[child]) {
+ for (k = 0; k < base.parentWays[child].length; k++) {
+ id = base.parentWays[child][k];
+ if (!this.entities.hasOwnProperty(id) && !_.includes(this._parentWays[child], id)) {
+ this._parentWays[child].push(id);
+ }
+ }
+ }
+ }
+
+ keys = Object.keys(this._parentRels);
+ for (i = 0; i < keys.length; i++) {
+ child = keys[i];
+ if (base.parentRels[child]) {
+ for (k = 0; k < base.parentRels[child].length; k++) {
+ id = base.parentRels[child][k];
+ if (!this.entities.hasOwnProperty(id) && !_.includes(this._parentRels[child], id)) {
+ this._parentRels[child].push(id);
+ }
+ }
+ }
+ }
+
+ this.transients = {};
+
+ // this._childNodes is not updated, under the assumption that
+ // ways are always downloaded with their child nodes.
+ },
+
+ // Updates calculated properties (parentWays, parentRels) for the specified change
+ _updateCalculated: function(oldentity, entity, parentWays, parentRels) {
+
+ parentWays = parentWays || this._parentWays;
+ parentRels = parentRels || this._parentRels;
+
+ var type = entity && entity.type || oldentity && oldentity.type,
+ removed, added, ways, rels, i;
+
+
+ if (type === 'way') {
+
+ // Update parentWays
+ if (oldentity && entity) {
+ removed = _.difference(oldentity.nodes, entity.nodes);
+ added = _.difference(entity.nodes, oldentity.nodes);
+ } else if (oldentity) {
+ removed = oldentity.nodes;
+ added = [];
+ } else if (entity) {
+ removed = [];
+ added = entity.nodes;
+ }
+ for (i = 0; i < removed.length; i++) {
+ parentWays[removed[i]] = _.without(parentWays[removed[i]], oldentity.id);
+ }
+ for (i = 0; i < added.length; i++) {
+ ways = _.without(parentWays[added[i]], entity.id);
+ ways.push(entity.id);
+ parentWays[added[i]] = ways;
+ }
+
+ } else if (type === 'relation') {
+
+ // Update parentRels
+ if (oldentity && entity) {
+ removed = _.difference(oldentity.members, entity.members);
+ added = _.difference(entity.members, oldentity);
+ } else if (oldentity) {
+ removed = oldentity.members;
+ added = [];
+ } else if (entity) {
+ removed = [];
+ added = entity.members;
+ }
+ for (i = 0; i < removed.length; i++) {
+ parentRels[removed[i].id] = _.without(parentRels[removed[i].id], oldentity.id);
+ }
+ for (i = 0; i < added.length; i++) {
+ rels = _.without(parentRels[added[i].id], entity.id);
+ rels.push(entity.id);
+ parentRels[added[i].id] = rels;
+ }
+ }
+ },
+
+ replace: function(entity) {
+ if (this.entities[entity.id] === entity)
+ return this;
+
+ return this.update(function() {
+ this._updateCalculated(this.entities[entity.id], entity);
+ this.entities[entity.id] = entity;
+ });
+ },
+
+ remove: function(entity) {
+ return this.update(function() {
+ this._updateCalculated(entity, undefined);
+ this.entities[entity.id] = undefined;
+ });
+ },
+
+ revert: function(id) {
+ var baseEntity = this.base().entities[id],
+ headEntity = this.entities[id];
+
+ if (headEntity === baseEntity)
+ return this;
+
+ return this.update(function() {
+ this._updateCalculated(headEntity, baseEntity);
+ delete this.entities[id];
+ });
+ },
+
+ update: function() {
+ var graph = this.frozen ? Graph(this, true) : this;
+
+ for (var i = 0; i < arguments.length; i++) {
+ arguments[i].call(graph, graph);
+ }
+
+ if (this.frozen) graph.frozen = true;
+
+ return graph;
+ },
+
+ // Obliterates any existing entities
+ load: function(entities) {
+ var base = this.base();
+ this.entities = Object.create(base.entities);
+
+ for (var i in entities) {
+ this.entities[i] = entities[i];
+ this._updateCalculated(base.entities[i], this.entities[i]);
+ }
+
+ return this;
+ }
+ };
+
+ function Tree(head) {
+ var rtree = rbush(),
+ rectangles = {};
+
+ function entityRectangle(entity) {
+ var rect = entity.extent(head).rectangle();
+ rect.id = entity.id;
+ rectangles[entity.id] = rect;
+ return rect;
+ }
+
+ function updateParents(entity, insertions, memo) {
+ head.parentWays(entity).forEach(function(way) {
+ if (rectangles[way.id]) {
+ rtree.remove(rectangles[way.id]);
+ insertions[way.id] = way;
+ }
+ updateParents(way, insertions, memo);
+ });
+
+ head.parentRelations(entity).forEach(function(relation) {
+ if (memo[entity.id]) return;
+ memo[entity.id] = true;
+ if (rectangles[relation.id]) {
+ rtree.remove(rectangles[relation.id]);
+ insertions[relation.id] = relation;
+ }
+ updateParents(relation, insertions, memo);
+ });
+ }
+
+ var tree = {};
+
+ tree.rebase = function(entities, force) {
+ var insertions = {};
+
+ for (var i = 0; i < entities.length; i++) {
+ var entity = entities[i];
+
+ if (!entity.visible)
+ continue;
+
+ if (head.entities.hasOwnProperty(entity.id) || rectangles[entity.id]) {
+ if (!force) {
+ continue;
+ } else if (rectangles[entity.id]) {
+ rtree.remove(rectangles[entity.id]);
+ }
+ }
+
+ insertions[entity.id] = entity;
+ updateParents(entity, insertions, {});
+ }
+
+ rtree.load(_.map(insertions, entityRectangle));
+
+ return tree;
+ };
+
+ tree.intersects = function(extent, graph) {
+ if (graph !== head) {
+ var diff = Difference(head, graph),
+ insertions = {};
+
+ head = graph;
+
+ diff.deleted().forEach(function(entity) {
+ rtree.remove(rectangles[entity.id]);
+ delete rectangles[entity.id];
+ });
+
+ diff.modified().forEach(function(entity) {
+ rtree.remove(rectangles[entity.id]);
+ insertions[entity.id] = entity;
+ updateParents(entity, insertions, {});
+ });
+
+ diff.created().forEach(function(entity) {
+ insertions[entity.id] = entity;
+ });
+
+ rtree.load(_.map(insertions, entityRectangle));
+ }
+
+ return rtree.search(extent.rectangle()).map(function(rect) {
+ return head.entity(rect.id);
+ });
+ };
+
+ return tree;
+ }
+
+ function modalModule(selection, blocking) {
+ var keybinding = d3.keybinding('modal');
+ var previous = selection.select('div.modal');
+ var animate = previous.empty();
+
+ previous.transition()
+ .duration(200)
+ .style('opacity', 0)
+ .remove();
+
+ var shaded = selection
+ .append('div')
+ .attr('class', 'shaded')
+ .style('opacity', 0);
+
+ shaded.close = function() {
+ shaded
+ .transition()
+ .duration(200)
+ .style('opacity',0)
+ .remove();
+ modal
+ .transition()
+ .duration(200)
+ .style('top','0px');
+
+ keybinding.off();
+ };
+
+
+ var modal = shaded.append('div')
+ .attr('class', 'modal fillL col6');
+
+ if (!blocking) {
+ shaded.on('click.remove-modal', function() {
+ if (d3.event.target === this) {
+ shaded.close();
+ }
+ });
+
+ modal.append('button')
+ .attr('class', 'close')
+ .on('click', shaded.close)
+ .call(iD.svg.Icon('#icon-close'));
+
+ keybinding
+ .on('⌫', shaded.close)
+ .on('⎋', shaded.close);
+
+ d3.select(document).call(keybinding);
+ }
+
+ modal.append('div')
+ .attr('class', 'content');
+
+ if (animate) {
+ shaded.transition().style('opacity', 1);
+ } else {
+ shaded.style('opacity', 1);
+ }
+
+ return shaded;
+ }
+
+ function Loading(context) {
+ var message = '',
+ blocking = false,
+ modal;
+
+ var loading = function(selection) {
+ modal = modalModule(selection, blocking);
+
+ var loadertext = modal.select('.content')
+ .classed('loading-modal', true)
+ .append('div')
+ .attr('class', 'modal-section fillL');
+
+ loadertext.append('img')
+ .attr('class', 'loader')
+ .attr('src', context.imagePath('loader-white.gif'));
+
+ loadertext.append('h3')
+ .text(message);
+
+ modal.select('button.close')
+ .attr('class', 'hide');
+
+ return loading;
+ };
+
+ loading.message = function(_) {
+ if (!arguments.length) return message;
+ message = _;
+ return loading;
+ };
+
+ loading.blocking = function(_) {
+ if (!arguments.length) return blocking;
+ blocking = _;
+ return loading;
+ };
+
+ loading.close = function() {
+ modal.remove();
+ };
+
+ return loading;
+ }
+
+ function History(context) {
+ var stack, index, tree,
+ imageryUsed = ['Bing'],
+ dispatch = d3.dispatch('change', 'undone', 'redone'),
+ lock = SessionMutex('lock');
+
+ function perform(actions) {
+ actions = Array.prototype.slice.call(actions);
+
+ var annotation;
+
+ if (!_.isFunction(_.last(actions))) {
+ annotation = actions.pop();
+ }
+
+ var graph = stack[index].graph;
+ for (var i = 0; i < actions.length; i++) {
+ graph = actions[i](graph);
+ }
+
+ return {
+ graph: graph,
+ annotation: annotation,
+ imageryUsed: imageryUsed
+ };
+ }
+
+ function change(previous) {
+ var difference = Difference(previous, history.graph());
+ dispatch.change(difference);
+ return difference;
+ }
+
+ // iD uses namespaced keys so multiple installations do not conflict
+ function getKey(n) {
+ return 'iD_' + window.location.origin + '_' + n;
+ }
+
+ var history = {
+ graph: function() {
+ return stack[index].graph;
+ },
+
+ base: function() {
+ return stack[0].graph;
+ },
+
+ merge: function(entities, extent) {
+ stack[0].graph.rebase(entities, _.map(stack, 'graph'), false);
+ tree.rebase(entities, false);
+
+ dispatch.change(undefined, extent);
+ },
+
+ perform: function() {
+ var previous = stack[index].graph;
+
+ stack = stack.slice(0, index + 1);
+ stack.push(perform(arguments));
+ index++;
+
+ return change(previous);
+ },
+
+ replace: function() {
+ var previous = stack[index].graph;
+
+ // assert(index == stack.length - 1)
+ stack[index] = perform(arguments);
+
+ return change(previous);
+ },
+
+ pop: function() {
+ var previous = stack[index].graph;
+
+ if (index > 0) {
+ index--;
+ stack.pop();
+ return change(previous);
+ }
+ },
+
+ // Same as calling pop and then perform
+ overwrite: function() {
+ var previous = stack[index].graph;
+
+ if (index > 0) {
+ index--;
+ stack.pop();
+ }
+ stack = stack.slice(0, index + 1);
+ stack.push(perform(arguments));
+ index++;
+
+ return change(previous);
+ },
+
+ undo: function() {
+ var previous = stack[index].graph;
+
+ // Pop to the next annotated state.
+ while (index > 0) {
+ index--;
+ if (stack[index].annotation) break;
+ }
+
+ dispatch.undone();
+ return change(previous);
+ },
+
+ redo: function() {
+ var previous = stack[index].graph;
+
+ while (index < stack.length - 1) {
+ index++;
+ if (stack[index].annotation) break;
+ }
+
+ dispatch.redone();
+ return change(previous);
+ },
+
+ undoAnnotation: function() {
+ var i = index;
+ while (i >= 0) {
+ if (stack[i].annotation) return stack[i].annotation;
+ i--;
+ }
+ },
+
+ redoAnnotation: function() {
+ var i = index + 1;
+ while (i <= stack.length - 1) {
+ if (stack[i].annotation) return stack[i].annotation;
+ i++;
+ }
+ },
+
+ intersects: function(extent) {
+ return tree.intersects(extent, stack[index].graph);
+ },
+
+ difference: function() {
+ var base = stack[0].graph,
+ head = stack[index].graph;
+ return Difference(base, head);
+ },
+
+ changes: function(action) {
+ var base = stack[0].graph,
+ head = stack[index].graph;
+
+ if (action) {
+ head = action(head);
+ }
+
+ var difference = Difference(base, head);
+
+ return {
+ modified: difference.modified(),
+ created: difference.created(),
+ deleted: difference.deleted()
+ };
+ },
+
+ validate: function(changes) {
+ return _(iD.validations)
+ .map(function(fn) { return fn()(changes, stack[index].graph); })
+ .flatten()
+ .value();
+ },
+
+ hasChanges: function() {
+ return this.difference().length() > 0;
+ },
+
+ imageryUsed: function(sources) {
+ if (sources) {
+ imageryUsed = sources;
+ return history;
+ } else {
+ return _(stack.slice(1, index + 1))
+ .map('imageryUsed')
+ .flatten()
+ .uniq()
+ .without(undefined, 'Custom')
+ .value();
+ }
+ },
+
+ reset: function() {
+ stack = [{graph: Graph()}];
+ index = 0;
+ tree = Tree(stack[0].graph);
+ dispatch.change();
+ return history;
+ },
+
+ toJSON: function() {
+ if (!this.hasChanges()) return;
+
+ var allEntities = {},
+ baseEntities = {},
+ base = stack[0];
+
+ var s = stack.map(function(i) {
+ var modified = [], deleted = [];
+
+ _.forEach(i.graph.entities, function(entity, id) {
+ if (entity) {
+ var key = Entity.key(entity);
+ allEntities[key] = entity;
+ modified.push(key);
+ } else {
+ deleted.push(id);
+ }
+
+ // make sure that the originals of changed or deleted entities get merged
+ // into the base of the stack after restoring the data from JSON.
+ if (id in base.graph.entities) {
+ baseEntities[id] = base.graph.entities[id];
+ }
+ // get originals of parent entities too
+ _.forEach(base.graph._parentWays[id], function(parentId) {
+ if (parentId in base.graph.entities) {
+ baseEntities[parentId] = base.graph.entities[parentId];
+ }
+ });
+ });
+
+ var x = {};
+
+ if (modified.length) x.modified = modified;
+ if (deleted.length) x.deleted = deleted;
+ if (i.imageryUsed) x.imageryUsed = i.imageryUsed;
+ if (i.annotation) x.annotation = i.annotation;
+
+ return x;
+ });
+
+ return JSON.stringify({
+ version: 3,
+ entities: _.values(allEntities),
+ baseEntities: _.values(baseEntities),
+ stack: s,
+ nextIDs: Entity.id.next,
+ index: index
+ });
+ },
+
+ fromJSON: function(json, loadChildNodes) {
+ var h = JSON.parse(json),
+ loadComplete = true;
+
+ Entity.id.next = h.nextIDs;
+ index = h.index;
+
+ if (h.version === 2 || h.version === 3) {
+ var allEntities = {};
+
+ h.entities.forEach(function(entity) {
+ allEntities[Entity.key(entity)] = Entity(entity);
+ });
+
+ if (h.version === 3) {
+ // This merges originals for changed entities into the base of
+ // the stack even if the current stack doesn't have them (for
+ // example when iD has been restarted in a different region)
+ var baseEntities = h.baseEntities.map(function(d) { return Entity(d); });
+ stack[0].graph.rebase(baseEntities, _.map(stack, 'graph'), true);
+ tree.rebase(baseEntities, true);
+
+ // When we restore a modified way, we also need to fetch any missing
+ // childnodes that would normally have been downloaded with it.. #2142
+ if (loadChildNodes) {
+ var missing = _(baseEntities)
+ .filter({ type: 'way' })
+ .map('nodes')
+ .flatten()
+ .uniq()
+ .reject(function(n) { return stack[0].graph.hasEntity(n); })
+ .value();
+
+ if (!_.isEmpty(missing)) {
+ loadComplete = false;
+ context.redrawEnable(false);
+
+ var loading = Loading(context).blocking(true);
+ context.container().call(loading);
+
+ var childNodesLoaded = function(err, result) {
+ if (!err) {
+ var visible = _.groupBy(result.data, 'visible');
+ if (!_.isEmpty(visible.true)) {
+ missing = _.difference(missing, _.map(visible.true, 'id'));
+ stack[0].graph.rebase(visible.true, _.map(stack, 'graph'), true);
+ tree.rebase(visible.true, true);
+ }
+
+ // fetch older versions of nodes that were deleted..
+ _.each(visible.false, function(entity) {
+ context.connection()
+ .loadEntityVersion(entity.id, +entity.version - 1, childNodesLoaded);
+ });
+ }
+
+ if (err || _.isEmpty(missing)) {
+ loading.close();
+ context.redrawEnable(true);
+ dispatch.change();
+ }
+ };
+
+ context.connection().loadMultiple(missing, childNodesLoaded);
+ }
+ }
+ }
+
+ stack = h.stack.map(function(d) {
+ var entities = {}, entity;
+
+ if (d.modified) {
+ d.modified.forEach(function(key) {
+ entity = allEntities[key];
+ entities[entity.id] = entity;
+ });
+ }
+
+ if (d.deleted) {
+ d.deleted.forEach(function(id) {
+ entities[id] = undefined;
+ });
+ }
+
+ return {
+ graph: Graph(stack[0].graph).load(entities),
+ annotation: d.annotation,
+ imageryUsed: d.imageryUsed
+ };
+ });
+
+ } else { // original version
+ stack = h.stack.map(function(d) {
+ var entities = {};
+
+ for (var i in d.entities) {
+ var entity = d.entities[i];
+ entities[i] = entity === 'undefined' ? undefined : Entity(entity);
+ }
+
+ d.graph = Graph(stack[0].graph).load(entities);
+ return d;
+ });
+ }
+
+ if (loadComplete) {
+ dispatch.change();
+ }
+
+ return history;
+ },
+
+ save: function() {
+ if (lock.locked()) context.storage(getKey('saved_history'), history.toJSON() || null);
+ return history;
+ },
+
+ clearSaved: function() {
+ context.debouncedSave.cancel();
+ if (lock.locked()) context.storage(getKey('saved_history'), null);
+ return history;
+ },
+
+ lock: function() {
+ return lock.lock();
+ },
+
+ unlock: function() {
+ lock.unlock();
+ },
+
+ // is iD not open in another window and it detects that
+ // there's a history stored in localStorage that's recoverable?
+ restorableChanges: function() {
+ return lock.locked() && !!context.storage(getKey('saved_history'));
+ },
+
+ // load history from a version stored in localStorage
+ restore: function() {
+ if (!lock.locked()) return;
+
+ var json = context.storage(getKey('saved_history'));
+ if (json) history.fromJSON(json, true);
+ },
+
+ _getKey: getKey
+
+ };
+
+ history.reset();
+
+ return d3.rebind(history, dispatch, 'on');
+ }
+
+ function Turn(turn) {
+ if (!(this instanceof Turn))
+ return new Turn(turn);
+ _.extend(this, turn);
+ }
+
+ function Intersection(graph, vertexId) {
+ var vertex = graph.entity(vertexId),
+ parentWays = graph.parentWays(vertex),
+ coincident = [],
+ highways = {};
+
+ function addHighway(way, adjacentNodeId) {
+ if (highways[adjacentNodeId]) {
+ coincident.push(adjacentNodeId);
+ } else {
+ highways[adjacentNodeId] = way;
+ }
+ }
+
+ // Pre-split ways that would need to be split in
+ // order to add a restriction. The real split will
+ // happen when the restriction is added.
+ parentWays.forEach(function(way) {
+ if (!way.tags.highway || way.isArea() || way.isDegenerate())
+ return;
+
+ var isFirst = (vertexId === way.first()),
+ isLast = (vertexId === way.last()),
+ isAffix = (isFirst || isLast),
+ isClosingNode = (isFirst && isLast);
+
+ if (isAffix && !isClosingNode) {
+ var index = (isFirst ? 1 : way.nodes.length - 2);
+ addHighway(way, way.nodes[index]);
+
+ } else {
+ var splitIndex, wayA, wayB, indexA, indexB;
+ if (isClosingNode) {
+ splitIndex = Math.ceil(way.nodes.length / 2); // split at midpoint
+ wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)});
+ wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
+ indexA = 1;
+ indexB = way.nodes.length - 2;
+ } else {
+ splitIndex = _.indexOf(way.nodes, vertex.id, 1); // split at vertexid
+ wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)});
+ wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
+ indexA = splitIndex - 1;
+ indexB = splitIndex + 1;
+ }
+ graph = graph.replace(wayA).replace(wayB);
+ addHighway(wayA, way.nodes[indexA]);
+ addHighway(wayB, way.nodes[indexB]);
+ }
+ });
+
+ // remove any ways from this intersection that are coincident
+ // (i.e. any adjacent node used by more than one intersecting way)
+ coincident.forEach(function (n) {
+ delete highways[n];
+ });
+
+
+ var intersection = {
+ highways: highways,
+ ways: _.values(highways),
+ graph: graph
+ };
+
+ intersection.adjacentNodeId = function(fromWayId) {
+ return _.find(_.keys(highways), function(k) {
+ return highways[k].id === fromWayId;
+ });
+ };
+
+ intersection.turns = function(fromNodeId) {
+ var start = highways[fromNodeId];
+ if (!start)
+ return [];
+
+ if (start.first() === vertex.id && start.tags.oneway === 'yes')
+ return [];
+ if (start.last() === vertex.id && start.tags.oneway === '-1')
+ return [];
+
+ function withRestriction(turn) {
+ graph.parentRelations(graph.entity(turn.from.way)).forEach(function(relation) {
+ if (relation.tags.type !== 'restriction')
+ return;
+
+ var f = relation.memberByRole('from'),
+ t = relation.memberByRole('to'),
+ v = relation.memberByRole('via');
+
+ if (f && f.id === turn.from.way &&
+ v && v.id === turn.via.node &&
+ t && t.id === turn.to.way) {
+ turn.restriction = relation.id;
+ } else if (/^only_/.test(relation.tags.restriction) &&
+ f && f.id === turn.from.way &&
+ v && v.id === turn.via.node &&
+ t && t.id !== turn.to.way) {
+ turn.restriction = relation.id;
+ turn.indirect_restriction = true;
+ }
+ });
+
+ return Turn(turn);
+ }
+
+ var from = {
+ node: fromNodeId,
+ way: start.id.split(/-(a|b)/)[0]
+ },
+ via = { node: vertex.id },
+ turns = [];
+
+ _.each(highways, function(end, adjacentNodeId) {
+ if (end === start)
+ return;
+
+ // backward
+ if (end.first() !== vertex.id && end.tags.oneway !== 'yes') {
+ turns.push(withRestriction({
+ from: from,
+ via: via,
+ to: {
+ node: adjacentNodeId,
+ way: end.id.split(/-(a|b)/)[0]
+ }
+ }));
+ }
+
+ // forward
+ if (end.last() !== vertex.id && end.tags.oneway !== '-1') {
+ turns.push(withRestriction({
+ from: from,
+ via: via,
+ to: {
+ node: adjacentNodeId,
+ way: end.id.split(/-(a|b)/)[0]
+ }
+ }));
+ }
+
+ });
+
+ // U-turn
+ if (start.tags.oneway !== 'yes' && start.tags.oneway !== '-1') {
+ turns.push(withRestriction({
+ from: from,
+ via: via,
+ to: from,
+ u: true
+ }));
+ }
+
+ return turns;
+ };
+
+ return intersection;
+ }
+
+
+ function inferRestriction(graph, from, via, to, projection) {
+ var fromWay = graph.entity(from.way),
+ fromNode = graph.entity(from.node),
+ toWay = graph.entity(to.way),
+ toNode = graph.entity(to.node),
+ viaNode = graph.entity(via.node),
+ fromOneWay = (fromWay.tags.oneway === 'yes' && fromWay.last() === via.node) ||
+ (fromWay.tags.oneway === '-1' && fromWay.first() === via.node),
+ toOneWay = (toWay.tags.oneway === 'yes' && toWay.first() === via.node) ||
+ (toWay.tags.oneway === '-1' && toWay.last() === via.node),
+ angle = getAngle(viaNode, fromNode, projection) -
+ getAngle(viaNode, toNode, projection);
+
+ angle = angle * 180 / Math.PI;
+
+ while (angle < 0)
+ angle += 360;
+
+ if (fromNode === toNode)
+ return 'no_u_turn';
+ if ((angle < 23 || angle > 336) && fromOneWay && toOneWay)
+ return 'no_u_turn';
+ if (angle < 158)
+ return 'no_right_turn';
+ if (angle > 202)
+ return 'no_left_turn';
+
+ return 'no_straight_on';
+ }
+
+ // For fixing up rendering of multipolygons with tags on the outer member.
+ // https://github.com/openstreetmap/iD/issues/613
+ function isSimpleMultipolygonOuterMember(entity, graph) {
+ if (entity.type !== 'way')
+ return false;
+
+ var parents = graph.parentRelations(entity);
+ if (parents.length !== 1)
+ return false;
+
+ var parent = parents[0];
+ if (!parent.isMultipolygon() || Object.keys(parent.tags).length > 1)
+ return false;
+
+ var members = parent.members, member;
+ for (var i = 0; i < members.length; i++) {
+ member = members[i];
+ if (member.id === entity.id && member.role && member.role !== 'outer')
+ return false; // Not outer member
+ if (member.id !== entity.id && (!member.role || member.role === 'outer'))
+ return false; // Not a simple multipolygon
+ }
+
+ return parent;
+ }
+
+ function simpleMultipolygonOuterMember(entity, graph) {
+ if (entity.type !== 'way')
+ return false;
+
+ var parents = graph.parentRelations(entity);
+ if (parents.length !== 1)
+ return false;
+
+ var parent = parents[0];
+ if (!parent.isMultipolygon() || Object.keys(parent.tags).length > 1)
+ return false;
+
+ var members = parent.members, member, outerMember;
+ for (var i = 0; i < members.length; i++) {
+ member = members[i];
+ if (!member.role || member.role === 'outer') {
+ if (outerMember)
+ return false; // Not a simple multipolygon
+ outerMember = member;
+ }
+ }
+
+ return outerMember && graph.hasEntity(outerMember.id);
+ }
+
+ // Join `array` into sequences of connecting ways.
+ //
+ // Segments which share identical start/end nodes will, as much as possible,
+ // be connected with each other.
+ //
+ // The return value is a nested array. Each constituent array contains elements
+ // of `array` which have been determined to connect. Each consitituent array
+ // also has a `nodes` property whose value is an ordered array of member nodes,
+ // with appropriate order reversal and start/end coordinate de-duplication.
+ //
+ // Members of `array` must have, at minimum, `type` and `id` properties.
+ // Thus either an array of `iD.Way`s or a relation member array may be
+ // used.
+ //
+ // If an member has a `tags` property, its tags will be reversed via
+ // `iD.actions.Reverse` in the output.
+ //
+ // Incomplete members (those for which `graph.hasEntity(element.id)` returns
+ // false) and non-way members are ignored.
+ //
+ function joinWays(array, graph) {
+ var joined = [], member, current, nodes, first, last, i, how, what;
+
+ array = array.filter(function(member) {
+ return member.type === 'way' && graph.hasEntity(member.id);
+ });
+
+ function resolve(member) {
+ return graph.childNodes(graph.entity(member.id));
+ }
+
+ function reverse(member) {
+ return member.tags ? iD.actions.Reverse(member.id, {reverseOneway: true})(graph).entity(member.id) : member;
+ }
+
+ while (array.length) {
+ member = array.shift();
+ current = [member];
+ current.nodes = nodes = resolve(member).slice();
+ joined.push(current);
+
+ while (array.length && _.first(nodes) !== _.last(nodes)) {
+ first = _.first(nodes);
+ last = _.last(nodes);
+
+ for (i = 0; i < array.length; i++) {
+ member = array[i];
+ what = resolve(member);
+
+ if (last === _.first(what)) {
+ how = nodes.push;
+ what = what.slice(1);
+ break;
+ } else if (last === _.last(what)) {
+ how = nodes.push;
+ what = what.slice(0, -1).reverse();
+ member = reverse(member);
+ break;
+ } else if (first === _.last(what)) {
+ how = nodes.unshift;
+ what = what.slice(0, -1);
+ break;
+ } else if (first === _.first(what)) {
+ how = nodes.unshift;
+ what = what.slice(1).reverse();
+ member = reverse(member);
+ break;
+ } else {
+ what = how = null;
+ }
+ }
+
+ if (!what)
+ break; // No more joinable ways.
+
+ how.apply(current, [member]);
+ how.apply(nodes, what);
+
+ array.splice(i, 1);
+ }
+ }
+
+ return joined;
+ }
+
+ /*
+ Bypasses features of D3's default projection stream pipeline that are unnecessary:
+ * Antimeridian clipping
+ * Spherical rotation
+ * Resampling
+ */
+ function RawMercator() {
+ var project = d3.geo.mercator.raw,
+ k = 512 / Math.PI, // scale
+ x = 0, y = 0, // translate
+ clipExtent = [[0, 0], [0, 0]];
+
+ function projection(point) {
+ point = project(point[0] * Math.PI / 180, point[1] * Math.PI / 180);
+ return [point[0] * k + x, y - point[1] * k];
+ }
+
+ projection.invert = function(point) {
+ point = project.invert((point[0] - x) / k, (y - point[1]) / k);
+ return point && [point[0] * 180 / Math.PI, point[1] * 180 / Math.PI];
+ };
+
+ projection.scale = function(_) {
+ if (!arguments.length) return k;
+ k = +_;
+ return projection;
+ };
+
+ projection.translate = function(_) {
+ if (!arguments.length) return [x, y];
+ x = +_[0];
+ y = +_[1];
+ return projection;
+ };
+
+ projection.clipExtent = function(_) {
+ if (!arguments.length) return clipExtent;
+ clipExtent = _;
+ return projection;
+ };
+
+ projection.stream = d3.geo.transform({
+ point: function(x, y) {
+ x = projection([x, y]);
+ this.stream.point(x[0], x[1]);
+ }
+ }).stream;
+
+ return projection;
+ }
+
+ function roundCoords(c) {
+ return [Math.floor(c[0]), Math.floor(c[1])];
+ }
+
+ function interp(p1, p2, t) {
+ return [p1[0] + (p2[0] - p1[0]) * t,
+ p1[1] + (p2[1] - p1[1]) * t];
+ }
+
+ // 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
+ // Returns a positive value, if OAB makes a counter-clockwise turn,
+ // negative for clockwise turn, and zero if the points are collinear.
+ function cross(o, a, b) {
+ return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
+ }
+
+ // http://jsperf.com/id-dist-optimization
+ function euclideanDistance(a, b) {
+ var x = a[0] - b[0], y = a[1] - b[1];
+ return Math.sqrt((x * x) + (y * y));
+ }
+
+ // using WGS84 polar radius (6356752.314245179 m)
+ // const = 2 * PI * r / 360
+ function latToMeters(dLat) {
+ return dLat * 110946.257617;
+ }
+
+ // using WGS84 equatorial radius (6378137.0 m)
+ // const = 2 * PI * r / 360
+ function lonToMeters(dLon, atLat) {
+ return Math.abs(atLat) >= 90 ? 0 :
+ dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180)));
+ }
+
+ // using WGS84 polar radius (6356752.314245179 m)
+ // const = 2 * PI * r / 360
+ function metersToLat(m) {
+ return m / 110946.257617;
+ }
+
+ // using WGS84 equatorial radius (6378137.0 m)
+ // const = 2 * PI * r / 360
+ function metersToLon(m, atLat) {
+ return Math.abs(atLat) >= 90 ? 0 :
+ m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180)));
+ }
+
+ function offsetToMeters(offset) {
+ var equatRadius = 6356752.314245179,
+ polarRadius = 6378137.0,
+ tileSize = 256;
+
+ return [
+ offset[0] * 2 * Math.PI * equatRadius / tileSize,
+ -offset[1] * 2 * Math.PI * polarRadius / tileSize
+ ];
+ }
+
+ function metersToOffset(meters) {
+ var equatRadius = 6356752.314245179,
+ polarRadius = 6378137.0,
+ tileSize = 256;
+
+ return [
+ meters[0] * tileSize / (2 * Math.PI * equatRadius),
+ -meters[1] * tileSize / (2 * Math.PI * polarRadius)
+ ];
+ }
+
+ // Equirectangular approximation of spherical distances on Earth
+ function sphericalDistance(a, b) {
+ var x = lonToMeters(a[0] - b[0], (a[1] + b[1]) / 2),
+ y = latToMeters(a[1] - b[1]);
+ return Math.sqrt((x * x) + (y * y));
+ }
+
+ function edgeEqual(a, b) {
+ return (a[0] === b[0] && a[1] === b[1]) ||
+ (a[0] === b[1] && a[1] === b[0]);
+ }
+
+ // Return the counterclockwise angle in the range (-pi, pi)
+ // between the positive X axis and the line intersecting a and b.
+ function getAngle(a, b, projection) {
+ a = projection(a.loc);
+ b = projection(b.loc);
+ return Math.atan2(b[1] - a[1], b[0] - a[0]);
+ }
+
+ // Choose the edge with the minimal distance from `point` to its orthogonal
+ // projection onto that edge, if such a projection exists, or the distance to
+ // the closest vertex on that edge. Returns an object with the `index` of the
+ // chosen edge, the chosen `loc` on that edge, and the `distance` to to it.
+ function chooseEdge(nodes, point, projection) {
+ var dist = euclideanDistance,
+ points = nodes.map(function(n) { return projection(n.loc); }),
+ min = Infinity,
+ idx, loc;
+
+ function dot(p, q) {
+ return p[0] * q[0] + p[1] * q[1];
+ }
+
+ for (var i = 0; i < points.length - 1; i++) {
+ var o = points[i],
+ s = [points[i + 1][0] - o[0],
+ points[i + 1][1] - o[1]],
+ v = [point[0] - o[0],
+ point[1] - o[1]],
+ proj = dot(v, s) / dot(s, s),
+ p;
+
+ if (proj < 0) {
+ p = o;
+ } else if (proj > 1) {
+ p = points[i + 1];
+ } else {
+ p = [o[0] + proj * s[0], o[1] + proj * s[1]];
+ }
+
+ var d = dist(p, point);
+ if (d < min) {
+ min = d;
+ idx = i + 1;
+ loc = projection.invert(p);
+ }
+ }
+
+ return {
+ index: idx,
+ distance: min,
+ loc: loc
+ };
+ }
+
+ // Return the intersection point of 2 line segments.
+ // From https://github.com/pgkelley4/line-segments-intersect
+ // This uses the vector cross product approach described below:
+ // http://stackoverflow.com/a/565282/786339
+ function lineIntersection(a, b) {
+ function subtractPoints(point1, point2) {
+ return [point1[0] - point2[0], point1[1] - point2[1]];
+ }
+ function crossProduct(point1, point2) {
+ return point1[0] * point2[1] - point1[1] * point2[0];
+ }
+
+ var p = [a[0][0], a[0][1]],
+ p2 = [a[1][0], a[1][1]],
+ q = [b[0][0], b[0][1]],
+ q2 = [b[1][0], b[1][1]],
+ r = subtractPoints(p2, p),
+ s = subtractPoints(q2, q),
+ uNumerator = crossProduct(subtractPoints(q, p), r),
+ denominator = crossProduct(r, s);
+
+ if (uNumerator && denominator) {
+ var u = uNumerator / denominator,
+ t = crossProduct(subtractPoints(q, p), s) / denominator;
+
+ if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) {
+ return interp(p, p2, t);
+ }
+ }
+
+ return null;
+ }
+
+ function pathIntersections(path1, path2) {
+ var intersections = [];
+ for (var i = 0; i < path1.length - 1; i++) {
+ for (var j = 0; j < path2.length - 1; j++) {
+ var a = [ path1[i], path1[i+1] ],
+ b = [ path2[j], path2[j+1] ],
+ hit = lineIntersection(a, b);
+ if (hit) intersections.push(hit);
+ }
+ }
+ return intersections;
+ }
+
+ // Return whether point is contained in polygon.
+ //
+ // `point` should be a 2-item array of coordinates.
+ // `polygon` should be an array of 2-item arrays of coordinates.
+ //
+ // From https://github.com/substack/point-in-polygon.
+ // ray-casting algorithm based on
+ // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
+ //
+ function pointInPolygon(point, polygon) {
+ var x = point[0],
+ y = point[1],
+ inside = false;
+
+ for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
+ var xi = polygon[i][0], yi = polygon[i][1];
+ var xj = polygon[j][0], yj = polygon[j][1];
+
+ var intersect = ((yi > y) !== (yj > y)) &&
+ (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
+ if (intersect) inside = !inside;
+ }
+
+ return inside;
+ }
+
+ function polygonContainsPolygon(outer, inner) {
+ return _.every(inner, function(point) {
+ return pointInPolygon(point, outer);
+ });
+ }
+
+ function polygonIntersectsPolygon(outer, inner, checkSegments) {
+ function testSegments(outer, inner) {
+ for (var i = 0; i < outer.length - 1; i++) {
+ for (var j = 0; j < inner.length - 1; j++) {
+ var a = [ outer[i], outer[i+1] ],
+ b = [ inner[j], inner[j+1] ];
+ if (lineIntersection(a, b)) return true;
+ }
+ }
+ return false;
+ }
+
+ function testPoints(outer, inner) {
+ return _.some(inner, function(point) {
+ return pointInPolygon(point, outer);
+ });
+ }
+
+ return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner));
+ }
+
+ function pathLength(path) {
+ var length = 0,
+ dx, dy;
+ for (var i = 0; i < path.length - 1; i++) {
+ dx = path[i][0] - path[i + 1][0];
+ dy = path[i][1] - path[i + 1][1];
+ length += Math.sqrt(dx * dx + dy * dy);
+ }
+ return length;
+ }
+
+
+ var geo = Object.freeze({
+ roundCoords: roundCoords,
+ interp: interp,
+ cross: cross,
+ euclideanDistance: euclideanDistance,
+ latToMeters: latToMeters,
+ lonToMeters: lonToMeters,
+ metersToLat: metersToLat,
+ metersToLon: metersToLon,
+ offsetToMeters: offsetToMeters,
+ metersToOffset: metersToOffset,
+ sphericalDistance: sphericalDistance,
+ edgeEqual: edgeEqual,
+ angle: getAngle,
+ chooseEdge: chooseEdge,
+ lineIntersection: lineIntersection,
+ pathIntersections: pathIntersections,
+ pointInPolygon: pointInPolygon,
+ polygonContainsPolygon: polygonContainsPolygon,
+ polygonIntersectsPolygon: polygonIntersectsPolygon,
+ pathLength: pathLength,
+ Extent: Extent,
+ Intersection: Intersection,
+ Turn: Turn,
+ inferRestriction: inferRestriction,
+ isSimpleMultipolygonOuterMember: isSimpleMultipolygonOuterMember,
+ simpleMultipolygonOuterMember: simpleMultipolygonOuterMember,
+ joinWays: joinWays,
+ RawMercator: RawMercator
+ });
+
+ function AddMember(relationId, member, memberIndex) {
+ return function(graph) {
+ var relation = graph.entity(relationId);
+
+ if (isNaN(memberIndex) && member.type === 'way') {
+ var members = relation.indexedMembers();
+ members.push(member);
+
+ var joined = joinWays(members, graph);
+ for (var i = 0; i < joined.length; i++) {
+ var segment = joined[i];
+ for (var j = 0; j < segment.length && segment.length >= 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));
+ };
+ }
+
+ function AddMidpoint(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 (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;
+ };
+ }
+
+ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as
+ function AddVertex(wayId, nodeId, index) {
+ return function(graph) {
+ return graph.replace(graph.entity(wayId).addNode(nodeId, index));
+ };
+ }
+
+ function ChangeMember(relationId, member, memberIndex) {
+ return function(graph) {
+ return graph.replace(graph.entity(relationId).updateMember(member, memberIndex));
+ };
+ }
+
+ function ChangePreset(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}));
+ };
+ }
+
+ function ChangeTags(entityId, tags) {
+ return function(graph) {
+ var entity = graph.entity(entityId);
+ return graph.replace(entity.update({tags: tags}));
+ };
+ }
+
+ function Circularize(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) ? interp(points[0], points[1], 0.5) : d3.geom.polygon(points).centroid(),
+ radius = d3.median(points, function(p) { return 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 = 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 = 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 = 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;
+ }
+
+ function DeleteMultiple(ids) {
+ var actions = {
+ way: DeleteWay,
+ node: DeleteNode,
+ relation: 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;
+ }
+
+ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteRelationAction.as
+ function DeleteRelation(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 = 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 = DeleteMultiple([memberId])(graph);
+ }
+ });
+
+ return graph.remove(relation);
+ };
+
+ action.disabled = function(graph) {
+ if (!graph.entity(relationId).isComplete(graph))
+ return 'incomplete_relation';
+ };
+
+ return action;
+ }
+
+ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteWayAction.as
+ function DeleteWay(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 = 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;
+ }
+
+ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteNodeAction.as
+ function DeleteNode(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 = DeleteWay(parent.id)(graph);
+ }
+ });
+
+ graph.parentRelations(node)
+ .forEach(function(parent) {
+ parent = parent.removeMembersWithID(nodeId);
+ graph = graph.replace(parent);
+
+ if (parent.isDegenerate()) {
+ graph = DeleteRelation(parent.id)(graph);
+ }
+ });
+
+ return graph.remove(node);
+ };
+
+ action.disabled = function() {
+ return false;
+ };
+
+ return action;
+ }
+
+ // 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
+ //
+ function Connect(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 = DeleteNode(node.id)(graph);
+ }
+
+ graph = graph.replace(survivor);
+
+ return graph;
+ };
+ }
+
+ function CopyEntities(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;
+ }
+
+ function DeleteMember(relationId, memberIndex) {
+ return function(graph) {
+ var relation = graph.entity(relationId)
+ .removeMember(memberIndex);
+
+ graph = graph.replace(relation);
+
+ if (relation.isDegenerate())
+ graph = DeleteRelation(relation.id)(graph);
+
+ return graph;
+ };
+ }
+
+ function DeprecateTags(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;
+ }
+ };
+ }
+
+ function DiscardTags(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;
+ };
+ }
+
+ function Disconnect(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 = 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;
+ }
+
+ function Join(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 = 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 = 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 = 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] && interestingTag(k) && tags[k] !== way.tags[k]) {
+ conflicting = true;
+ }
+ }
+ });
+
+ if (relation)
+ return 'restriction';
+
+ if (conflicting)
+ return 'conflicting_tags';
+ };
+
+ return action;
+ }
+
+ function Merge(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;
+ }
+
+ function MergePolygon(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(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 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] ||
+ 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;
+ }
+
+ function MergeRemoteChanges(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 = 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 = 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 = 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 = Entity(local, { version: remote.version });
+
+ // delete/undelete
+ if (!remote.visible) {
+ if (option === 'force_remote') {
+ return 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;
+ }
+
+ function Move(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 = 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(getAngle(orig, prev, projection) -
+ getAngle(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 (sphericalDistance(prev.loc, orig.loc) < 10 ||
+ 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 = pathLength(p1),
+ d2 = 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 && chooseEdge(nodes1, projection(vertex.loc), projection),
+ edge2 = !isEP2 && 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 = interp(edge1.loc, edge2.loc, 0.5);
+ edge1 = chooseEdge(nodes1, projection(loc), projection);
+ edge2 = 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 = pathIntersections(movedPath, unmovedPath);
+
+ for (var i = 0; i < hits.length; i++) {
+ if (_.isEqual(hits[i], end)) continue;
+ var edge = 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;
+ }
+
+ // 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
+ function MoveNode(nodeId, loc) {
+ return function(graph) {
+ return graph.replace(graph.entity(nodeId).move(loc));
+ };
+ }
+
+ function Noop() {
+ return function(graph) {
+ return graph;
+ };
+ }
+
+ /*
+ * Based on https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/potlatch2/tools/Quadrilateralise.as
+ */
+
+ function Orthogonalize(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 = 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(euclideanDistance(p, [0, 0]), 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;
+ }
+
+ // 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
+ //
+ function Split(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 Wrap(index, nodes.length);
+ }
+
+ function dist(nA, nB) {
+ return 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 = Way({id: newWayId, tags: wayA.tags}),
+ nodesA,
+ nodesB,
+ isArea = wayA.isArea(),
+ isOuter = 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 = AddMember(relation.id, member)(graph);
+ }
+ });
+
+ if (!isOuter && isArea) {
+ var multipolygon = 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;
+ }
+
+ // 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
+ // 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.
+ //
+ function RestrictTurn(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 || Way().id;
+ graph = 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(Relation({
+ id: restrictionId,
+ tags: {
+ type: 'restriction',
+ restriction: turn.restriction ||
+ 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'}
+ ]
+ }));
+ };
+ }
+
+ /*
+ 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
+ */
+ function Reverse(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}));
+ };
+ }
+
+ function Revert(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 = DeleteWay(parent.id)(graph);
+ }
+ });
+ }
+
+ graph.parentRelations(entity)
+ .forEach(function(parent) {
+ parent = parent.removeMembersWithID(id);
+ graph = graph.replace(parent);
+
+ if (parent.isDegenerate()) {
+ graph = DeleteRelation(parent.id)(graph);
+ }
+ });
+ }
+
+ return graph.revert(id);
+ };
+
+ return action;
+ }
+
+ function RotateWay(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)));
+
+ });
+
+ });
+ };
+ }
+
+ /*
+ * Based on https://github.com/openstreetmap/potlatch2/net/systemeD/potlatch2/tools/Straighten.as
+ */
+
+ function Straighten(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 = 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;
+ }
+
+ // 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.
+ //
+ function UnrestrictTurn(turn) {
+ return function(graph) {
+ return DeleteRelation(turn.restriction)(graph);
+ };
+ }
+
+
+
+ var actions = Object.freeze({
+ AddEntity: AddEntity,
+ AddMember: AddMember,
+ AddMidpoint: AddMidpoint,
+ AddVertex: AddVertex,
+ ChangeMember: ChangeMember,
+ ChangePreset: ChangePreset,
+ ChangeTags: ChangeTags,
+ Circularize: Circularize,
+ Connect: Connect,
+ CopyEntities: CopyEntities,
+ DeleteMember: DeleteMember,
+ DeleteMultiple: DeleteMultiple,
+ DeleteNode: DeleteNode,
+ DeleteRelation: DeleteRelation,
+ DeleteWay: DeleteWay,
+ DeprecateTags: DeprecateTags,
+ DiscardTags: DiscardTags,
+ Disconnect: Disconnect,
+ Join: Join,
+ Merge: Merge,
+ MergePolygon: MergePolygon,
+ MergeRemoteChanges: MergeRemoteChanges,
+ Move: Move,
+ MoveNode: MoveNode,
+ Noop: Noop,
+ Orthogonalize: Orthogonalize,
+ RestrictTurn: RestrictTurn,
+ Reverse: Reverse,
+ Revert: Revert,
+ RotateWay: RotateWay,
+ Split: Split,
+ Straighten: Straighten,
+ UnrestrictTurn: UnrestrictTurn
+ });
+
+ exports.actions = actions;
+ exports.geo = geo;
+ exports.Connection = Connection;
+ exports.Difference = Difference;
+ exports.Entity = Entity;
+ exports.Graph = Graph;
+ exports.History = History;
+ exports.Node = Node;
+ exports.Relation = Relation;
+ exports.oneWayTags = oneWayTags;
+ exports.pavedTags = pavedTags;
+ exports.interestingTag = interestingTag;
+ exports.Tree = Tree;
+ exports.Way = Way;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+}));
\ No newline at end of file
diff --git a/js/lib/id/ui/intro.js b/js/lib/id/ui/intro.js
index 465216d98..512396d5e 100644
--- a/js/lib/id/ui/intro.js
+++ b/js/lib/id/ui/intro.js
@@ -247,40 +247,6 @@
return d3.rebind(step, event, 'on');
}
-
- function pointBox(point, context) {
- var rect = context.surfaceRect();
- point = context.projection(point);
- return {
- left: point[0] + rect.left - 30,
- top: point[1] + rect.top - 50,
- width: 60,
- height: 70
- };
- }
-
- function pad(box, padding, context) {
- if (box instanceof Array) {
- var rect = context.surfaceRect();
- box = context.projection(box);
- box = {
- left: box[0] + rect.left,
- top: box[1] + rect.top
- };
- }
- return {
- left: box.left - padding,
- top: box.top - padding,
- width: (box.width || 0) + 2 * padding,
- height: (box.width || 0) + 2 * padding
- };
- }
-
- function icon(name, svgklass) {
- return '';
- }
-
function navigation(context, reveal) {
var event = d3.dispatch('done'),
timeouts = [];
@@ -607,9 +573,6 @@
exports.area = area;
exports.line = line;
- exports.pad = pad;
- exports.pointBox = pointBox;
- exports.icon = icon;
exports.navigation = navigation;
exports.point = point;
exports.startEditing = startEditing;
diff --git a/modules/actions/add_member.js b/modules/actions/add_member.js
index 6e7a1e688..b3e6bafb9 100644
--- a/modules/actions/add_member.js
+++ b/modules/actions/add_member.js
@@ -1,3 +1,4 @@
+import { joinWays } from '../geo/index';
export function AddMember(relationId, member, memberIndex) {
return function(graph) {
var relation = graph.entity(relationId);
@@ -6,7 +7,7 @@ export function AddMember(relationId, member, memberIndex) {
var members = relation.indexedMembers();
members.push(member);
- var joined = iD.geo.joinWays(members, graph);
+ var joined = joinWays(members, graph);
for (var i = 0; i < joined.length; i++) {
var segment = joined[i];
for (var j = 0; j < segment.length && segment.length >= 2; j++) {
diff --git a/modules/actions/add_midpoint.js b/modules/actions/add_midpoint.js
index 94a458a8f..bfa216d8a 100644
--- a/modules/actions/add_midpoint.js
+++ b/modules/actions/add_midpoint.js
@@ -1,3 +1,4 @@
+import { edgeEqual } from '../geo/index';
export function AddMidpoint(midpoint, node) {
return function(graph) {
graph = graph.replace(node.move(midpoint.loc));
@@ -8,7 +9,7 @@ export function AddMidpoint(midpoint, node) {
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)) {
+ if (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,
diff --git a/modules/actions/circularize.js b/modules/actions/circularize.js
index 076a5990b..54a3f7e47 100644
--- a/modules/actions/circularize.js
+++ b/modules/actions/circularize.js
@@ -1,4 +1,8 @@
-export function Circularize(wayId, projection, maxAngle) {
+import { Node } from '../core/index';
+import { interp, euclideanDistance } from '../geo/index';
+
+export function Circularize(wayId
+ , projection, maxAngle) {
maxAngle = (maxAngle || 20) * Math.PI / 180;
var action = function(graph) {
@@ -12,8 +16,8 @@ export function Circularize(wayId, projection, maxAngle) {
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); }),
+ centroid = (points.length === 2) ? interp(points[0], points[1], 0.5) : d3.geom.polygon(points).centroid(),
+ radius = d3.median(points, function(p) { return euclideanDistance(centroid, p); }),
sign = d3.geom.polygon(points).area() > 0 ? 1 : -1,
ids;
@@ -52,7 +56,7 @@ export function Circularize(wayId, projection, maxAngle) {
}
// position this key node
- distance = iD.geo.euclideanDistance(centroid, keyPoints[i]);
+ distance = euclideanDistance(centroid, keyPoints[i]);
if (distance === 0) { distance = 1e-4; }
keyPoints[i] = [
centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius,
@@ -92,7 +96,7 @@ export function Circularize(wayId, projection, maxAngle) {
centroid[0] + Math.cos(angle) * radius,
centroid[1] + Math.sin(angle) * radius]);
- node = iD.Node({loc: loc});
+ node = Node({loc: loc});
graph = graph.replace(node);
nodes.splice(endNodeIndex + j, 0, node);
@@ -166,7 +170,7 @@ export function Circularize(wayId, projection, maxAngle) {
// 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),
+ var point = interp(hull[i], hull[i+1], j / indexRange),
node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point));
graph = graph.replace(node);
}
diff --git a/modules/actions/disconnect.js b/modules/actions/disconnect.js
index c4aced41e..dbc2a08ed 100644
--- a/modules/actions/disconnect.js
+++ b/modules/actions/disconnect.js
@@ -12,6 +12,7 @@
// 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
//
+import { Node } from '../core/index';
export function Disconnect(nodeId, newNodeId) {
var wayIds;
@@ -21,7 +22,7 @@ export function Disconnect(nodeId, newNodeId) {
connections.forEach(function(connection) {
var way = graph.entity(connection.wayID),
- newNode = iD.Node({id: newNodeId, loc: node.loc, tags: node.tags});
+ newNode = Node({id: newNodeId, loc: node.loc, tags: node.tags});
graph = graph.replace(newNode);
if (connection.index === 0 && way.isArea()) {
diff --git a/modules/actions/join.js b/modules/actions/join.js
index 7f2b08b94..4eb093b7b 100644
--- a/modules/actions/join.js
+++ b/modules/actions/join.js
@@ -1,5 +1,3 @@
-import { DeleteWay } from './delete_way';
-
// Join ways at the end node they share.
//
// This is the inverse of `iD.actions.Split`.
@@ -8,6 +6,10 @@ import { DeleteWay } from './delete_way';
// 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
//
+import { joinWays } from '../geo/index';
+import { interestingTag } from '../core/index';
+import { DeleteWay } from './delete_way';
+
export function Join(ids) {
function groupEntitiesByGeometry(graph) {
@@ -27,7 +29,7 @@ export function Join(ids) {
}
}
- var joined = iD.geo.joinWays(ways, graph)[0];
+ var joined = joinWays(ways, graph)[0];
survivor = survivor.update({nodes: _.map(joined.nodes, 'id')});
graph = graph.replace(survivor);
@@ -54,7 +56,7 @@ export function Join(ids) {
if (ids.length < 2 || ids.length !== geometries.line.length)
return 'not_eligible';
- var joined = iD.geo.joinWays(ids.map(graph.entity, graph), graph);
+ var joined = joinWays(ids.map(graph.entity, graph), graph);
if (joined.length > 1)
return 'not_adjacent';
@@ -73,7 +75,7 @@ export function Join(ids) {
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]) {
+ } else if (tags[k] && interestingTag(k) && tags[k] !== way.tags[k]) {
conflicting = true;
}
}
diff --git a/modules/actions/merge_polygon.js b/modules/actions/merge_polygon.js
index 666e2a76f..ef4a114b9 100644
--- a/modules/actions/merge_polygon.js
+++ b/modules/actions/merge_polygon.js
@@ -1,3 +1,6 @@
+import { joinWays, polygonContainsPolygon } from '../geo/index';
+import { Relation } from '../core/index';
+
export function MergePolygon(ids, newRelationId) {
function groupEntities(graph) {
@@ -25,7 +28,7 @@ export function MergePolygon(ids, newRelationId) {
// 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));
+ return polygons.concat(joinWays(m.members, graph));
}, []).concat(entities.closedWay.map(function(d) {
var member = [{id: d.id}];
member.nodes = graph.childNodes(d);
@@ -38,7 +41,7 @@ export function MergePolygon(ids, newRelationId) {
var contained = polygons.map(function(w, i) {
return polygons.map(function(d, n) {
if (i === n) return null;
- return iD.geo.polygonContainsPolygon(
+ return polygonContainsPolygon(
_.map(d.nodes, 'loc'),
_.map(w.nodes, 'loc'));
});
@@ -79,7 +82,7 @@ export function MergePolygon(ids, newRelationId) {
// Move all tags to one relation
var relation = entities.multipolygon[0] ||
- iD.Relation({ id: newRelationId, tags: { type: 'multipolygon' }});
+ Relation({ id: newRelationId, tags: { type: 'multipolygon' }});
entities.multipolygon.slice(1).forEach(function(m) {
relation = relation.mergeTags(m.tags);
diff --git a/modules/actions/merge_remote_changes.js b/modules/actions/merge_remote_changes.js
index 6c2cb7012..07e118573 100644
--- a/modules/actions/merge_remote_changes.js
+++ b/modules/actions/merge_remote_changes.js
@@ -1,4 +1,5 @@
import { DeleteMultiple } from './delete_multiple';
+import { Entity } from '../core/index';
export function MergeRemoteChanges(id, localGraph, remoteGraph, formatUser) {
var option = 'safe', // 'safe', 'force_local', 'force_remote'
@@ -96,14 +97,14 @@ export function MergeRemoteChanges(id, localGraph, remoteGraph, formatUser) {
updates.replacements.push(remote);
} else if (option === 'force_local' && local) {
- target = iD.Entity(local);
+ target = 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 });
+ target = Entity(local, { version: remote.version });
if (remote.visible) {
target = mergeLocation(remote, target);
} else {
@@ -201,7 +202,7 @@ export function MergeRemoteChanges(id, localGraph, remoteGraph, formatUser) {
base = graph.base().entities[id],
local = localGraph.entity(id),
remote = remoteGraph.entity(id),
- target = iD.Entity(local, { version: remote.version });
+ target = Entity(local, { version: remote.version });
// delete/undelete
if (!remote.visible) {
diff --git a/modules/actions/move.js b/modules/actions/move.js
index 93257e20e..a1499d4cd 100644
--- a/modules/actions/move.js
+++ b/modules/actions/move.js
@@ -1,5 +1,15 @@
// 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
+import {
+ angle as getAngle,
+ sphericalDistance,
+ chooseEdge,
+ pathLength,
+ interp,
+ pathIntersections
+} from '../geo/index';
+import { Node } from '../core/index';
+
export function Move(moveIds, tryDelta, projection, cache) {
var delta = tryDelta;
@@ -114,7 +124,7 @@ export function Move(moveIds, tryDelta, projection, cache) {
var key = wayId + '_' + nodeId,
orig = cache.replacedVertex[key];
if (!orig) {
- orig = iD.Node();
+ orig = Node();
cache.replacedVertex[key] = orig;
cache.startLoc[orig.id] = cache.startLoc[nodeId];
}
@@ -128,21 +138,21 @@ export function Move(moveIds, tryDelta, projection, cache) {
}
orig = orig.move(end);
- var angle = Math.abs(iD.geo.angle(orig, prev, projection) -
- iD.geo.angle(orig, next, projection)) * 180 / Math.PI;
+ var angle = Math.abs(getAngle(orig, prev, projection) -
+ getAngle(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;
+ if (sphericalDistance(prev.loc, orig.loc) < 10 ||
+ 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),
+ d1 = pathLength(p1),
+ d2 = pathLength(p2),
insertAt = (d1 < d2) ? movedIndex : nextIndex;
// moving around closed loop?
@@ -169,17 +179,17 @@ export function Move(moveIds, tryDelta, projection, cache) {
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),
+ var edge1 = !isEP1 && chooseEdge(nodes1, projection(vertex.loc), projection),
+ edge2 = !isEP2 && 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);
+ loc = interp(edge1.loc, edge2.loc, 0.5);
+ edge1 = chooseEdge(nodes1, projection(loc), projection);
+ edge2 = chooseEdge(nodes2, projection(loc), projection);
if (Math.abs(edge1.distance - edge2.distance) < epsilon) break;
}
} else if (!isEP1) {
@@ -227,11 +237,11 @@ export function Move(moveIds, tryDelta, projection, cache) {
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);
+ hits = 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);
+ var edge = chooseEdge(unmovedNodes, end, projection);
delta = vecSub(projection(edge.loc), start);
}
});
diff --git a/modules/actions/orthogonalize.js b/modules/actions/orthogonalize.js
index c14ff50cc..f79c01c61 100644
--- a/modules/actions/orthogonalize.js
+++ b/modules/actions/orthogonalize.js
@@ -1,5 +1,5 @@
import { DeleteNode } from './delete_node';
-
+import { euclideanDistance } from '../geo/index';
/*
* Based on https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/potlatch2/tools/Quadrilateralise.as
*/
@@ -86,7 +86,7 @@ export function Orthogonalize(wayId, projection) {
q = subtractPoints(c, b),
scale, dotp;
- scale = 2 * Math.min(iD.geo.euclideanDistance(p, [0, 0]), iD.geo.euclideanDistance(q, [0, 0]));
+ scale = 2 * Math.min(euclideanDistance(p, [0, 0]), euclideanDistance(q, [0, 0]));
p = normalizePoint(p, 1.0);
q = normalizePoint(q, 1.0);
diff --git a/modules/actions/restrict_turn.js b/modules/actions/restrict_turn.js
index 49d9d7be3..1e94e2998 100644
--- a/modules/actions/restrict_turn.js
+++ b/modules/actions/restrict_turn.js
@@ -1,5 +1,6 @@
import { Split } from './split';
-
+import { inferRestriction } from '../geo/index';
+import { Relation, Way } from '../core/index';
// Create a restriction relation for `turn`, which must have the following structure:
//
// {
@@ -14,7 +15,7 @@ import { Split } from './split';
// (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.
+// 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
@@ -35,7 +36,7 @@ export function RestrictTurn(turn, projection, restrictionId) {
}
function split(toOrFrom) {
- var newID = toOrFrom.newID || iD.Way().id;
+ var newID = toOrFrom.newID || Way().id;
graph = Split(via.id, [newID])
.limitWays([toOrFrom.way])(graph);
@@ -68,12 +69,12 @@ export function RestrictTurn(turn, projection, restrictionId) {
to = split(turn.to)[0];
}
- return graph.replace(iD.Relation({
+ return graph.replace(Relation({
id: restrictionId,
tags: {
type: 'restriction',
restriction: turn.restriction ||
- iD.geo.inferRestriction(
+ inferRestriction(
graph,
turn.from,
turn.via,
diff --git a/modules/actions/split.js b/modules/actions/split.js
index 5cae44bb5..ef444404c 100644
--- a/modules/actions/split.js
+++ b/modules/actions/split.js
@@ -1,4 +1,8 @@
import { AddMember } from './add_member';
+import { sphericalDistance, isSimpleMultipolygonOuterMember} from '../geo/index';
+import { wrap as Wrap } from '../util/index';
+import { Way, Relation } from '../core/index';
+
// Split a way at the given node.
//
// Optionally, split only the given ways, if multiple ways share
@@ -34,11 +38,11 @@ export function Split(nodeId, newWayIds) {
idxB;
function wrap(index) {
- return iD.util.wrap(index, nodes.length);
+ return Wrap(index, nodes.length);
}
function dist(nA, nB) {
- return iD.geo.sphericalDistance(graph.entity(nA).loc, graph.entity(nB).loc);
+ return sphericalDistance(graph.entity(nA).loc, graph.entity(nB).loc);
}
// calculate lengths
@@ -68,11 +72,11 @@ export function Split(nodeId, newWayIds) {
}
function split(graph, wayA, newWayId) {
- var wayB = iD.Way({id: newWayId, tags: wayA.tags}),
+ var wayB = Way({id: newWayId, tags: wayA.tags}),
nodesA,
nodesB,
isArea = wayA.isArea(),
- isOuter = iD.geo.isSimpleMultipolygonOuterMember(wayA, graph);
+ isOuter = isSimpleMultipolygonOuterMember(wayA, graph);
if (wayA.isClosed()) {
var nodes = wayA.nodes.slice(0, -1),
@@ -123,7 +127,7 @@ export function Split(nodeId, newWayIds) {
});
if (!isOuter && isArea) {
- var multipolygon = iD.Relation({
+ var multipolygon = Relation({
tags: _.extend({}, wayA.tags, {type: 'multipolygon'}),
members: [
{id: wayA.id, role: 'outer', type: 'way'},
diff --git a/modules/core/connection.js b/modules/core/connection.js
index ad44447b3..5c0916054 100644
--- a/modules/core/connection.js
+++ b/modules/core/connection.js
@@ -2,6 +2,7 @@ import { Entity } from './entity';
import { Way } from './way';
import { Relation } from './relation';
import { Node } from './node';
+import { Extent } from '../geo/index';
export function Connection(useHttps) {
if (typeof useHttps !== 'boolean') {
@@ -384,7 +385,7 @@ export function Connection(useHttps) {
return {
id: tile.toString(),
- extent: iD.geo.Extent(
+ extent: Extent(
projection.invert([x, y + ts]),
projection.invert([x + ts, y]))
};
diff --git a/modules/core/graph.js b/modules/core/graph.js
index a46200ba1..a5bf356b5 100644
--- a/modules/core/graph.js
+++ b/modules/core/graph.js
@@ -1,3 +1,5 @@
+import { getPrototypeOf } from '../util/index';
+
export function Graph(other, mutable) {
if (!(this instanceof Graph)) return new Graph(other, mutable);
@@ -97,9 +99,9 @@ Graph.prototype = {
base: function() {
return {
- 'entities': iD.util.getPrototypeOf(this.entities),
- 'parentWays': iD.util.getPrototypeOf(this._parentWays),
- 'parentRels': iD.util.getPrototypeOf(this._parentRels)
+ 'entities': getPrototypeOf(this.entities),
+ 'parentWays': getPrototypeOf(this._parentWays),
+ 'parentRels': getPrototypeOf(this._parentRels)
};
},
diff --git a/modules/core/history.js b/modules/core/history.js
index ad9ae1243..fc4b36b58 100644
--- a/modules/core/history.js
+++ b/modules/core/history.js
@@ -2,12 +2,14 @@ import { Entity } from './entity';
import { Graph } from './graph';
import { Difference } from './difference';
import { Tree } from './tree';
+import { SessionMutex } from '../util/index';
+import { Loading } from '../ui/core/index';
export function History(context) {
var stack, index, tree,
imageryUsed = ['Bing'],
dispatch = d3.dispatch('change', 'undone', 'redone'),
- lock = iD.util.SessionMutex('lock');
+ lock = SessionMutex('lock');
function perform(actions) {
actions = Array.prototype.slice.call(actions);
@@ -291,7 +293,7 @@ export function History(context) {
loadComplete = false;
context.redrawEnable(false);
- var loading = iD.ui.Loading(context).blocking(true);
+ var loading = Loading(context).blocking(true);
context.container().call(loading);
var childNodesLoaded = function(err, result) {
diff --git a/modules/core/node.js b/modules/core/node.js
index c01375e45..929769141 100644
--- a/modules/core/node.js
+++ b/modules/core/node.js
@@ -1,5 +1,7 @@
// iD.Node = iD.Entity.node;
import { Entity } from './entity';
+import { Extent } from '../geo/index';
+
export function Node() {
if (!(this instanceof Node)) {
return (new Node()).initialize(arguments);
@@ -16,7 +18,7 @@ _.extend(Node.prototype, {
type: 'node',
extent: function() {
- return new iD.geo.Extent(this.loc);
+ return new Extent(this.loc);
},
geometry: function(graph) {
diff --git a/modules/core/relation.js b/modules/core/relation.js
index d9670cdac..395b962d8 100644
--- a/modules/core/relation.js
+++ b/modules/core/relation.js
@@ -1,4 +1,5 @@
import { Entity } from './entity';
+import { Extent, joinWays, polygonContainsPolygon, polygonIntersectsPolygon } from '../geo/index';
export function Relation() {
if (!(this instanceof Relation)) {
@@ -41,11 +42,11 @@ _.extend(Relation.prototype, {
extent: function(resolver, memo) {
return resolver.transient(this, 'extent', function() {
- if (memo && memo[this.id]) return iD.geo.Extent();
+ if (memo && memo[this.id]) return Extent();
memo = memo || {};
memo[this.id] = true;
- var extent = iD.geo.Extent();
+ var extent = Extent();
for (var i = 0; i < this.members.length; i++) {
var member = resolver.hasEntity(this.members[i].id);
if (member) {
@@ -224,8 +225,8 @@ _.extend(Relation.prototype, {
var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); }),
inners = this.members.filter(function(m) { return 'inner' === m.role; });
- outers = iD.geo.joinWays(outers, resolver);
- inners = iD.geo.joinWays(inners, resolver);
+ outers = joinWays(outers, resolver);
+ inners = joinWays(inners, resolver);
outers = outers.map(function(outer) { return _.map(outer.nodes, 'loc'); });
inners = inners.map(function(inner) { return _.map(inner.nodes, 'loc'); });
@@ -241,13 +242,13 @@ _.extend(Relation.prototype, {
for (o = 0; o < outers.length; o++) {
outer = outers[o];
- if (iD.geo.polygonContainsPolygon(outer, inner))
+ if (polygonContainsPolygon(outer, inner))
return o;
}
for (o = 0; o < outers.length; o++) {
outer = outers[o];
- if (iD.geo.polygonIntersectsPolygon(outer, inner))
+ if (polygonIntersectsPolygon(outer, inner))
return o;
}
}
diff --git a/modules/core/way.js b/modules/core/way.js
index f7788e2ff..dfcb455c9 100644
--- a/modules/core/way.js
+++ b/modules/core/way.js
@@ -1,5 +1,7 @@
import { Entity } from './entity';
import { oneWayTags } from './tags';
+import { cross, Extent } from '../geo/index';
+
export function Way() {
if (!(this instanceof Way)) {
return (new Way()).initialize(arguments);
@@ -34,7 +36,7 @@ _.extend(Way.prototype, {
extent: function(resolver) {
return resolver.transient(this, 'extent', function() {
- var extent = iD.geo.Extent();
+ var extent = Extent();
for (var i = 0; i < this.nodes.length; i++) {
var node = resolver.hasEntity(this.nodes[i]);
if (node) {
@@ -113,7 +115,7 @@ _.extend(Way.prototype, {
var o = coords[(i+1) % coords.length],
a = coords[i],
b = coords[(i+2) % coords.length],
- res = iD.geo.cross(o, a, b);
+ res = cross(o, a, b);
curr = (res > 0) ? 1 : (res < 0) ? -1 : 0;
if (curr === 0) {
diff --git a/modules/geo/intersection.js b/modules/geo/intersection.js
index f1c3c00ef..38c17c7a4 100644
--- a/modules/geo/intersection.js
+++ b/modules/geo/intersection.js
@@ -1,4 +1,5 @@
import { angle as getAngle } from './index';
+import { Way } from '../core/index';
export function Turn(turn) {
if (!(this instanceof Turn))
@@ -40,14 +41,14 @@ export function Intersection(graph, vertexId) {
var splitIndex, wayA, wayB, indexA, indexB;
if (isClosingNode) {
splitIndex = Math.ceil(way.nodes.length / 2); // split at midpoint
- wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)});
- wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
+ wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)});
+ wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
indexA = 1;
indexB = way.nodes.length - 2;
} else {
splitIndex = _.indexOf(way.nodes, vertex.id, 1); // split at vertexid
- wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)});
- wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
+ wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)});
+ wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
indexA = splitIndex - 1;
indexB = splitIndex + 1;
}
diff --git a/modules/index.js b/modules/index.js
new file mode 100644
index 000000000..227c4d95b
--- /dev/null
+++ b/modules/index.js
@@ -0,0 +1,18 @@
+import * as actions from './actions/index';
+import * as geo from './geo/index';
+
+export { Connection } from './core/connection';
+export { Difference } from './core/difference';
+export { Entity } from './core/entity';
+export { Graph } from './core/graph';
+export { History } from './core/history';
+export { Node } from './core/node';
+export { Relation } from './core/relation';
+export { oneWayTags, pavedTags, interestingTag } from './core/tags';
+export { Tree } from './core/tree';
+export { Way } from './core/way';
+
+export {
+ actions,
+ geo
+};
diff --git a/replaceStuff.js b/replaceStuff.js
new file mode 100644
index 000000000..fa722e78c
--- /dev/null
+++ b/replaceStuff.js
@@ -0,0 +1,68 @@
+var fs = require('fs');
+var args = require('minimist')(process.argv.slice(2));
+var global = [];
+function readFiles(dirname, outdir) {
+ var filenames = fs.readdirSync(dirname);
+ filenames.forEach(function(filename) {
+ var fileData = fs.readFileSync(dirname + filename).toString().split('\n');
+ processFile(fileData, outdir + filename);
+ });
+}
+var POSSIBLE_MODULES = [ 'actions', 'geo', 'modes', 'util', 'core', 'behavior' ];
+function findData(data) {
+ var modules = { 'actions': [], 'geo': [], 'modes': [], 'util': [], 'core': [], 'behavior': [] };
+ var cores = [ 'Entity', 'Way', 'Relation', 'Node', 'Graph', 'Tree', 'Difference', 'History' ];
+ var ret = data.map(function(lineArg) {
+ var line = lineArg;
+ POSSIBLE_MODULES.forEach(function(mod) {
+ cores.forEach(function(c) {
+ while (line.indexOf('iD.' + c) > -1 ) {
+ var start = line.indexOf('iD.' + c);
+ var prefix = 3;
+ line = line.slice(0, start) + line.slice(start + prefix);
+ if (modules.core.indexOf(c) === -1) {
+ modules.core.push(c);
+ console.log(c);
+ }
+ }
+ });
+
+ while (line.indexOf('iD.' + mod + '.') > -1 ) {
+ var start = line.indexOf('iD.' + mod + '.');
+ var prefix = 3 + mod.length + 1;
+ var end = line.indexOf('(', start);
+ var foo = line.slice(start + prefix, end);
+ if (modules[mod].indexOf(foo) === -1) {
+ modules[mod].push(foo);
+ }
+ line = line.slice(0, start) + line.slice(start + prefix);
+ }
+ });
+ return line;
+ });
+ POSSIBLE_MODULES.forEach(function(mod) {
+ if (modules[mod].length > 0) {
+ var importStuff = modules[mod].join(', ');
+ ret.unshift(`import { ${importStuff} } from '../${mod}/index';`);
+ /*eslint-disable */
+ /*eslint-enable */
+ }
+ });
+
+ return ret;
+}
+
+function processFile(fd, name) {
+ global.push({
+ name: name,
+ data: fd
+ });
+}
+
+readFiles(args.dir, args.out);
+
+
+global.forEach(function (f) {
+ var processedData = findData(f.data);
+ fs.writeFile(f.name, processedData.join('\n'), function (e) {if (e) console.log(e);});
+});
diff --git a/test/index.html b/test/index.html
index 125e39bab..d118c0f38 100644
--- a/test/index.html
+++ b/test/index.html
@@ -41,10 +41,8 @@
-
-
+
-