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