mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-20 20:53:37 +00:00
2259 lines
75 KiB
JavaScript
2259 lines
75 KiB
JavaScript
(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 });
|
|
|
|
})); |