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