diff --git a/Makefile b/Makefile index 70f5d558e..e8ec9c8f8 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,11 @@ MODULE_TARGETS = \ js/lib/id/util.js \ js/lib/id/validations.js \ js/lib/id/geo.js \ - js/lib/id/operations.js + js/lib/id/operations.js \ + js/lib/id/core.js + +js/lib/id/core.js: modules/ + node_modules/.bin/rollup -f umd -n iD modules/core/index.js --no-strict > $@ js/lib/id/actions.js: modules/ node_modules/.bin/rollup -f umd -n iD.actions modules/actions/index.js --no-strict > $@ @@ -115,16 +119,6 @@ dist/iD.js: \ js/id/behavior/paste.js \ js/id/behavior/select.js \ js/id/behavior/tail.js \ - js/id/core/connection.js \ - js/id/core/difference.js \ - js/id/core/entity.js \ - js/id/core/graph.js \ - js/id/core/history.js \ - js/id/core/node.js \ - js/id/core/relation.js \ - js/id/core/tags.js \ - js/id/core/tree.js \ - js/id/core/way.js \ js/id/renderer/background.js \ js/id/renderer/background_source.js \ js/id/renderer/features.js \ diff --git a/index.html b/index.html index 16256a922..147a8c40e 100644 --- a/index.html +++ b/index.html @@ -34,6 +34,7 @@ + @@ -159,17 +160,6 @@ - - - - - - - - - - - diff --git a/js/lib/id/core.js b/js/lib/id/core.js new file mode 100644 index 000000000..4552438f1 --- /dev/null +++ b/js/lib/id/core.js @@ -0,0 +1,2259 @@ +(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 }); + +})); \ No newline at end of file diff --git a/js/id/core/connection.js b/modules/core/connection.js similarity index 95% rename from js/id/core/connection.js rename to modules/core/connection.js index c000305a5..ad44447b3 100644 --- a/js/id/core/connection.js +++ b/modules/core/connection.js @@ -1,4 +1,9 @@ -iD.Connection = function(useHttps) { +import { Entity } from './entity'; +import { Way } from './way'; +import { Relation } from './relation'; +import { Node } from './node'; + +export function Connection(useHttps) { if (typeof useHttps !== 'boolean') { useHttps = window.location.protocol === 'https:'; } @@ -55,8 +60,8 @@ iD.Connection = function(useHttps) { }; connection.loadEntity = function(id, callback) { - var type = iD.Entity.id.type(id), - osmID = iD.Entity.id.toOSM(id); + var type = Entity.id.type(id), + osmID = Entity.id.toOSM(id); connection.loadFromURL( url + '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : ''), @@ -66,8 +71,8 @@ iD.Connection = function(useHttps) { }; connection.loadEntityVersion = function(id, version, callback) { - var type = iD.Entity.id.type(id), - osmID = iD.Entity.id.toOSM(id); + var type = Entity.id.type(id), + osmID = Entity.id.toOSM(id); connection.loadFromURL( url + '/api/0.6/' + type + '/' + osmID + '/' + version, @@ -77,9 +82,9 @@ iD.Connection = function(useHttps) { }; connection.loadMultiple = function(ids, callback) { - _.each(_.groupBy(_.uniq(ids), iD.Entity.id.type), function(v, k) { + _.each(_.groupBy(_.uniq(ids), Entity.id.type), function(v, k) { var type = k + 's', - osmIDs = _.map(v, iD.Entity.id.toOSM); + osmIDs = _.map(v, Entity.id.toOSM); _.each(_.chunk(osmIDs, 150), function(arr) { connection.loadFromURL( @@ -145,8 +150,8 @@ iD.Connection = function(useHttps) { var parsers = { node: function nodeData(obj) { var attrs = obj.attributes; - return new iD.Node({ - id: iD.Entity.id.fromOSM(nodeStr, attrs.id.value), + return new Node({ + id: Entity.id.fromOSM(nodeStr, attrs.id.value), loc: getLoc(attrs), version: attrs.version.value, user: attrs.user && attrs.user.value, @@ -157,8 +162,8 @@ iD.Connection = function(useHttps) { way: function wayData(obj) { var attrs = obj.attributes; - return new iD.Way({ - id: iD.Entity.id.fromOSM(wayStr, attrs.id.value), + return new Way({ + id: Entity.id.fromOSM(wayStr, attrs.id.value), version: attrs.version.value, user: attrs.user && attrs.user.value, tags: getTags(obj), @@ -169,8 +174,8 @@ iD.Connection = function(useHttps) { relation: function relationData(obj) { var attrs = obj.attributes; - return new iD.Relation({ - id: iD.Entity.id.fromOSM(relationStr, attrs.id.value), + return new Relation({ + id: Entity.id.fromOSM(relationStr, attrs.id.value), version: attrs.version.value, user: attrs.user && attrs.user.value, tags: getTags(obj), @@ -466,4 +471,4 @@ iD.Connection = function(useHttps) { }; return d3.rebind(connection, event, 'on'); -}; +} diff --git a/js/id/core/difference.js b/modules/core/difference.js similarity index 99% rename from js/id/core/difference.js rename to modules/core/difference.js index 9f95846b5..f60553c34 100644 --- a/js/id/core/difference.js +++ b/modules/core/difference.js @@ -6,7 +6,7 @@ of entities that will require a redraw, taking into account child and parent relationships. */ -iD.Difference = function(base, head) { +export function Difference(base, head) { var changes = {}, length = 0; function changed(h, b) { @@ -173,4 +173,4 @@ iD.Difference = function(base, head) { }; return difference; -}; +} diff --git a/js/id/core/entity.js b/modules/core/entity.js similarity index 77% rename from js/id/core/entity.js rename to modules/core/entity.js index 6953a51c5..1004b403c 100644 --- a/js/id/core/entity.js +++ b/modules/core/entity.js @@ -1,42 +1,44 @@ -iD.Entity = function(attrs) { +import { interestingTag } from './tags'; + +export function Entity(attrs) { // For prototypal inheritance. - if (this instanceof iD.Entity) return; + if (this instanceof Entity) return; // Create the appropriate subtype. if (attrs && attrs.type) { - return iD.Entity[attrs.type].apply(this, arguments); + return Entity[attrs.type].apply(this, arguments); } else if (attrs && attrs.id) { - return iD.Entity[iD.Entity.id.type(attrs.id)].apply(this, arguments); + return Entity[Entity.id.type(attrs.id)].apply(this, arguments); } // Initialize a generic Entity (used only in tests). - return (new iD.Entity()).initialize(arguments); + return (new Entity()).initialize(arguments); +} + +Entity.id = function(type) { + return Entity.id.fromOSM(type, Entity.id.next[type]--); }; -iD.Entity.id = function(type) { - return iD.Entity.id.fromOSM(type, iD.Entity.id.next[type]--); -}; +Entity.id.next = {node: -1, way: -1, relation: -1}; -iD.Entity.id.next = {node: -1, way: -1, relation: -1}; - -iD.Entity.id.fromOSM = function(type, id) { +Entity.id.fromOSM = function(type, id) { return type[0] + id; }; -iD.Entity.id.toOSM = function(id) { +Entity.id.toOSM = function(id) { return id.slice(1); }; -iD.Entity.id.type = function(id) { +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(). -iD.Entity.key = function(entity) { +Entity.key = function(entity) { return entity.id + 'v' + (entity.v || 0); }; -iD.Entity.prototype = { +Entity.prototype = { tags: {}, initialize: function(sources) { @@ -54,7 +56,7 @@ iD.Entity.prototype = { } if (!this.id && this.type) { - this.id = iD.Entity.id(this.type); + this.id = Entity.id(this.type); } if (!this.hasOwnProperty('visible')) { this.visible = true; @@ -76,14 +78,14 @@ iD.Entity.prototype = { if (copies[this.id]) return copies[this.id]; - var copy = iD.Entity(this, {id: undefined, user: undefined, version: undefined}); + var copy = Entity(this, {id: undefined, user: undefined, version: undefined}); copies[this.id] = copy; return copy; }, osmId: function() { - return iD.Entity.id.toOSM(this.id); + return Entity.id.toOSM(this.id); }, isNew: function() { @@ -91,7 +93,7 @@ iD.Entity.prototype = { }, update: function(attrs) { - return iD.Entity(this, attrs, {v: 1 + (this.v || 0)}); + return Entity(this, attrs, {v: 1 + (this.v || 0)}); }, mergeTags: function(tags) { @@ -120,7 +122,7 @@ iD.Entity.prototype = { }, hasInterestingTags: function() { - return _.keys(this.tags).some(iD.interestingTag); + return _.keys(this.tags).some(interestingTag); }, isHighwayIntersection: function() { diff --git a/js/id/core/graph.js b/modules/core/graph.js similarity index 97% rename from js/id/core/graph.js rename to modules/core/graph.js index a17a6479b..a46200ba1 100644 --- a/js/id/core/graph.js +++ b/modules/core/graph.js @@ -1,7 +1,7 @@ -iD.Graph = function(other, mutable) { - if (!(this instanceof iD.Graph)) return new iD.Graph(other, mutable); +export function Graph(other, mutable) { + if (!(this instanceof Graph)) return new Graph(other, mutable); - if (other instanceof iD.Graph) { + 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); @@ -17,9 +17,9 @@ iD.Graph = function(other, mutable) { this.transients = {}; this._childNodes = {}; this.frozen = !mutable; -}; +} -iD.Graph.prototype = { +Graph.prototype = { hasEntity: function(id) { return this.entities[id]; }, @@ -263,7 +263,7 @@ iD.Graph.prototype = { }, update: function() { - var graph = this.frozen ? iD.Graph(this, true) : this; + var graph = this.frozen ? Graph(this, true) : this; for (var i = 0; i < arguments.length; i++) { arguments[i].call(graph, graph); diff --git a/js/id/core/history.js b/modules/core/history.js similarity index 93% rename from js/id/core/history.js rename to modules/core/history.js index f262300a8..ad9ae1243 100644 --- a/js/id/core/history.js +++ b/modules/core/history.js @@ -1,4 +1,9 @@ -iD.History = function(context) { +import { Entity } from './entity'; +import { Graph } from './graph'; +import { Difference } from './difference'; +import { Tree } from './tree'; + +export function History(context) { var stack, index, tree, imageryUsed = ['Bing'], dispatch = d3.dispatch('change', 'undone', 'redone'), @@ -26,7 +31,7 @@ iD.History = function(context) { } function change(previous) { - var difference = iD.Difference(previous, history.graph()); + var difference = Difference(previous, history.graph()); dispatch.change(difference); return difference; } @@ -144,7 +149,7 @@ iD.History = function(context) { difference: function() { var base = stack[0].graph, head = stack[index].graph; - return iD.Difference(base, head); + return Difference(base, head); }, changes: function(action) { @@ -155,7 +160,7 @@ iD.History = function(context) { head = action(head); } - var difference = iD.Difference(base, head); + var difference = Difference(base, head); return { modified: difference.modified(), @@ -190,9 +195,9 @@ iD.History = function(context) { }, reset: function() { - stack = [{graph: iD.Graph()}]; + stack = [{graph: Graph()}]; index = 0; - tree = iD.Tree(stack[0].graph); + tree = Tree(stack[0].graph); dispatch.change(); return history; }, @@ -209,7 +214,7 @@ iD.History = function(context) { _.forEach(i.graph.entities, function(entity, id) { if (entity) { - var key = iD.Entity.key(entity); + var key = Entity.key(entity); allEntities[key] = entity; modified.push(key); } else { @@ -244,7 +249,7 @@ iD.History = function(context) { entities: _.values(allEntities), baseEntities: _.values(baseEntities), stack: s, - nextIDs: iD.Entity.id.next, + nextIDs: Entity.id.next, index: index }); }, @@ -253,21 +258,21 @@ iD.History = function(context) { var h = JSON.parse(json), loadComplete = true; - iD.Entity.id.next = h.nextIDs; + Entity.id.next = h.nextIDs; index = h.index; if (h.version === 2 || h.version === 3) { var allEntities = {}; h.entities.forEach(function(entity) { - allEntities[iD.Entity.key(entity)] = iD.Entity(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 iD.Entity(d); }); + var baseEntities = h.baseEntities.map(function(d) { return Entity(d); }); stack[0].graph.rebase(baseEntities, _.map(stack, 'graph'), true); tree.rebase(baseEntities, true); @@ -334,7 +339,7 @@ iD.History = function(context) { } return { - graph: iD.Graph(stack[0].graph).load(entities), + graph: Graph(stack[0].graph).load(entities), annotation: d.annotation, imageryUsed: d.imageryUsed }; @@ -346,10 +351,10 @@ iD.History = function(context) { for (var i in d.entities) { var entity = d.entities[i]; - entities[i] = entity === 'undefined' ? undefined : iD.Entity(entity); + entities[i] = entity === 'undefined' ? undefined : Entity(entity); } - d.graph = iD.Graph(stack[0].graph).load(entities); + d.graph = Graph(stack[0].graph).load(entities); return d; }); } @@ -401,4 +406,4 @@ iD.History = function(context) { history.reset(); return d3.rebind(history, dispatch, 'on'); -}; +} diff --git a/modules/core/index.js b/modules/core/index.js new file mode 100644 index 000000000..afad4fa10 --- /dev/null +++ b/modules/core/index.js @@ -0,0 +1,10 @@ +export { Connection } from './connection'; +export { Difference } from './difference'; +export { Entity } from './entity'; +export { Graph } from './graph'; +export { History } from './history'; +export { Node } from './node'; +export { Relation } from './relation'; +export { oneWayTags, pavedTags, interestingTag } from './tags'; +export { Tree } from './tree'; +export { Way } from './way'; diff --git a/js/id/core/node.js b/modules/core/node.js similarity index 86% rename from js/id/core/node.js rename to modules/core/node.js index 03674f7e3..c01375e45 100644 --- a/js/id/core/node.js +++ b/modules/core/node.js @@ -1,14 +1,18 @@ -iD.Node = iD.Entity.node = function iD_Node() { - if (!(this instanceof iD_Node)) { - return (new iD_Node()).initialize(arguments); +// iD.Node = iD.Entity.node; +import { Entity } from './entity'; +export function Node() { + if (!(this instanceof Node)) { + return (new Node()).initialize(arguments); } else if (arguments.length) { this.initialize(arguments); } -}; +} -iD.Node.prototype = Object.create(iD.Entity.prototype); +Entity.node = Node; -_.extend(iD.Node.prototype, { +Node.prototype = Object.create(Entity.prototype); + +_.extend(Node.prototype, { type: 'node', extent: function() { diff --git a/js/id/core/relation.js b/modules/core/relation.js similarity index 94% rename from js/id/core/relation.js rename to modules/core/relation.js index daf19f3ef..d9670cdac 100644 --- a/js/id/core/relation.js +++ b/modules/core/relation.js @@ -1,22 +1,25 @@ -iD.Relation = iD.Entity.relation = function iD_Relation() { - if (!(this instanceof iD_Relation)) { - return (new iD_Relation()).initialize(arguments); +import { Entity } from './entity'; + +export function Relation() { + if (!(this instanceof Relation)) { + return (new Relation()).initialize(arguments); } else if (arguments.length) { this.initialize(arguments); } -}; +} +Entity.relation = Relation; -iD.Relation.prototype = Object.create(iD.Entity.prototype); +Relation.prototype = Object.create(Entity.prototype); -iD.Relation.creationOrder = function(a, b) { - var aId = parseInt(iD.Entity.id.toOSM(a.id), 10); - var bId = parseInt(iD.Entity.id.toOSM(b.id), 10); +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(iD.Relation.prototype, { +_.extend(Relation.prototype, { type: 'relation', members: [], @@ -24,7 +27,7 @@ _.extend(iD.Relation.prototype, { if (copies[this.id]) return copies[this.id]; - var copy = iD.Entity.prototype.copy.call(this, resolver, copies); + 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}); @@ -154,7 +157,7 @@ _.extend(iD.Relation.prototype, { '@id': this.osmId(), '@version': this.version || 0, member: _.map(this.members, function(member) { - return { keyAttributes: { type: member.type, role: member.role, ref: iD.Entity.id.toOSM(member.id) } }; + 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 } }; diff --git a/js/id/core/tags.js b/modules/core/tags.js similarity index 90% rename from js/id/core/tags.js rename to modules/core/tags.js index ae4a87f27..c857fe606 100644 --- a/js/id/core/tags.js +++ b/modules/core/tags.js @@ -1,4 +1,13 @@ -iD.oneWayTags = { +export function interestingTag(key) { + return key !== 'attribution' && + key !== 'created_by' && + key !== 'source' && + key !== 'odbl' && + key.indexOf('tiger:') !== 0; + +} + +export var oneWayTags = { 'aerialway': { 'chair_lift': true, 'mixed_lift': true, @@ -30,7 +39,7 @@ iD.oneWayTags = { } }; -iD.pavedTags = { +export var pavedTags = { 'surface': { 'paved': true, 'asphalt': true, @@ -40,12 +49,3 @@ iD.pavedTags = { 'grade1': true } }; - -iD.interestingTag = function (key) { - return key !== 'attribution' && - key !== 'created_by' && - key !== 'source' && - key !== 'odbl' && - key.indexOf('tiger:') !== 0; - -}; diff --git a/js/id/core/tree.js b/modules/core/tree.js similarity index 95% rename from js/id/core/tree.js rename to modules/core/tree.js index db53c9599..99741d875 100644 --- a/js/id/core/tree.js +++ b/modules/core/tree.js @@ -1,4 +1,6 @@ -iD.Tree = function(head) { +import { Difference } from './difference'; + +export function Tree(head) { var rtree = rbush(), rectangles = {}; @@ -59,7 +61,7 @@ iD.Tree = function(head) { tree.intersects = function(extent, graph) { if (graph !== head) { - var diff = iD.Difference(head, graph), + var diff = Difference(head, graph), insertions = {}; head = graph; @@ -88,4 +90,4 @@ iD.Tree = function(head) { }; return tree; -}; +} diff --git a/js/id/core/way.js b/modules/core/way.js similarity index 93% rename from js/id/core/way.js rename to modules/core/way.js index d6e2aeb5c..f7788e2ff 100644 --- a/js/id/core/way.js +++ b/modules/core/way.js @@ -1,14 +1,18 @@ -iD.Way = iD.Entity.way = function iD_Way() { - if (!(this instanceof iD_Way)) { - return (new iD_Way()).initialize(arguments); +import { Entity } from './entity'; +import { oneWayTags } from './tags'; +export function Way() { + if (!(this instanceof Way)) { + return (new Way()).initialize(arguments); } else if (arguments.length) { this.initialize(arguments); } -}; +} -iD.Way.prototype = Object.create(iD.Entity.prototype); +Entity.way = Way; -_.extend(iD.Way.prototype, { +Way.prototype = Object.create(Entity.prototype); + +_.extend(Way.prototype, { type: 'way', nodes: [], @@ -16,7 +20,7 @@ _.extend(iD.Way.prototype, { if (copies[this.id]) return copies[this.id]; - var copy = iD.Entity.prototype.copy.call(this, resolver, copies); + var copy = Entity.prototype.copy.call(this, resolver, copies); var nodes = this.nodes.map(function(id) { return resolver.entity(id).copy(resolver, copies).id; @@ -88,7 +92,7 @@ _.extend(iD.Way.prototype, { // implied oneway tag.. for (var key in this.tags) { - if (key in iD.oneWayTags && (this.tags[key] in iD.oneWayTags[key])) + if (key in oneWayTags && (this.tags[key] in oneWayTags[key])) return true; } return false; @@ -202,7 +206,7 @@ _.extend(iD.Way.prototype, { '@id': this.osmId(), '@version': this.version || 0, nd: _.map(this.nodes, function(id) { - return { keyAttributes: { ref: iD.Entity.id.toOSM(id) } }; + return { keyAttributes: { ref: Entity.id.toOSM(id) } }; }), tag: _.map(this.tags, function(v, k) { return { keyAttributes: { k: k, v: v } }; diff --git a/test/index.html b/test/index.html index 01c41b4f3..ab009649b 100644 --- a/test/index.html +++ b/test/index.html @@ -41,6 +41,7 @@ + @@ -144,17 +145,6 @@ - - - - - - - - - - -