(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 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 = iD.geo.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 = iD.geo.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 iD.geo.Extent(); memo = memo || {}; memo[this.id] = true; var extent = iD.geo.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 = iD.geo.joinWays(outers, resolver); inners = iD.geo.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 (iD.geo.polygonContainsPolygon(outer, inner)) return o; } for (o = 0; o < outers.length; o++) { outer = outers[o]; if (iD.geo.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 iD.geo.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: iD.geo.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; } 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': iD.util.getPrototypeOf(this.entities), 'parentWays': iD.util.getPrototypeOf(this._parentWays), 'parentRels': iD.util.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 History(context) { var stack, index, tree, imageryUsed = ['Bing'], dispatch = d3.dispatch('change', 'undone', 'redone'), lock = iD.util.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 = iD.ui.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'); } 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 }); }));