Files
iD/js/lib/id/index.js
T
2016-06-26 21:34:10 -04:00

13862 lines
426 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 AddEntity(way) {
return function(graph) {
return graph.replace(way);
};
}
function Extent(min, max) {
if (!(this instanceof Extent)) return new Extent(min, max);
if (min instanceof Extent) {
return min;
} else if (min && min.length === 2 && min[0].length === 2 && min[1].length === 2) {
this[0] = min[0];
this[1] = min[1];
} else {
this[0] = min || [ Infinity, Infinity];
this[1] = max || min || [-Infinity, -Infinity];
}
}
Extent.prototype = new Array(2);
_.extend(Extent.prototype, {
equals: function (obj) {
return this[0][0] === obj[0][0] &&
this[0][1] === obj[0][1] &&
this[1][0] === obj[1][0] &&
this[1][1] === obj[1][1];
},
extend: function(obj) {
if (!(obj instanceof Extent)) obj = new Extent(obj);
return Extent([Math.min(obj[0][0], this[0][0]),
Math.min(obj[0][1], this[0][1])],
[Math.max(obj[1][0], this[1][0]),
Math.max(obj[1][1], this[1][1])]);
},
_extend: function(extent) {
this[0][0] = Math.min(extent[0][0], this[0][0]);
this[0][1] = Math.min(extent[0][1], this[0][1]);
this[1][0] = Math.max(extent[1][0], this[1][0]);
this[1][1] = Math.max(extent[1][1], this[1][1]);
},
area: function() {
return Math.abs((this[1][0] - this[0][0]) * (this[1][1] - this[0][1]));
},
center: function() {
return [(this[0][0] + this[1][0]) / 2,
(this[0][1] + this[1][1]) / 2];
},
rectangle: function() {
return [this[0][0], this[0][1], this[1][0], this[1][1]];
},
polygon: function() {
return [
[this[0][0], this[0][1]],
[this[0][0], this[1][1]],
[this[1][0], this[1][1]],
[this[1][0], this[0][1]],
[this[0][0], this[0][1]]
];
},
contains: function(obj) {
if (!(obj instanceof Extent)) obj = new Extent(obj);
return obj[0][0] >= this[0][0] &&
obj[0][1] >= this[0][1] &&
obj[1][0] <= this[1][0] &&
obj[1][1] <= this[1][1];
},
intersects: function(obj) {
if (!(obj instanceof Extent)) obj = new Extent(obj);
return obj[0][0] <= this[1][0] &&
obj[0][1] <= this[1][1] &&
obj[1][0] >= this[0][0] &&
obj[1][1] >= this[0][1];
},
intersection: function(obj) {
if (!this.intersects(obj)) return new Extent();
return new Extent([Math.max(obj[0][0], this[0][0]),
Math.max(obj[0][1], this[0][1])],
[Math.min(obj[1][0], this[1][0]),
Math.min(obj[1][1], this[1][1])]);
},
percentContainedIn: function(obj) {
if (!(obj instanceof Extent)) obj = new Extent(obj);
var a1 = this.intersection(obj).area(),
a2 = this.area();
if (a1 === Infinity || a2 === Infinity || a1 === 0 || a2 === 0) {
return 0;
} else {
return a1 / a2;
}
},
padByMeters: function(meters) {
var dLat = metersToLat(meters),
dLon = metersToLon(meters, this.center()[1]);
return Extent(
[this[0][0] - dLon, this[0][1] - dLat],
[this[1][0] + dLon, this[1][1] + dLat]);
},
toParam: function() {
return this.rectangle().join(',');
}
});
function interestingTag(key) {
return key !== 'attribution' &&
key !== 'created_by' &&
key !== 'source' &&
key !== 'odbl' &&
key.indexOf('tiger:') !== 0;
}
var oneWayTags = {
'aerialway': {
'chair_lift': true,
'mixed_lift': true,
't-bar': true,
'j-bar': true,
'platter': true,
'rope_tow': true,
'magic_carpet': true,
'yes': true
},
'highway': {
'motorway': true,
'motorway_link': true
},
'junction': {
'roundabout': true
},
'man_made': {
'piste:halfpipe': true
},
'piste:type': {
'downhill': true,
'sled': true,
'yes': true
},
'waterway': {
'river': true,
'stream': true
}
};
var pavedTags = {
'surface': {
'paved': true,
'asphalt': true,
'concrete': true
},
'tracktype': {
'grade1': true
}
};
function Entity(attrs) {
// For prototypal inheritance.
if (this instanceof Entity) return;
// Create the appropriate subtype.
if (attrs && attrs.type) {
return Entity[attrs.type].apply(this, arguments);
} else if (attrs && attrs.id) {
return Entity[Entity.id.type(attrs.id)].apply(this, arguments);
}
// Initialize a generic Entity (used only in tests).
return (new Entity()).initialize(arguments);
}
Entity.id = function(type) {
return Entity.id.fromOSM(type, Entity.id.next[type]--);
};
Entity.id.next = {node: -1, way: -1, relation: -1};
Entity.id.fromOSM = function(type, id) {
return type[0] + id;
};
Entity.id.toOSM = function(id) {
return id.slice(1);
};
Entity.id.type = function(id) {
return {'n': 'node', 'w': 'way', 'r': 'relation'}[id[0]];
};
// A function suitable for use as the second argument to d3.selection#data().
Entity.key = function(entity) {
return entity.id + 'v' + (entity.v || 0);
};
Entity.prototype = {
tags: {},
initialize: function(sources) {
for (var i = 0; i < sources.length; ++i) {
var source = sources[i];
for (var prop in source) {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
if (source[prop] === undefined) {
delete this[prop];
} else {
this[prop] = source[prop];
}
}
}
}
if (!this.id && this.type) {
this.id = Entity.id(this.type);
}
if (!this.hasOwnProperty('visible')) {
this.visible = true;
}
if (iD.debug) {
Object.freeze(this);
Object.freeze(this.tags);
if (this.loc) Object.freeze(this.loc);
if (this.nodes) Object.freeze(this.nodes);
if (this.members) Object.freeze(this.members);
}
return this;
},
copy: function(resolver, copies) {
if (copies[this.id])
return copies[this.id];
var copy = Entity(this, {id: undefined, user: undefined, version: undefined});
copies[this.id] = copy;
return copy;
},
osmId: function() {
return Entity.id.toOSM(this.id);
},
isNew: function() {
return this.osmId() < 0;
},
update: function(attrs) {
return Entity(this, attrs, {v: 1 + (this.v || 0)});
},
mergeTags: function(tags) {
var merged = _.clone(this.tags), changed = false;
for (var k in tags) {
var t1 = merged[k],
t2 = tags[k];
if (!t1) {
changed = true;
merged[k] = t2;
} else if (t1 !== t2) {
changed = true;
merged[k] = _.union(t1.split(/;\s*/), t2.split(/;\s*/)).join(';');
}
}
return changed ? this.update({tags: merged}) : this;
},
intersects: function(extent, resolver) {
return this.extent(resolver).intersects(extent);
},
isUsed: function(resolver) {
return _.without(Object.keys(this.tags), 'area').length > 0 ||
resolver.parentRelations(this).length > 0;
},
hasInterestingTags: function() {
return _.keys(this.tags).some(interestingTag);
},
isHighwayIntersection: function() {
return false;
},
deprecatedTags: function() {
var tags = _.toPairs(this.tags);
var deprecated = {};
iD.data.deprecated.forEach(function(d) {
var match = _.toPairs(d.old)[0];
tags.forEach(function(t) {
if (t[0] === match[0] &&
(t[1] === match[1] || match[1] === '*')) {
deprecated[t[0]] = t[1];
}
});
});
return deprecated;
}
};
function Way() {
if (!(this instanceof Way)) {
return (new Way()).initialize(arguments);
} else if (arguments.length) {
this.initialize(arguments);
}
}
Entity.way = Way;
Way.prototype = Object.create(Entity.prototype);
_.extend(Way.prototype, {
type: 'way',
nodes: [],
copy: function(resolver, copies) {
if (copies[this.id])
return copies[this.id];
var copy = Entity.prototype.copy.call(this, resolver, copies);
var nodes = this.nodes.map(function(id) {
return resolver.entity(id).copy(resolver, copies).id;
});
copy = copy.update({nodes: nodes});
copies[this.id] = copy;
return copy;
},
extent: function(resolver) {
return resolver.transient(this, 'extent', function() {
var extent = Extent();
for (var i = 0; i < this.nodes.length; i++) {
var node = resolver.hasEntity(this.nodes[i]);
if (node) {
extent._extend(node.extent());
}
}
return extent;
});
},
first: function() {
return this.nodes[0];
},
last: function() {
return this.nodes[this.nodes.length - 1];
},
contains: function(node) {
return this.nodes.indexOf(node) >= 0;
},
affix: function(node) {
if (this.nodes[0] === node) return 'prefix';
if (this.nodes[this.nodes.length - 1] === node) return 'suffix';
},
layer: function() {
// explicit layer tag, clamp between -10, 10..
if (this.tags.layer !== undefined) {
return Math.max(-10, Math.min(+(this.tags.layer), 10));
}
// implied layer tag..
if (this.tags.location === 'overground') return 1;
if (this.tags.location === 'underground') return -1;
if (this.tags.location === 'underwater') return -10;
if (this.tags.power === 'line') return 10;
if (this.tags.power === 'minor_line') return 10;
if (this.tags.aerialway) return 10;
if (this.tags.bridge) return 1;
if (this.tags.cutting) return -1;
if (this.tags.tunnel) return -1;
if (this.tags.waterway) return -1;
if (this.tags.man_made === 'pipeline') return -10;
if (this.tags.boundary) return -10;
return 0;
},
isOneWay: function() {
// explicit oneway tag..
if (['yes', '1', '-1'].indexOf(this.tags.oneway) !== -1) { return true; }
if (['no', '0'].indexOf(this.tags.oneway) !== -1) { return false; }
// implied oneway tag..
for (var key in this.tags) {
if (key in oneWayTags && (this.tags[key] in oneWayTags[key]))
return true;
}
return false;
},
lanes: function() {
// function parseTurnLane(str) {
// if (!str || str === '') return null;
//
// return str.split('|').map(function(s) {
// return s.split(';');
// });
// }
if (!this.tags.highway) return null;
var defaultLanes = {}, tagged = {};
switch (this.tags.highway) {
case 'trunk':
case 'motorway':
defaultLanes.count = this.isOneWay() ? 2 : 4;
break;
default:
defaultLanes.count = this.isOneWay() ? 1 : 2;
break;
}
tagged.oneway = this.isOneWay();
tagged.lanes = {};
if (this.tags.lanes) tagged.lanes.count = this.tags.lanes;
if (this.tags['lanes:forward']) tagged.lanes.forward = this.tags['lanes:forward'];
if (this.tags['lanes:backward']) tagged.lanes.backward = this.tags['lanes:backward'];
return {
defaults: {
lanes: defaultLanes
},
tagged: tagged
};
},
isClosed: function() {
return this.nodes.length > 0 && this.first() === this.last();
},
isConvex: function(resolver) {
if (!this.isClosed() || this.isDegenerate()) return null;
var nodes = _.uniq(resolver.childNodes(this)),
coords = _.map(nodes, 'loc'),
curr = 0, prev = 0;
for (var i = 0; i < coords.length; i++) {
var o = coords[(i+1) % coords.length],
a = coords[i],
b = coords[(i+2) % coords.length],
res = cross(o, a, b);
curr = (res > 0) ? 1 : (res < 0) ? -1 : 0;
if (curr === 0) {
continue;
} else if (prev && curr !== prev) {
return false;
}
prev = curr;
}
return true;
},
isArea: function() {
if (this.tags.area === 'yes')
return true;
if (!this.isClosed() || this.tags.area === 'no')
return false;
for (var key in this.tags)
if (key in iD.areaKeys && !(this.tags[key] in iD.areaKeys[key]))
return true;
return false;
},
isDegenerate: function() {
return _.uniq(this.nodes).length < (this.isArea() ? 3 : 2);
},
areAdjacent: function(n1, n2) {
for (var i = 0; i < this.nodes.length; i++) {
if (this.nodes[i] === n1) {
if (this.nodes[i - 1] === n2) return true;
if (this.nodes[i + 1] === n2) return true;
}
}
return false;
},
geometry: function(graph) {
return graph.transient(this, 'geometry', function() {
return this.isArea() ? 'area' : 'line';
});
},
addNode: function(id, index) {
var nodes = this.nodes.slice();
nodes.splice(index === undefined ? nodes.length : index, 0, id);
return this.update({nodes: nodes});
},
updateNode: function(id, index) {
var nodes = this.nodes.slice();
nodes.splice(index, 1, id);
return this.update({nodes: nodes});
},
replaceNode: function(needle, replacement) {
if (this.nodes.indexOf(needle) < 0)
return this;
var nodes = this.nodes.slice();
for (var i = 0; i < nodes.length; i++) {
if (nodes[i] === needle) {
nodes[i] = replacement;
}
}
return this.update({nodes: nodes});
},
removeNode: function(id) {
var nodes = [];
for (var i = 0; i < this.nodes.length; i++) {
var node = this.nodes[i];
if (node !== id && nodes[nodes.length - 1] !== node) {
nodes.push(node);
}
}
// Preserve circularity
if (this.nodes.length > 1 && this.first() === id && this.last() === id && nodes[nodes.length - 1] !== nodes[0]) {
nodes.push(nodes[0]);
}
return this.update({nodes: nodes});
},
asJXON: function(changeset_id) {
var r = {
way: {
'@id': this.osmId(),
'@version': this.version || 0,
nd: _.map(this.nodes, function(id) {
return { keyAttributes: { ref: Entity.id.toOSM(id) } };
}),
tag: _.map(this.tags, function(v, k) {
return { keyAttributes: { k: k, v: v } };
})
}
};
if (changeset_id) r.way['@changeset'] = changeset_id;
return r;
},
asGeoJSON: function(resolver) {
return resolver.transient(this, 'GeoJSON', function() {
var coordinates = _.map(resolver.childNodes(this), 'loc');
if (this.isArea() && this.isClosed()) {
return {
type: 'Polygon',
coordinates: [coordinates]
};
} else {
return {
type: 'LineString',
coordinates: coordinates
};
}
});
},
area: function(resolver) {
return resolver.transient(this, 'area', function() {
var nodes = resolver.childNodes(this);
var json = {
type: 'Polygon',
coordinates: [_.map(nodes, 'loc')]
};
if (!this.isClosed() && nodes.length) {
json.coordinates[0].push(nodes[0].loc);
}
var area = d3.geo.area(json);
// Heuristic for detecting counterclockwise winding order. Assumes
// that OpenStreetMap polygons are not hemisphere-spanning.
if (area > 2 * Math.PI) {
json.coordinates[0] = json.coordinates[0].reverse();
area = d3.geo.area(json);
}
return isNaN(area) ? 0 : area;
});
}
});
function Relation() {
if (!(this instanceof Relation)) {
return (new Relation()).initialize(arguments);
} else if (arguments.length) {
this.initialize(arguments);
}
}
Entity.relation = Relation;
Relation.prototype = Object.create(Entity.prototype);
Relation.creationOrder = function(a, b) {
var aId = parseInt(Entity.id.toOSM(a.id), 10);
var bId = parseInt(Entity.id.toOSM(b.id), 10);
if (aId < 0 || bId < 0) return aId - bId;
return bId - aId;
};
_.extend(Relation.prototype, {
type: 'relation',
members: [],
copy: function(resolver, copies) {
if (copies[this.id])
return copies[this.id];
var copy = Entity.prototype.copy.call(this, resolver, copies);
var members = this.members.map(function(member) {
return _.extend({}, member, {id: resolver.entity(member.id).copy(resolver, copies).id});
});
copy = copy.update({members: members});
copies[this.id] = copy;
return copy;
},
extent: function(resolver, memo) {
return resolver.transient(this, 'extent', function() {
if (memo && memo[this.id]) return Extent();
memo = memo || {};
memo[this.id] = true;
var extent = Extent();
for (var i = 0; i < this.members.length; i++) {
var member = resolver.hasEntity(this.members[i].id);
if (member) {
extent._extend(member.extent(resolver, memo));
}
}
return extent;
});
},
geometry: function(graph) {
return graph.transient(this, 'geometry', function() {
return this.isMultipolygon() ? 'area' : 'relation';
});
},
isDegenerate: function() {
return this.members.length === 0;
},
// Return an array of members, each extended with an 'index' property whose value
// is the member index.
indexedMembers: function() {
var result = new Array(this.members.length);
for (var i = 0; i < this.members.length; i++) {
result[i] = _.extend({}, this.members[i], {index: i});
}
return result;
},
// Return the first member with the given role. A copy of the member object
// is returned, extended with an 'index' property whose value is the member index.
memberByRole: function(role) {
for (var i = 0; i < this.members.length; i++) {
if (this.members[i].role === role) {
return _.extend({}, this.members[i], {index: i});
}
}
},
// Return the first member with the given id. A copy of the member object
// is returned, extended with an 'index' property whose value is the member index.
memberById: function(id) {
for (var i = 0; i < this.members.length; i++) {
if (this.members[i].id === id) {
return _.extend({}, this.members[i], {index: i});
}
}
},
// Return the first member with the given id and role. A copy of the member object
// is returned, extended with an 'index' property whose value is the member index.
memberByIdAndRole: function(id, role) {
for (var i = 0; i < this.members.length; i++) {
if (this.members[i].id === id && this.members[i].role === role) {
return _.extend({}, this.members[i], {index: i});
}
}
},
addMember: function(member, index) {
var members = this.members.slice();
members.splice(index === undefined ? members.length : index, 0, member);
return this.update({members: members});
},
updateMember: function(member, index) {
var members = this.members.slice();
members.splice(index, 1, _.extend({}, members[index], member));
return this.update({members: members});
},
removeMember: function(index) {
var members = this.members.slice();
members.splice(index, 1);
return this.update({members: members});
},
removeMembersWithID: function(id) {
var members = _.reject(this.members, function(m) { return m.id === id; });
return this.update({members: members});
},
// Wherever a member appears with id `needle.id`, replace it with a member
// with id `replacement.id`, type `replacement.type`, and the original role,
// unless a member already exists with that id and role. Return an updated
// relation.
replaceMember: function(needle, replacement) {
if (!this.memberById(needle.id))
return this;
var members = [];
for (var i = 0; i < this.members.length; i++) {
var member = this.members[i];
if (member.id !== needle.id) {
members.push(member);
} else if (!this.memberByIdAndRole(replacement.id, member.role)) {
members.push({id: replacement.id, type: replacement.type, role: member.role});
}
}
return this.update({members: members});
},
asJXON: function(changeset_id) {
var r = {
relation: {
'@id': this.osmId(),
'@version': this.version || 0,
member: _.map(this.members, function(member) {
return { keyAttributes: { type: member.type, role: member.role, ref: Entity.id.toOSM(member.id) } };
}),
tag: _.map(this.tags, function(v, k) {
return { keyAttributes: { k: k, v: v } };
})
}
};
if (changeset_id) r.relation['@changeset'] = changeset_id;
return r;
},
asGeoJSON: function(resolver) {
return resolver.transient(this, 'GeoJSON', function () {
if (this.isMultipolygon()) {
return {
type: 'MultiPolygon',
coordinates: this.multipolygon(resolver)
};
} else {
return {
type: 'FeatureCollection',
properties: this.tags,
features: this.members.map(function (member) {
return _.extend({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver));
})
};
}
});
},
area: function(resolver) {
return resolver.transient(this, 'area', function() {
return d3.geo.area(this.asGeoJSON(resolver));
});
},
isMultipolygon: function() {
return this.tags.type === 'multipolygon';
},
isComplete: function(resolver) {
for (var i = 0; i < this.members.length; i++) {
if (!resolver.hasEntity(this.members[i].id)) {
return false;
}
}
return true;
},
isRestriction: function() {
return !!(this.tags.type && this.tags.type.match(/^restriction:?/));
},
// Returns an array [A0, ... An], each Ai being an array of node arrays [Nds0, ... Ndsm],
// where Nds0 is an outer ring and subsequent Ndsi's (if any i > 0) being inner rings.
//
// This corresponds to the structure needed for rendering a multipolygon path using a
// `evenodd` fill rule, as well as the structure of a GeoJSON MultiPolygon geometry.
//
// In the case of invalid geometries, this function will still return a result which
// includes the nodes of all way members, but some Nds may be unclosed and some inner
// rings not matched with the intended outer ring.
//
multipolygon: function(resolver) {
var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); }),
inners = this.members.filter(function(m) { return 'inner' === m.role; });
outers = joinWays(outers, resolver);
inners = joinWays(inners, resolver);
outers = outers.map(function(outer) { return _.map(outer.nodes, 'loc'); });
inners = inners.map(function(inner) { return _.map(inner.nodes, 'loc'); });
var result = outers.map(function(o) {
// Heuristic for detecting counterclockwise winding order. Assumes
// that OpenStreetMap polygons are not hemisphere-spanning.
return [d3.geo.area({type: 'Polygon', coordinates: [o]}) > 2 * Math.PI ? o.reverse() : o];
});
function findOuter(inner) {
var o, outer;
for (o = 0; o < outers.length; o++) {
outer = outers[o];
if (polygonContainsPolygon(outer, inner))
return o;
}
for (o = 0; o < outers.length; o++) {
outer = outers[o];
if (polygonIntersectsPolygon(outer, inner))
return o;
}
}
for (var i = 0; i < inners.length; i++) {
var inner = inners[i];
if (d3.geo.area({type: 'Polygon', coordinates: [inner]}) < 2 * Math.PI) {
inner = inner.reverse();
}
var o = findOuter(inners[i]);
if (o !== undefined)
result[o].push(inners[i]);
else
result.push([inners[i]]); // Invalid geometry
}
return result;
}
});
function Node() {
if (!(this instanceof Node)) {
return (new Node()).initialize(arguments);
} else if (arguments.length) {
this.initialize(arguments);
}
}
Entity.node = Node;
Node.prototype = Object.create(Entity.prototype);
_.extend(Node.prototype, {
type: 'node',
extent: function() {
return new Extent(this.loc);
},
geometry: function(graph) {
return graph.transient(this, 'geometry', function() {
return graph.isPoi(this) ? 'point' : 'vertex';
});
},
move: function(loc) {
return this.update({loc: loc});
},
isIntersection: function(resolver) {
return resolver.transient(this, 'isIntersection', function() {
return resolver.parentWays(this).filter(function(parent) {
return (parent.tags.highway ||
parent.tags.waterway ||
parent.tags.railway ||
parent.tags.aeroway) &&
parent.geometry(resolver) === 'line';
}).length > 1;
});
},
isHighwayIntersection: function(resolver) {
return resolver.transient(this, 'isHighwayIntersection', function() {
return resolver.parentWays(this).filter(function(parent) {
return parent.tags.highway && parent.geometry(resolver) === 'line';
}).length > 1;
});
},
asJXON: function(changeset_id) {
var r = {
node: {
'@id': this.osmId(),
'@lon': this.loc[0],
'@lat': this.loc[1],
'@version': (this.version || 0),
tag: _.map(this.tags, function(v, k) {
return { keyAttributes: { k: k, v: v } };
})
}
};
if (changeset_id) r.node['@changeset'] = changeset_id;
return r;
},
asGeoJSON: function() {
return {
type: 'Point',
coordinates: this.loc
};
}
});
function Connection(useHttps) {
if (typeof useHttps !== 'boolean') {
useHttps = window.location.protocol === 'https:';
}
var event = d3.dispatch('authenticating', 'authenticated', 'auth', 'loading', 'loaded'),
protocol = useHttps ? 'https:' : 'http:',
url = protocol + '//www.openstreetmap.org',
connection = {},
inflight = {},
loadedTiles = {},
tileZoom = 16,
oauth = osmAuth({
url: protocol + '//www.openstreetmap.org',
oauth_consumer_key: '5A043yRSEugj4DJ5TljuapfnrflWDte8jTOcWLlT',
oauth_secret: 'aB3jKq1TRsCOUrfOIZ6oQMEDmv2ptV76PA54NGLL',
loading: authenticating,
done: authenticated
}),
ndStr = 'nd',
tagStr = 'tag',
memberStr = 'member',
nodeStr = 'node',
wayStr = 'way',
relationStr = 'relation',
userDetails,
off;
connection.changesetURL = function(changesetId) {
return url + '/changeset/' + changesetId;
};
connection.changesetsURL = function(center, zoom) {
var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
return url + '/history#map=' +
Math.floor(zoom) + '/' +
center[1].toFixed(precision) + '/' +
center[0].toFixed(precision);
};
connection.entityURL = function(entity) {
return url + '/' + entity.type + '/' + entity.osmId();
};
connection.userURL = function(username) {
return url + '/user/' + username;
};
connection.loadFromURL = function(url, callback) {
function done(err, dom) {
return callback(err, parse(dom));
}
return d3.xml(url).get(done);
};
connection.loadEntity = function(id, callback) {
var type = Entity.id.type(id),
osmID = Entity.id.toOSM(id);
connection.loadFromURL(
url + '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : ''),
function(err, entities) {
if (callback) callback(err, {data: entities});
});
};
connection.loadEntityVersion = function(id, version, callback) {
var type = Entity.id.type(id),
osmID = Entity.id.toOSM(id);
connection.loadFromURL(
url + '/api/0.6/' + type + '/' + osmID + '/' + version,
function(err, entities) {
if (callback) callback(err, {data: entities});
});
};
connection.loadMultiple = function(ids, callback) {
_.each(_.groupBy(_.uniq(ids), Entity.id.type), function(v, k) {
var type = k + 's',
osmIDs = _.map(v, Entity.id.toOSM);
_.each(_.chunk(osmIDs, 150), function(arr) {
connection.loadFromURL(
url + '/api/0.6/' + type + '?' + type + '=' + arr.join(),
function(err, entities) {
if (callback) callback(err, {data: entities});
});
});
});
};
function authenticating() {
event.authenticating();
}
function authenticated() {
event.authenticated();
}
function getLoc(attrs) {
var lon = attrs.lon && attrs.lon.value,
lat = attrs.lat && attrs.lat.value;
return [parseFloat(lon), parseFloat(lat)];
}
function getNodes(obj) {
var elems = obj.getElementsByTagName(ndStr),
nodes = new Array(elems.length);
for (var i = 0, l = elems.length; i < l; i++) {
nodes[i] = 'n' + elems[i].attributes.ref.value;
}
return nodes;
}
function getTags(obj) {
var elems = obj.getElementsByTagName(tagStr),
tags = {};
for (var i = 0, l = elems.length; i < l; i++) {
var attrs = elems[i].attributes;
tags[attrs.k.value] = attrs.v.value;
}
return tags;
}
function getMembers(obj) {
var elems = obj.getElementsByTagName(memberStr),
members = new Array(elems.length);
for (var i = 0, l = elems.length; i < l; i++) {
var attrs = elems[i].attributes;
members[i] = {
id: attrs.type.value[0] + attrs.ref.value,
type: attrs.type.value,
role: attrs.role.value
};
}
return members;
}
function getVisible(attrs) {
return (!attrs.visible || attrs.visible.value !== 'false');
}
var parsers = {
node: function nodeData(obj) {
var attrs = obj.attributes;
return new Node({
id: Entity.id.fromOSM(nodeStr, attrs.id.value),
loc: getLoc(attrs),
version: attrs.version.value,
user: attrs.user && attrs.user.value,
tags: getTags(obj),
visible: getVisible(attrs)
});
},
way: function wayData(obj) {
var attrs = obj.attributes;
return new Way({
id: Entity.id.fromOSM(wayStr, attrs.id.value),
version: attrs.version.value,
user: attrs.user && attrs.user.value,
tags: getTags(obj),
nodes: getNodes(obj),
visible: getVisible(attrs)
});
},
relation: function relationData(obj) {
var attrs = obj.attributes;
return new Relation({
id: Entity.id.fromOSM(relationStr, attrs.id.value),
version: attrs.version.value,
user: attrs.user && attrs.user.value,
tags: getTags(obj),
members: getMembers(obj),
visible: getVisible(attrs)
});
}
};
function parse(dom) {
if (!dom || !dom.childNodes) return;
var root = dom.childNodes[0],
children = root.childNodes,
entities = [];
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i],
parser = parsers[child.nodeName];
if (parser) {
entities.push(parser(child));
}
}
return entities;
}
connection.authenticated = function() {
return oauth.authenticated();
};
// Generate Changeset XML. Returns a string.
connection.changesetJXON = function(tags) {
return {
osm: {
changeset: {
tag: _.map(tags, function(value, key) {
return { '@k': key, '@v': value };
}),
'@version': 0.6,
'@generator': 'iD'
}
}
};
};
// Generate [osmChange](http://wiki.openstreetmap.org/wiki/OsmChange)
// XML. Returns a string.
connection.osmChangeJXON = function(changeset_id, changes) {
function nest(x, order) {
var groups = {};
for (var i = 0; i < x.length; i++) {
var tagName = Object.keys(x[i])[0];
if (!groups[tagName]) groups[tagName] = [];
groups[tagName].push(x[i][tagName]);
}
var ordered = {};
order.forEach(function(o) {
if (groups[o]) ordered[o] = groups[o];
});
return ordered;
}
function rep(entity) {
return entity.asJXON(changeset_id);
}
return {
osmChange: {
'@version': 0.6,
'@generator': 'iD',
'create': nest(changes.created.map(rep), ['node', 'way', 'relation']),
'modify': nest(changes.modified.map(rep), ['node', 'way', 'relation']),
'delete': _.extend(nest(changes.deleted.map(rep), ['relation', 'way', 'node']), {'@if-unused': true})
}
};
};
connection.changesetTags = function(comment, imageryUsed) {
var detected = iD.detect(),
tags = {
created_by: 'iD ' + iD.version,
imagery_used: imageryUsed.join(';').substr(0, 255),
host: (window.location.origin + window.location.pathname).substr(0, 255),
locale: detected.locale
};
if (comment) {
tags.comment = comment.substr(0, 255);
}
return tags;
};
connection.putChangeset = function(changes, comment, imageryUsed, callback) {
oauth.xhr({
method: 'PUT',
path: '/api/0.6/changeset/create',
options: { header: { 'Content-Type': 'text/xml' } },
content: JXON.stringify(connection.changesetJXON(connection.changesetTags(comment, imageryUsed)))
}, function(err, changeset_id) {
if (err) return callback(err);
oauth.xhr({
method: 'POST',
path: '/api/0.6/changeset/' + changeset_id + '/upload',
options: { header: { 'Content-Type': 'text/xml' } },
content: JXON.stringify(connection.osmChangeJXON(changeset_id, changes))
}, function(err) {
if (err) return callback(err);
// POST was successful, safe to call the callback.
// Still attempt to close changeset, but ignore response because #2667
// Add delay to allow for postgres replication #1646 #2678
window.setTimeout(function() { callback(null, changeset_id); }, 2500);
oauth.xhr({
method: 'PUT',
path: '/api/0.6/changeset/' + changeset_id + '/close',
options: { header: { 'Content-Type': 'text/xml' } }
}, d3.functor(true));
});
});
};
connection.userDetails = function(callback) {
if (userDetails) {
callback(undefined, userDetails);
return;
}
function done(err, user_details) {
if (err) return callback(err);
var u = user_details.getElementsByTagName('user')[0],
img = u.getElementsByTagName('img'),
image_url = '';
if (img && img[0] && img[0].getAttribute('href')) {
image_url = img[0].getAttribute('href');
}
userDetails = {
display_name: u.attributes.display_name.value,
image_url: image_url,
id: u.attributes.id.value
};
callback(undefined, userDetails);
}
oauth.xhr({ method: 'GET', path: '/api/0.6/user/details' }, done);
};
connection.userChangesets = function(callback) {
connection.userDetails(function(err, user) {
if (err) return callback(err);
function done(changesets) {
callback(undefined, Array.prototype.map.call(changesets.getElementsByTagName('changeset'),
function (changeset) {
return { tags: getTags(changeset) };
}));
}
d3.xml(url + '/api/0.6/changesets?user=' + user.id).get()
.on('load', done)
.on('error', callback);
});
};
connection.status = function(callback) {
function done(capabilities) {
var apiStatus = capabilities.getElementsByTagName('status');
callback(undefined, apiStatus[0].getAttribute('api'));
}
d3.xml(url + '/api/capabilities').get()
.on('load', done)
.on('error', callback);
};
function abortRequest(i) { i.abort(); }
connection.tileZoom = function(_) {
if (!arguments.length) return tileZoom;
tileZoom = _;
return connection;
};
connection.loadTiles = function(projection, dimensions, callback) {
if (off) return;
var s = projection.scale() * 2 * Math.PI,
z = Math.max(Math.log(s) / Math.log(2) - 8, 0),
ts = 256 * Math.pow(2, z - tileZoom),
origin = [
s / 2 - projection.translate()[0],
s / 2 - projection.translate()[1]];
var tiles = d3.geo.tile()
.scaleExtent([tileZoom, tileZoom])
.scale(s)
.size(dimensions)
.translate(projection.translate())()
.map(function(tile) {
var x = tile[0] * ts - origin[0],
y = tile[1] * ts - origin[1];
return {
id: tile.toString(),
extent: Extent(
projection.invert([x, y + ts]),
projection.invert([x + ts, y]))
};
});
function bboxUrl(tile) {
return url + '/api/0.6/map?bbox=' + tile.extent.toParam();
}
_.filter(inflight, function(v, i) {
var wanted = _.find(tiles, function(tile) {
return i === tile.id;
});
if (!wanted) delete inflight[i];
return !wanted;
}).map(abortRequest);
tiles.forEach(function(tile) {
var id = tile.id;
if (loadedTiles[id] || inflight[id]) return;
if (_.isEmpty(inflight)) {
event.loading();
}
inflight[id] = connection.loadFromURL(bboxUrl(tile), function(err, parsed) {
loadedTiles[id] = true;
delete inflight[id];
if (callback) callback(err, _.extend({data: parsed}, tile));
if (_.isEmpty(inflight)) {
event.loaded();
}
});
});
};
connection.switch = function(options) {
url = options.url;
oauth.options(_.extend({
loading: authenticating,
done: authenticated
}, options));
event.auth();
connection.flush();
return connection;
};
connection.toggle = function(_) {
off = !_;
return connection;
};
connection.flush = function() {
userDetails = undefined;
_.forEach(inflight, abortRequest);
loadedTiles = {};
inflight = {};
return connection;
};
connection.loadedTiles = function(_) {
if (!arguments.length) return loadedTiles;
loadedTiles = _;
return connection;
};
connection.logout = function() {
userDetails = undefined;
oauth.logout();
event.auth();
return connection;
};
connection.authenticate = function(callback) {
userDetails = undefined;
function done(err, res) {
event.auth();
if (callback) callback(err, res);
}
return oauth.authenticate(done);
};
return d3.rebind(connection, event, 'on');
}
/*
iD.Difference represents the difference between two graphs.
It knows how to calculate the set of entities that were
created, modified, or deleted, and also contains the logic
for recursively extending a difference to the complete set
of entities that will require a redraw, taking into account
child and parent relationships.
*/
function Difference(base, head) {
var changes = {}, length = 0;
function changed(h, b) {
return h !== b && !_.isEqual(_.omit(h, 'v'), _.omit(b, 'v'));
}
_.each(head.entities, function(h, id) {
var b = base.entities[id];
if (changed(h, b)) {
changes[id] = {base: b, head: h};
length++;
}
});
_.each(base.entities, function(b, id) {
var h = head.entities[id];
if (!changes[id] && changed(h, b)) {
changes[id] = {base: b, head: h};
length++;
}
});
function addParents(parents, result) {
for (var i = 0; i < parents.length; i++) {
var parent = parents[i];
if (parent.id in result)
continue;
result[parent.id] = parent;
addParents(head.parentRelations(parent), result);
}
}
var difference = {};
difference.length = function() {
return length;
};
difference.changes = function() {
return changes;
};
difference.extantIDs = function() {
var result = [];
_.each(changes, function(change, id) {
if (change.head) result.push(id);
});
return result;
};
difference.modified = function() {
var result = [];
_.each(changes, function(change) {
if (change.base && change.head) result.push(change.head);
});
return result;
};
difference.created = function() {
var result = [];
_.each(changes, function(change) {
if (!change.base && change.head) result.push(change.head);
});
return result;
};
difference.deleted = function() {
var result = [];
_.each(changes, function(change) {
if (change.base && !change.head) result.push(change.base);
});
return result;
};
difference.summary = function() {
var relevant = {};
function addEntity(entity, graph, changeType) {
relevant[entity.id] = {
entity: entity,
graph: graph,
changeType: changeType
};
}
function addParents(entity) {
var parents = head.parentWays(entity);
for (var j = parents.length - 1; j >= 0; j--) {
var parent = parents[j];
if (!(parent.id in relevant)) addEntity(parent, head, 'modified');
}
}
_.each(changes, function(change) {
if (change.head && change.head.geometry(head) !== 'vertex') {
addEntity(change.head, head, change.base ? 'modified' : 'created');
} else if (change.base && change.base.geometry(base) !== 'vertex') {
addEntity(change.base, base, 'deleted');
} else if (change.base && change.head) { // modified vertex
var moved = !_.isEqual(change.base.loc, change.head.loc),
retagged = !_.isEqual(change.base.tags, change.head.tags);
if (moved) {
addParents(change.head);
}
if (retagged || (moved && change.head.hasInterestingTags())) {
addEntity(change.head, head, 'modified');
}
} else if (change.head && change.head.hasInterestingTags()) { // created vertex
addEntity(change.head, head, 'created');
} else if (change.base && change.base.hasInterestingTags()) { // deleted vertex
addEntity(change.base, base, 'deleted');
}
});
return d3.values(relevant);
};
difference.complete = function(extent) {
var result = {}, id, change;
for (id in changes) {
change = changes[id];
var h = change.head,
b = change.base,
entity = h || b;
if (extent &&
(!h || !h.intersects(extent, head)) &&
(!b || !b.intersects(extent, base)))
continue;
result[id] = h;
if (entity.type === 'way') {
var nh = h ? h.nodes : [],
nb = b ? b.nodes : [],
diff, i;
diff = _.difference(nh, nb);
for (i = 0; i < diff.length; i++) {
result[diff[i]] = head.hasEntity(diff[i]);
}
diff = _.difference(nb, nh);
for (i = 0; i < diff.length; i++) {
result[diff[i]] = head.hasEntity(diff[i]);
}
}
addParents(head.parentWays(entity), result);
addParents(head.parentRelations(entity), result);
}
return result;
};
return difference;
}
function tagText(entity) {
return d3.entries(entity.tags).map(function(e) {
return e.key + '=' + e.value;
}).join(', ');
}
function entitySelector(ids) {
return ids.length ? '.' + ids.join(',.') : 'nothing';
}
function entityOrMemberSelector(ids, graph) {
var s = entitySelector(ids);
ids.forEach(function(id) {
var entity = graph.hasEntity(id);
if (entity && entity.type === 'relation') {
entity.members.forEach(function(member) {
s += ',.' + member.id;
});
}
});
return s;
}
function displayName(entity) {
var localeName = 'name:' + iD.detect().locale.toLowerCase().split('-')[0];
return entity.tags[localeName] || entity.tags.name || entity.tags.ref;
}
function displayType(id) {
return {
n: t('inspector.node'),
w: t('inspector.way'),
r: t('inspector.relation')
}[id.charAt(0)];
}
function stringQs(str) {
return str.split('&').reduce(function(obj, pair){
var parts = pair.split('=');
if (parts.length === 2) {
obj[parts[0]] = (null === parts[1]) ? '' : decodeURIComponent(parts[1]);
}
return obj;
}, {});
}
function qsString(obj, noencode) {
function softEncode(s) {
// encode everything except special characters used in certain hash parameters:
// "/" in map states, ":", ",", {" and "}" in background
return encodeURIComponent(s).replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent);
}
return Object.keys(obj).sort().map(function(key) {
return encodeURIComponent(key) + '=' + (
noencode ? softEncode(obj[key]) : encodeURIComponent(obj[key]));
}).join('&');
}
function prefixDOMProperty(property) {
var prefixes = ['webkit', 'ms', 'moz', 'o'],
i = -1,
n = prefixes.length,
s = document.body;
if (property in s)
return property;
property = property.substr(0, 1).toUpperCase() + property.substr(1);
while (++i < n)
if (prefixes[i] + property in s)
return prefixes[i] + property;
return false;
}
function prefixCSSProperty(property) {
var prefixes = ['webkit', 'ms', 'Moz', 'O'],
i = -1,
n = prefixes.length,
s = document.body.style;
if (property.toLowerCase() in s)
return property.toLowerCase();
while (++i < n)
if (prefixes[i] + property in s)
return '-' + prefixes[i].toLowerCase() + property.replace(/([A-Z])/g, '-$1').toLowerCase();
return false;
}
var transformProperty;
function setTransform(el, x, y, scale) {
var prop = transformProperty = transformProperty || prefixCSSProperty('Transform'),
translate = iD.detect().opera ?
'translate(' + x + 'px,' + y + 'px)' :
'translate3d(' + x + 'px,' + y + 'px,0)';
return el.style(prop, translate + (scale ? ' scale(' + scale + ')' : ''));
}
function getStyle(selector) {
for (var i = 0; i < document.styleSheets.length; i++) {
var rules = document.styleSheets[i].rules || document.styleSheets[i].cssRules || [];
for (var k = 0; k < rules.length; k++) {
var selectorText = rules[k].selectorText && rules[k].selectorText.split(', ');
if (_.includes(selectorText, selector)) {
return rules[k];
}
}
}
}
function editDistance(a, b) {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
var matrix = [];
for (var i = 0; i <= b.length; i++) { matrix[i] = [i]; }
for (var j = 0; j <= a.length; j++) { matrix[0][j] = j; }
for (i = 1; i <= b.length; i++) {
for (j = 1; j <= a.length; j++) {
if (b.charAt(i-1) === a.charAt(j-1)) {
matrix[i][j] = matrix[i-1][j-1];
} else {
matrix[i][j] = Math.min(matrix[i-1][j-1] + 1, // substitution
Math.min(matrix[i][j-1] + 1, // insertion
matrix[i-1][j] + 1)); // deletion
}
}
}
return matrix[b.length][a.length];
}
// a d3.mouse-alike which
// 1. Only works on HTML elements, not SVG
// 2. Does not cause style recalculation
function fastMouse(container) {
var rect = container.getBoundingClientRect(),
rectLeft = rect.left,
rectTop = rect.top,
clientLeft = +container.clientLeft,
clientTop = +container.clientTop;
return function(e) {
return [
e.clientX - rectLeft - clientLeft,
e.clientY - rectTop - clientTop];
};
}
/* eslint-disable no-proto */
var getPrototypeOf = Object.getPrototypeOf || function(obj) { return obj.__proto__; };
/* eslint-enable no-proto */
function asyncMap(inputs, func, callback) {
var remaining = inputs.length,
results = [],
errors = [];
inputs.forEach(function(d, i) {
func(d, function done(err, data) {
errors[i] = err;
results[i] = data;
remaining--;
if (!remaining) callback(errors, results);
});
});
}
// wraps an index to an interval [0..length-1]
function Wrap(index, length) {
if (index < 0)
index += Math.ceil(-index/length)*length;
return index % length;
}
// A per-domain session mutex backed by a cookie and dead man's
// switch. If the session crashes, the mutex will auto-release
// after 5 seconds.
function SessionMutex(name) {
var mutex = {},
intervalID;
function renew() {
var expires = new Date();
expires.setSeconds(expires.getSeconds() + 5);
document.cookie = name + '=1; expires=' + expires.toUTCString();
}
mutex.lock = function() {
if (intervalID) return true;
var cookie = document.cookie.replace(new RegExp('(?:(?:^|.*;)\\s*' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1');
if (cookie) return false;
renew();
intervalID = window.setInterval(renew, 4000);
return true;
};
mutex.unlock = function() {
if (!intervalID) return;
document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
clearInterval(intervalID);
intervalID = null;
};
mutex.locked = function() {
return !!intervalID;
};
return mutex;
}
function SuggestNames(preset, suggestions) {
preset = preset.id.split('/', 2);
var k = preset[0],
v = preset[1];
return function(value, callback) {
var result = [];
if (value && value.length > 2) {
if (suggestions[k] && suggestions[k][v]) {
for (var sugg in suggestions[k][v]) {
var dist = editDistance(value, sugg.substring(0, value.length));
if (dist < 3) {
result.push({
title: sugg,
value: sugg,
dist: dist
});
}
}
}
result.sort(function(a, b) {
return a.dist - b.dist;
});
}
result = result.slice(0,3);
callback(result);
};
}
var util = Object.freeze({
tagText: tagText,
entitySelector: entitySelector,
entityOrMemberSelector: entityOrMemberSelector,
displayName: displayName,
displayType: displayType,
stringQs: stringQs,
qsString: qsString,
prefixDOMProperty: prefixDOMProperty,
prefixCSSProperty: prefixCSSProperty,
setTransform: setTransform,
getStyle: getStyle,
editDistance: editDistance,
fastMouse: fastMouse,
getPrototypeOf: getPrototypeOf,
asyncMap: asyncMap,
wrap: Wrap,
SessionMutex: SessionMutex,
SuggestNames: SuggestNames
});
function Graph(other, mutable) {
if (!(this instanceof Graph)) return new Graph(other, mutable);
if (other instanceof Graph) {
var base = other.base();
this.entities = _.assign(Object.create(base.entities), other.entities);
this._parentWays = _.assign(Object.create(base.parentWays), other._parentWays);
this._parentRels = _.assign(Object.create(base.parentRels), other._parentRels);
} else {
this.entities = Object.create({});
this._parentWays = Object.create({});
this._parentRels = Object.create({});
this.rebase(other || [], [this]);
}
this.transients = {};
this._childNodes = {};
this.frozen = !mutable;
}
Graph.prototype = {
hasEntity: function(id) {
return this.entities[id];
},
entity: function(id) {
var entity = this.entities[id];
if (!entity) {
throw new Error('entity ' + id + ' not found');
}
return entity;
},
transient: function(entity, key, fn) {
var id = entity.id,
transients = this.transients[id] ||
(this.transients[id] = {});
if (transients[key] !== undefined) {
return transients[key];
}
transients[key] = fn.call(entity);
return transients[key];
},
parentWays: function(entity) {
var parents = this._parentWays[entity.id],
result = [];
if (parents) {
for (var i = 0; i < parents.length; i++) {
result.push(this.entity(parents[i]));
}
}
return result;
},
isPoi: function(entity) {
var parentWays = this._parentWays[entity.id];
return !parentWays || parentWays.length === 0;
},
isShared: function(entity) {
var parentWays = this._parentWays[entity.id];
return parentWays && parentWays.length > 1;
},
parentRelations: function(entity) {
var parents = this._parentRels[entity.id],
result = [];
if (parents) {
for (var i = 0; i < parents.length; i++) {
result.push(this.entity(parents[i]));
}
}
return result;
},
childNodes: function(entity) {
if (this._childNodes[entity.id]) return this._childNodes[entity.id];
if (!entity.nodes) return [];
var nodes = [];
for (var i = 0; i < entity.nodes.length; i++) {
nodes[i] = this.entity(entity.nodes[i]);
}
if (iD.debug) Object.freeze(nodes);
this._childNodes[entity.id] = nodes;
return this._childNodes[entity.id];
},
base: function() {
return {
'entities': getPrototypeOf(this.entities),
'parentWays': getPrototypeOf(this._parentWays),
'parentRels': getPrototypeOf(this._parentRels)
};
},
// Unlike other graph methods, rebase mutates in place. This is because it
// is used only during the history operation that merges newly downloaded
// data into each state. To external consumers, it should appear as if the
// graph always contained the newly downloaded data.
rebase: function(entities, stack, force) {
var base = this.base(),
i, j, k, id;
for (i = 0; i < entities.length; i++) {
var entity = entities[i];
if (!entity.visible || (!force && base.entities[entity.id]))
continue;
// Merging data into the base graph
base.entities[entity.id] = entity;
this._updateCalculated(undefined, entity, base.parentWays, base.parentRels);
// Restore provisionally-deleted nodes that are discovered to have an extant parent
if (entity.type === 'way') {
for (j = 0; j < entity.nodes.length; j++) {
id = entity.nodes[j];
for (k = 1; k < stack.length; k++) {
var ents = stack[k].entities;
if (ents.hasOwnProperty(id) && ents[id] === undefined) {
delete ents[id];
}
}
}
}
}
for (i = 0; i < stack.length; i++) {
stack[i]._updateRebased();
}
},
_updateRebased: function() {
var base = this.base(),
i, k, child, id, keys;
keys = Object.keys(this._parentWays);
for (i = 0; i < keys.length; i++) {
child = keys[i];
if (base.parentWays[child]) {
for (k = 0; k < base.parentWays[child].length; k++) {
id = base.parentWays[child][k];
if (!this.entities.hasOwnProperty(id) && !_.includes(this._parentWays[child], id)) {
this._parentWays[child].push(id);
}
}
}
}
keys = Object.keys(this._parentRels);
for (i = 0; i < keys.length; i++) {
child = keys[i];
if (base.parentRels[child]) {
for (k = 0; k < base.parentRels[child].length; k++) {
id = base.parentRels[child][k];
if (!this.entities.hasOwnProperty(id) && !_.includes(this._parentRels[child], id)) {
this._parentRels[child].push(id);
}
}
}
}
this.transients = {};
// this._childNodes is not updated, under the assumption that
// ways are always downloaded with their child nodes.
},
// Updates calculated properties (parentWays, parentRels) for the specified change
_updateCalculated: function(oldentity, entity, parentWays, parentRels) {
parentWays = parentWays || this._parentWays;
parentRels = parentRels || this._parentRels;
var type = entity && entity.type || oldentity && oldentity.type,
removed, added, ways, rels, i;
if (type === 'way') {
// Update parentWays
if (oldentity && entity) {
removed = _.difference(oldentity.nodes, entity.nodes);
added = _.difference(entity.nodes, oldentity.nodes);
} else if (oldentity) {
removed = oldentity.nodes;
added = [];
} else if (entity) {
removed = [];
added = entity.nodes;
}
for (i = 0; i < removed.length; i++) {
parentWays[removed[i]] = _.without(parentWays[removed[i]], oldentity.id);
}
for (i = 0; i < added.length; i++) {
ways = _.without(parentWays[added[i]], entity.id);
ways.push(entity.id);
parentWays[added[i]] = ways;
}
} else if (type === 'relation') {
// Update parentRels
if (oldentity && entity) {
removed = _.difference(oldentity.members, entity.members);
added = _.difference(entity.members, oldentity);
} else if (oldentity) {
removed = oldentity.members;
added = [];
} else if (entity) {
removed = [];
added = entity.members;
}
for (i = 0; i < removed.length; i++) {
parentRels[removed[i].id] = _.without(parentRels[removed[i].id], oldentity.id);
}
for (i = 0; i < added.length; i++) {
rels = _.without(parentRels[added[i].id], entity.id);
rels.push(entity.id);
parentRels[added[i].id] = rels;
}
}
},
replace: function(entity) {
if (this.entities[entity.id] === entity)
return this;
return this.update(function() {
this._updateCalculated(this.entities[entity.id], entity);
this.entities[entity.id] = entity;
});
},
remove: function(entity) {
return this.update(function() {
this._updateCalculated(entity, undefined);
this.entities[entity.id] = undefined;
});
},
revert: function(id) {
var baseEntity = this.base().entities[id],
headEntity = this.entities[id];
if (headEntity === baseEntity)
return this;
return this.update(function() {
this._updateCalculated(headEntity, baseEntity);
delete this.entities[id];
});
},
update: function() {
var graph = this.frozen ? Graph(this, true) : this;
for (var i = 0; i < arguments.length; i++) {
arguments[i].call(graph, graph);
}
if (this.frozen) graph.frozen = true;
return graph;
},
// Obliterates any existing entities
load: function(entities) {
var base = this.base();
this.entities = Object.create(base.entities);
for (var i in entities) {
this.entities[i] = entities[i];
this._updateCalculated(base.entities[i], this.entities[i]);
}
return this;
}
};
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
var rbush = createCommonjsModule(function (module) {
/*
(c) 2015, Vladimir Agafonkin
RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles.
https://github.com/mourner/rbush
*/
(function () {
'use strict';
function rbush(maxEntries, format) {
if (!(this instanceof rbush)) return new rbush(maxEntries, format);
// max entries in a node is 9 by default; min node fill is 40% for best performance
this._maxEntries = Math.max(4, maxEntries || 9);
this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
if (format) {
this._initFormat(format);
}
this.clear();
}
rbush.prototype = {
all: function () {
return this._all(this.data, []);
},
search: function (bbox) {
var node = this.data,
result = [],
toBBox = this.toBBox;
if (!intersects(bbox, node.bbox)) return result;
var nodesToSearch = [],
i, len, child, childBBox;
while (node) {
for (i = 0, len = node.children.length; i < len; i++) {
child = node.children[i];
childBBox = node.leaf ? toBBox(child) : child.bbox;
if (intersects(bbox, childBBox)) {
if (node.leaf) result.push(child);
else if (contains(bbox, childBBox)) this._all(child, result);
else nodesToSearch.push(child);
}
}
node = nodesToSearch.pop();
}
return result;
},
collides: function (bbox) {
var node = this.data,
toBBox = this.toBBox;
if (!intersects(bbox, node.bbox)) return false;
var nodesToSearch = [],
i, len, child, childBBox;
while (node) {
for (i = 0, len = node.children.length; i < len; i++) {
child = node.children[i];
childBBox = node.leaf ? toBBox(child) : child.bbox;
if (intersects(bbox, childBBox)) {
if (node.leaf || contains(bbox, childBBox)) return true;
nodesToSearch.push(child);
}
}
node = nodesToSearch.pop();
}
return false;
},
load: function (data) {
if (!(data && data.length)) return this;
if (data.length < this._minEntries) {
for (var i = 0, len = data.length; i < len; i++) {
this.insert(data[i]);
}
return this;
}
// recursively build the tree with the given data from stratch using OMT algorithm
var node = this._build(data.slice(), 0, data.length - 1, 0);
if (!this.data.children.length) {
// save as is if tree is empty
this.data = node;
} else if (this.data.height === node.height) {
// split root if trees have the same height
this._splitRoot(this.data, node);
} else {
if (this.data.height < node.height) {
// swap trees if inserted one is bigger
var tmpNode = this.data;
this.data = node;
node = tmpNode;
}
// insert the small tree into the large tree at appropriate level
this._insert(node, this.data.height - node.height - 1, true);
}
return this;
},
insert: function (item) {
if (item) this._insert(item, this.data.height - 1);
return this;
},
clear: function () {
this.data = {
children: [],
height: 1,
bbox: empty(),
leaf: true
};
return this;
},
remove: function (item) {
if (!item) return this;
var node = this.data,
bbox = this.toBBox(item),
path = [],
indexes = [],
i, parent, index, goingUp;
// depth-first iterative tree traversal
while (node || path.length) {
if (!node) { // go up
node = path.pop();
parent = path[path.length - 1];
i = indexes.pop();
goingUp = true;
}
if (node.leaf) { // check current node
index = node.children.indexOf(item);
if (index !== -1) {
// item found, remove the item and condense tree upwards
node.children.splice(index, 1);
path.push(node);
this._condense(path);
return this;
}
}
if (!goingUp && !node.leaf && contains(node.bbox, bbox)) { // go down
path.push(node);
indexes.push(i);
i = 0;
parent = node;
node = node.children[0];
} else if (parent) { // go right
i++;
node = parent.children[i];
goingUp = false;
} else node = null; // nothing found
}
return this;
},
toBBox: function (item) { return item; },
compareMinX: function (a, b) { return a[0] - b[0]; },
compareMinY: function (a, b) { return a[1] - b[1]; },
toJSON: function () { return this.data; },
fromJSON: function (data) {
this.data = data;
return this;
},
_all: function (node, result) {
var nodesToSearch = [];
while (node) {
if (node.leaf) result.push.apply(result, node.children);
else nodesToSearch.push.apply(nodesToSearch, node.children);
node = nodesToSearch.pop();
}
return result;
},
_build: function (items, left, right, height) {
var N = right - left + 1,
M = this._maxEntries,
node;
if (N <= M) {
// reached leaf level; return leaf
node = {
children: items.slice(left, right + 1),
height: 1,
bbox: null,
leaf: true
};
calcBBox(node, this.toBBox);
return node;
}
if (!height) {
// target height of the bulk-loaded tree
height = Math.ceil(Math.log(N) / Math.log(M));
// target number of root entries to maximize storage utilization
M = Math.ceil(N / Math.pow(M, height - 1));
}
node = {
children: [],
height: height,
bbox: null,
leaf: false
};
// split the items into M mostly square tiles
var N2 = Math.ceil(N / M),
N1 = N2 * Math.ceil(Math.sqrt(M)),
i, j, right2, right3;
multiSelect(items, left, right, N1, this.compareMinX);
for (i = left; i <= right; i += N1) {
right2 = Math.min(i + N1 - 1, right);
multiSelect(items, i, right2, N2, this.compareMinY);
for (j = i; j <= right2; j += N2) {
right3 = Math.min(j + N2 - 1, right2);
// pack each entry recursively
node.children.push(this._build(items, j, right3, height - 1));
}
}
calcBBox(node, this.toBBox);
return node;
},
_chooseSubtree: function (bbox, node, level, path) {
var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;
while (true) {
path.push(node);
if (node.leaf || path.length - 1 === level) break;
minArea = minEnlargement = Infinity;
for (i = 0, len = node.children.length; i < len; i++) {
child = node.children[i];
area = bboxArea(child.bbox);
enlargement = enlargedArea(bbox, child.bbox) - area;
// choose entry with the least area enlargement
if (enlargement < minEnlargement) {
minEnlargement = enlargement;
minArea = area < minArea ? area : minArea;
targetNode = child;
} else if (enlargement === minEnlargement) {
// otherwise choose one with the smallest area
if (area < minArea) {
minArea = area;
targetNode = child;
}
}
}
node = targetNode || node.children[0];
}
return node;
},
_insert: function (item, level, isNode) {
var toBBox = this.toBBox,
bbox = isNode ? item.bbox : toBBox(item),
insertPath = [];
// find the best node for accommodating the item, saving all nodes along the path too
var node = this._chooseSubtree(bbox, this.data, level, insertPath);
// put the item into the node
node.children.push(item);
extend(node.bbox, bbox);
// split on node overflow; propagate upwards if necessary
while (level >= 0) {
if (insertPath[level].children.length > this._maxEntries) {
this._split(insertPath, level);
level--;
} else break;
}
// adjust bboxes along the insertion path
this._adjustParentBBoxes(bbox, insertPath, level);
},
// split overflowed node into two
_split: function (insertPath, level) {
var node = insertPath[level],
M = node.children.length,
m = this._minEntries;
this._chooseSplitAxis(node, m, M);
var splitIndex = this._chooseSplitIndex(node, m, M);
var newNode = {
children: node.children.splice(splitIndex, node.children.length - splitIndex),
height: node.height,
bbox: null,
leaf: false
};
if (node.leaf) newNode.leaf = true;
calcBBox(node, this.toBBox);
calcBBox(newNode, this.toBBox);
if (level) insertPath[level - 1].children.push(newNode);
else this._splitRoot(node, newNode);
},
_splitRoot: function (node, newNode) {
// split root node
this.data = {
children: [node, newNode],
height: node.height + 1,
bbox: null,
leaf: false
};
calcBBox(this.data, this.toBBox);
},
_chooseSplitIndex: function (node, m, M) {
var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;
minOverlap = minArea = Infinity;
for (i = m; i <= M - m; i++) {
bbox1 = distBBox(node, 0, i, this.toBBox);
bbox2 = distBBox(node, i, M, this.toBBox);
overlap = intersectionArea(bbox1, bbox2);
area = bboxArea(bbox1) + bboxArea(bbox2);
// choose distribution with minimum overlap
if (overlap < minOverlap) {
minOverlap = overlap;
index = i;
minArea = area < minArea ? area : minArea;
} else if (overlap === minOverlap) {
// otherwise choose distribution with minimum area
if (area < minArea) {
minArea = area;
index = i;
}
}
}
return index;
},
// sorts node children by the best axis for split
_chooseSplitAxis: function (node, m, M) {
var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,
compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,
xMargin = this._allDistMargin(node, m, M, compareMinX),
yMargin = this._allDistMargin(node, m, M, compareMinY);
// if total distributions margin value is minimal for x, sort by minX,
// otherwise it's already sorted by minY
if (xMargin < yMargin) node.children.sort(compareMinX);
},
// total margin of all possible split distributions where each node is at least m full
_allDistMargin: function (node, m, M, compare) {
node.children.sort(compare);
var toBBox = this.toBBox,
leftBBox = distBBox(node, 0, m, toBBox),
rightBBox = distBBox(node, M - m, M, toBBox),
margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),
i, child;
for (i = m; i < M - m; i++) {
child = node.children[i];
extend(leftBBox, node.leaf ? toBBox(child) : child.bbox);
margin += bboxMargin(leftBBox);
}
for (i = M - m - 1; i >= m; i--) {
child = node.children[i];
extend(rightBBox, node.leaf ? toBBox(child) : child.bbox);
margin += bboxMargin(rightBBox);
}
return margin;
},
_adjustParentBBoxes: function (bbox, path, level) {
// adjust bboxes along the given tree path
for (var i = level; i >= 0; i--) {
extend(path[i].bbox, bbox);
}
},
_condense: function (path) {
// go through the path, removing empty nodes and updating bboxes
for (var i = path.length - 1, siblings; i >= 0; i--) {
if (path[i].children.length === 0) {
if (i > 0) {
siblings = path[i - 1].children;
siblings.splice(siblings.indexOf(path[i]), 1);
} else this.clear();
} else calcBBox(path[i], this.toBBox);
}
},
_initFormat: function (format) {
// data format (minX, minY, maxX, maxY accessors)
// uses eval-type function compilation instead of just accepting a toBBox function
// because the algorithms are very sensitive to sorting functions performance,
// so they should be dead simple and without inner calls
var compareArr = ['return a', ' - b', ';'];
this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));
this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));
this.toBBox = new Function('a', 'return [a' + format.join(', a') + '];');
}
};
// calculate node's bbox from bboxes of its children
function calcBBox(node, toBBox) {
node.bbox = distBBox(node, 0, node.children.length, toBBox);
}
// min bounding rectangle of node children from k to p-1
function distBBox(node, k, p, toBBox) {
var bbox = empty();
for (var i = k, child; i < p; i++) {
child = node.children[i];
extend(bbox, node.leaf ? toBBox(child) : child.bbox);
}
return bbox;
}
function empty() { return [Infinity, Infinity, -Infinity, -Infinity]; }
function extend(a, b) {
a[0] = Math.min(a[0], b[0]);
a[1] = Math.min(a[1], b[1]);
a[2] = Math.max(a[2], b[2]);
a[3] = Math.max(a[3], b[3]);
return a;
}
function compareNodeMinX(a, b) { return a.bbox[0] - b.bbox[0]; }
function compareNodeMinY(a, b) { return a.bbox[1] - b.bbox[1]; }
function bboxArea(a) { return (a[2] - a[0]) * (a[3] - a[1]); }
function bboxMargin(a) { return (a[2] - a[0]) + (a[3] - a[1]); }
function enlargedArea(a, b) {
return (Math.max(b[2], a[2]) - Math.min(b[0], a[0])) *
(Math.max(b[3], a[3]) - Math.min(b[1], a[1]));
}
function intersectionArea(a, b) {
var minX = Math.max(a[0], b[0]),
minY = Math.max(a[1], b[1]),
maxX = Math.min(a[2], b[2]),
maxY = Math.min(a[3], b[3]);
return Math.max(0, maxX - minX) *
Math.max(0, maxY - minY);
}
function contains(a, b) {
return a[0] <= b[0] &&
a[1] <= b[1] &&
b[2] <= a[2] &&
b[3] <= a[3];
}
function intersects(a, b) {
return b[0] <= a[2] &&
b[1] <= a[3] &&
b[2] >= a[0] &&
b[3] >= a[1];
}
// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
// combines selection algorithm with binary divide & conquer approach
function multiSelect(arr, left, right, n, compare) {
var stack = [left, right],
mid;
while (stack.length) {
right = stack.pop();
left = stack.pop();
if (right - left <= n) continue;
mid = left + Math.ceil((right - left) / n / 2) * n;
select(arr, left, right, mid, compare);
stack.push(left, mid, mid, right);
}
}
// Floyd-Rivest selection algorithm:
// sort an array between left and right (inclusive) so that the smallest k elements come first (unordered)
function select(arr, left, right, k, compare) {
var n, i, z, s, sd, newLeft, newRight, t, j;
while (right > left) {
if (right - left > 600) {
n = right - left + 1;
i = k - left + 1;
z = Math.log(n);
s = 0.5 * Math.exp(2 * z / 3);
sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (i - n / 2 < 0 ? -1 : 1);
newLeft = Math.max(left, Math.floor(k - i * s / n + sd));
newRight = Math.min(right, Math.floor(k + (n - i) * s / n + sd));
select(arr, newLeft, newRight, k, compare);
}
t = arr[k];
i = left;
j = right;
swap(arr, left, k);
if (compare(arr[right], t) > 0) swap(arr, left, right);
while (i < j) {
swap(arr, i, j);
i++;
j--;
while (compare(arr[i], t) < 0) i++;
while (compare(arr[j], t) > 0) j--;
}
if (compare(arr[left], t) === 0) swap(arr, left, j);
else {
j++;
swap(arr, j, right);
}
if (j <= k) left = j + 1;
if (k <= j) right = j - 1;
}
}
function swap(arr, i, j) {
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// export as AMD/CommonJS module or global variable
if (typeof define === 'function' && define.amd) define('rbush', function () { return rbush; });
else if (typeof module !== 'undefined') module.exports = rbush;
else if (typeof self !== 'undefined') self.rbush = rbush;
else window.rbush = rbush;
})();
});
var rbush$1 = (rbush && typeof rbush === 'object' && 'default' in rbush ? rbush['default'] : rbush);
function Tree(head) {
var rtree = rbush$1(),
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;
}
// Translate a MacOS key command into the appropriate Windows/Linux equivalent.
// For example, ⌘Z -> Ctrl+Z
function cmd(code) {
if (iD.detect().os === 'mac') {
return code;
}
if (iD.detect().os === 'win') {
if (code === '⌘⇧Z') return 'Ctrl+Y';
}
var result = '',
replacements = {
'⌘': 'Ctrl',
'⇧': 'Shift',
'⌥': 'Alt',
'⌫': 'Backspace',
'⌦': 'Delete'
};
for (var i = 0; i < code.length; i++) {
if (code[i] in replacements) {
result += replacements[code[i]] + '+';
} else {
result += code[i];
}
}
return result;
}
function Commit(context) {
var dispatch = d3.dispatch('cancel', 'save');
function commit(selection) {
var changes = context.history().changes(),
summary = context.history().difference().summary();
function zoomToEntity(change) {
var entity = change.entity;
if (change.changeType !== 'deleted' &&
context.graph().entity(entity.id).geometry(context.graph()) !== 'vertex') {
context.map().zoomTo(entity);
context.surface().selectAll(
iD.util.entityOrMemberSelector([entity.id], context.graph()))
.classed('hover', true);
}
}
var header = selection.append('div')
.attr('class', 'header fillL');
header.append('h3')
.text(t('commit.title'));
var body = selection.append('div')
.attr('class', 'body');
// Comment Section
var commentSection = body.append('div')
.attr('class', 'modal-section form-field commit-form');
commentSection.append('label')
.attr('class', 'form-label')
.text(t('commit.message_label'));
var commentField = commentSection.append('textarea')
.attr('placeholder', t('commit.description_placeholder'))
.attr('maxlength', 255)
.property('value', context.storage('comment') || '')
.on('input.save', checkComment)
.on('change.save', checkComment)
.on('blur.save', function() {
context.storage('comment', this.value);
});
function checkComment() {
d3.selectAll('.save-section .save-button')
.attr('disabled', (this.value.length ? null : true));
var googleWarning = clippyArea
.html('')
.selectAll('a')
.data(this.value.match(/google/i) ? [true] : []);
googleWarning.exit().remove();
googleWarning.enter()
.append('a')
.attr('target', '_blank')
.attr('tabindex', -1)
.call(iD.svg.Icon('#icon-alert', 'inline'))
.attr('href', t('commit.google_warning_link'))
.append('span')
.text(t('commit.google_warning'));
}
commentField.node().select();
context.connection().userChangesets(function (err, changesets) {
if (err) return;
var comments = [];
for (var i = 0; i < changesets.length; i++) {
if (changesets[i].tags.comment) {
comments.push({
title: changesets[i].tags.comment,
value: changesets[i].tags.comment
});
}
}
commentField.call(d3.combobox().caseSensitive(true).data(comments));
});
var clippyArea = commentSection.append('div')
.attr('class', 'clippy-area');
var changeSetInfo = commentSection.append('div')
.attr('class', 'changeset-info');
changeSetInfo.append('a')
.attr('target', '_blank')
.attr('tabindex', -1)
.call(iD.svg.Icon('#icon-out-link', 'inline'))
.attr('href', t('commit.about_changeset_comments_link'))
.append('span')
.text(t('commit.about_changeset_comments'));
// Warnings
var warnings = body.selectAll('div.warning-section')
.data([context.history().validate(changes)])
.enter()
.append('div')
.attr('class', 'modal-section warning-section fillL2')
.style('display', function(d) { return _.isEmpty(d) ? 'none' : null; })
.style('background', '#ffb');
warnings.append('h3')
.text(t('commit.warnings'));
var warningLi = warnings.append('ul')
.attr('class', 'changeset-list')
.selectAll('li')
.data(function(d) { return d; })
.enter()
.append('li')
.style()
.on('mouseover', mouseover)
.on('mouseout', mouseout)
.on('click', warningClick);
warningLi
.call(iD.svg.Icon('#icon-alert', 'pre-text'));
warningLi
.append('strong').text(function(d) {
return d.message;
});
warningLi.filter(function(d) { return d.tooltip; })
.call(bootstrap.tooltip()
.title(function(d) { return d.tooltip; })
.placement('top')
);
// Upload Explanation
var saveSection = body.append('div')
.attr('class','modal-section save-section fillL cf');
var prose = saveSection.append('p')
.attr('class', 'commit-info')
.html(t('commit.upload_explanation'));
context.connection().userDetails(function(err, user) {
if (err) return;
var userLink = d3.select(document.createElement('div'));
if (user.image_url) {
userLink.append('img')
.attr('src', user.image_url)
.attr('class', 'icon pre-text user-icon');
}
userLink.append('a')
.attr('class','user-info')
.text(user.display_name)
.attr('href', context.connection().userURL(user.display_name))
.attr('tabindex', -1)
.attr('target', '_blank');
prose.html(t('commit.upload_explanation_with_user', {user: userLink.html()}));
});
// Buttons
var buttonSection = saveSection.append('div')
.attr('class','buttons fillL cf');
var cancelButton = buttonSection.append('button')
.attr('class', 'secondary-action col5 button cancel-button')
.on('click.cancel', function() { dispatch.cancel(); });
cancelButton.append('span')
.attr('class', 'label')
.text(t('commit.cancel'));
var saveButton = buttonSection.append('button')
.attr('class', 'action col5 button save-button')
.attr('disabled', function() {
var n = d3.select('.commit-form textarea').node();
return (n && n.value.length) ? null : true;
})
.on('click.save', function() {
dispatch.save({
comment: commentField.node().value
});
});
saveButton.append('span')
.attr('class', 'label')
.text(t('commit.save'));
// Changes
var changeSection = body.selectAll('div.commit-section')
.data([0])
.enter()
.append('div')
.attr('class', 'commit-section modal-section fillL2');
changeSection.append('h3')
.text(t('commit.changes', {count: summary.length}));
var li = changeSection.append('ul')
.attr('class', 'changeset-list')
.selectAll('li')
.data(summary)
.enter()
.append('li')
.on('mouseover', mouseover)
.on('mouseout', mouseout)
.on('click', zoomToEntity);
li.each(function(d) {
d3.select(this)
.call(iD.svg.Icon('#icon-' + d.entity.geometry(d.graph), 'pre-text ' + d.changeType));
});
li.append('span')
.attr('class', 'change-type')
.text(function(d) {
return t('commit.' + d.changeType) + ' ';
});
li.append('strong')
.attr('class', 'entity-type')
.text(function(d) {
return context.presets().match(d.entity, d.graph).name();
});
li.append('span')
.attr('class', 'entity-name')
.text(function(d) {
var name = iD.util.displayName(d.entity) || '',
string = '';
if (name !== '') string += ':';
return string += ' ' + name;
});
li.style('opacity', 0)
.transition()
.style('opacity', 1);
function mouseover(d) {
if (d.entity) {
context.surface().selectAll(
iD.util.entityOrMemberSelector([d.entity.id], context.graph())
).classed('hover', true);
}
}
function mouseout() {
context.surface().selectAll('.hover')
.classed('hover', false);
}
function warningClick(d) {
if (d.entity) {
context.map().zoomTo(d.entity);
context.enter(
iD.modes.Select(context, [d.entity.id])
.suppressMenu(true));
}
}
// Call checkComment off the bat, in case a changeset
// comment is recovered from localStorage
commentField.trigger('input');
}
return d3.rebind(commit, dispatch, 'on');
}
function modalModule(selection, blocking) {
var keybinding = d3.keybinding('modal');
var previous = selection.select('div.modal');
var animate = previous.empty();
previous.transition()
.duration(200)
.style('opacity', 0)
.remove();
var shaded = selection
.append('div')
.attr('class', 'shaded')
.style('opacity', 0);
shaded.close = function() {
shaded
.transition()
.duration(200)
.style('opacity',0)
.remove();
modal
.transition()
.duration(200)
.style('top','0px');
keybinding.off();
};
var modal = shaded.append('div')
.attr('class', 'modal fillL col6');
if (!blocking) {
shaded.on('click.remove-modal', function() {
if (d3.event.target === this) {
shaded.close();
}
});
modal.append('button')
.attr('class', 'close')
.on('click', shaded.close)
.call(iD.svg.Icon('#icon-close'));
keybinding
.on('⌫', shaded.close)
.on('⎋', shaded.close);
d3.select(document).call(keybinding);
}
modal.append('div')
.attr('class', 'content');
if (animate) {
shaded.transition().style('opacity', 1);
} else {
shaded.style('opacity', 1);
}
return shaded;
}
function Conflicts(context) {
var dispatch = d3.dispatch('download', 'cancel', 'save'),
list;
function conflicts(selection) {
var header = selection
.append('div')
.attr('class', 'header fillL');
header
.append('button')
.attr('class', 'fr')
.on('click', function() { dispatch.cancel(); })
.call(iD.svg.Icon('#icon-close'));
header
.append('h3')
.text(t('save.conflict.header'));
var body = selection
.append('div')
.attr('class', 'body fillL');
body
.append('div')
.attr('class', 'conflicts-help')
.text(t('save.conflict.help'))
.append('a')
.attr('class', 'conflicts-download')
.text(t('save.conflict.download_changes'))
.on('click.download', function() { dispatch.download(); });
body
.append('div')
.attr('class', 'conflict-container fillL3')
.call(showConflict, 0);
body
.append('div')
.attr('class', 'conflicts-done')
.attr('opacity', 0)
.style('display', 'none')
.text(t('save.conflict.done'));
var buttons = body
.append('div')
.attr('class','buttons col12 joined conflicts-buttons');
buttons
.append('button')
.attr('disabled', list.length > 1)
.attr('class', 'action conflicts-button col6')
.text(t('save.title'))
.on('click.try_again', function() { dispatch.save(); });
buttons
.append('button')
.attr('class', 'secondary-action conflicts-button col6')
.text(t('confirm.cancel'))
.on('click.cancel', function() { dispatch.cancel(); });
}
function showConflict(selection, index) {
if (index < 0 || index >= list.length) return;
var parent = d3.select(selection.node().parentNode);
// enable save button if this is the last conflict being reviewed..
if (index === list.length - 1) {
window.setTimeout(function() {
parent.select('.conflicts-button')
.attr('disabled', null);
parent.select('.conflicts-done')
.transition()
.attr('opacity', 1)
.style('display', 'block');
}, 250);
}
var item = selection
.selectAll('.conflict')
.data([list[index]]);
var enter = item.enter()
.append('div')
.attr('class', 'conflict');
enter
.append('h4')
.attr('class', 'conflict-count')
.text(t('save.conflict.count', { num: index + 1, total: list.length }));
enter
.append('a')
.attr('class', 'conflict-description')
.attr('href', '#')
.text(function(d) { return d.name; })
.on('click', function(d) {
zoomToEntity(d.id);
d3.event.preventDefault();
});
var details = enter
.append('div')
.attr('class', 'conflict-detail-container');
details
.append('ul')
.attr('class', 'conflict-detail-list')
.selectAll('li')
.data(function(d) { return d.details || []; })
.enter()
.append('li')
.attr('class', 'conflict-detail-item')
.html(function(d) { return d; });
details
.append('div')
.attr('class', 'conflict-choices')
.call(addChoices);
details
.append('div')
.attr('class', 'conflict-nav-buttons joined cf')
.selectAll('button')
.data(['previous', 'next'])
.enter()
.append('button')
.text(function(d) { return t('save.conflict.' + d); })
.attr('class', 'conflict-nav-button action col6')
.attr('disabled', function(d, i) {
return (i === 0 && index === 0) ||
(i === 1 && index === list.length - 1) || null;
})
.on('click', function(d, i) {
var container = parent.select('.conflict-container'),
sign = (i === 0 ? -1 : 1);
container
.selectAll('.conflict')
.remove();
container
.call(showConflict, index + sign);
d3.event.preventDefault();
});
item.exit()
.remove();
}
function addChoices(selection) {
var choices = selection
.append('ul')
.attr('class', 'layer-list')
.selectAll('li')
.data(function(d) { return d.choices || []; });
var enter = choices.enter()
.append('li')
.attr('class', 'layer');
var label = enter
.append('label');
label
.append('input')
.attr('type', 'radio')
.attr('name', function(d) { return d.id; })
.on('change', function(d, i) {
var ul = this.parentNode.parentNode.parentNode;
ul.__data__.chosen = i;
choose(ul, d);
});
label
.append('span')
.text(function(d) { return d.text; });
choices
.each(function(d, i) {
var ul = this.parentNode;
if (ul.__data__.chosen === i) choose(ul, d);
});
}
function choose(ul, datum) {
if (d3.event) d3.event.preventDefault();
d3.select(ul)
.selectAll('li')
.classed('active', function(d) { return d === datum; })
.selectAll('input')
.property('checked', function(d) { return d === datum; });
var extent = iD.geo.Extent(),
entity;
entity = context.graph().hasEntity(datum.id);
if (entity) extent._extend(entity.extent(context.graph()));
datum.action();
entity = context.graph().hasEntity(datum.id);
if (entity) extent._extend(entity.extent(context.graph()));
zoomToEntity(datum.id, extent);
}
function zoomToEntity(id, extent) {
context.surface().selectAll('.hover')
.classed('hover', false);
var entity = context.graph().hasEntity(id);
if (entity) {
if (extent) {
context.map().trimmedExtent(extent);
} else {
context.map().zoomTo(entity);
}
context.surface().selectAll(
iD.util.entityOrMemberSelector([entity.id], context.graph()))
.classed('hover', true);
}
}
// The conflict list should be an array of objects like:
// {
// id: id,
// name: entityName(local),
// details: merge.conflicts(),
// chosen: 1,
// choices: [
// choice(id, keepMine, forceLocal),
// choice(id, keepTheirs, forceRemote)
// ]
// }
conflicts.list = function(_) {
if (!arguments.length) return list;
list = _;
return conflicts;
};
return d3.rebind(conflicts, dispatch, 'on');
}
// toggles the visibility of ui elements, using a combination of the
// hide class, which sets display=none, and a d3 transition for opacity.
// this will cause blinking when called repeatedly, so check that the
// value actually changes between calls.
function Toggle(show, callback) {
return function(selection) {
selection
.style('opacity', show ? 0 : 1)
.classed('hide', false)
.transition()
.style('opacity', show ? 1 : 0)
.each('end', function() {
d3.select(this)
.classed('hide', !show)
.style('opacity', null);
if (callback) callback.apply(this);
});
};
}
var index = createCommonjsModule(function (module) {
module.exports = element;
module.exports.pair = pair;
module.exports.format = format;
module.exports.formatPair = formatPair;
module.exports.coordToDMS = coordToDMS;
function element(x, dims) {
return search(x, dims).val;
}
function formatPair(x) {
return format(x.lat, 'lat') + ' ' + format(x.lon, 'lon');
}
// Is 0 North or South?
function format(x, dim) {
var dms = coordToDMS(x,dim);
return dms.whole + '° ' +
(dms.minutes ? dms.minutes + '\' ' : '') +
(dms.seconds ? dms.seconds + '" ' : '') + dms.dir;
}
function coordToDMS(x,dim) {
var dirs = {
lat: ['N', 'S'],
lon: ['E', 'W']
}[dim] || '',
dir = dirs[x >= 0 ? 0 : 1],
abs = Math.abs(x),
whole = Math.floor(abs),
fraction = abs - whole,
fractionMinutes = fraction * 60,
minutes = Math.floor(fractionMinutes),
seconds = Math.floor((fractionMinutes - minutes) * 60);
return {
whole: whole,
minutes: minutes,
seconds: seconds,
dir: dir
};
}
function search(x, dims, r) {
if (!dims) dims = 'NSEW';
if (typeof x !== 'string') return { val: null, regex: r };
r = r || /[\s\,]*([\-|\—|\―]?[0-9.]+)°? *(?:([0-9.]+)['’′‘] *)?(?:([0-9.]+)(?:''|"|”|″) *)?([NSEW])?/gi;
var m = r.exec(x);
if (!m) return { val: null, regex: r };
else if (m[4] && dims.indexOf(m[4]) === -1) return { val: null, regex: r };
else return {
val: (((m[1]) ? parseFloat(m[1]) : 0) +
((m[2] ? parseFloat(m[2]) / 60 : 0)) +
((m[3] ? parseFloat(m[3]) / 3600 : 0))) *
((m[4] && m[4] === 'S' || m[4] === 'W') ? -1 : 1),
regex: r,
raw: m[0],
dim: m[4]
};
}
function pair(x, dims) {
x = x.trim();
var one = search(x, dims);
if (one.val === null) return null;
var two = search(x, dims, one.regex);
if (two.val === null) return null;
// null if one/two are not contiguous.
if (one.raw + two.raw !== x) return null;
if (one.dim) {
return swapdim(one.val, two.val, one.dim);
} else {
return [one.val, two.val];
}
}
function swapdim(a, b, dim) {
if (dim === 'N' || dim === 'S') return [a, b];
if (dim === 'W' || dim === 'E') return [b, a];
}
});
function flash(selection) {
var modal = modalModule(selection);
modal.select('.modal').classed('modal-flash', true);
modal.select('.content')
.classed('modal-section', true)
.append('div')
.attr('class', 'description');
modal.on('click.flash', function() { modal.remove(); });
setTimeout(function() {
modal.remove();
return true;
}, 1500);
return modal;
}
function Loading(context) {
var message = '',
blocking = false,
modal;
var loading = function(selection) {
modal = modalModule(selection, blocking);
var loadertext = modal.select('.content')
.classed('loading-modal', true)
.append('div')
.attr('class', 'modal-section fillL');
loadertext.append('img')
.attr('class', 'loader')
.attr('src', context.imagePath('loader-white.gif'));
loadertext.append('h3')
.text(message);
modal.select('button.close')
.attr('class', 'hide');
return loading;
};
loading.message = function(_) {
if (!arguments.length) return message;
message = _;
return loading;
};
loading.blocking = function(_) {
if (!arguments.length) return blocking;
blocking = _;
return loading;
};
loading.close = function() {
modal.remove();
};
return loading;
}
function uiLasso(context) {
var group, polygon;
lasso.coordinates = [];
function lasso(selection) {
context.container().classed('lasso', true);
group = selection.append('g')
.attr('class', 'lasso hide');
polygon = group.append('path')
.attr('class', 'lasso-path');
group.call(Toggle(true));
}
function draw() {
if (polygon) {
polygon.data([lasso.coordinates])
.attr('d', function(d) { return 'M' + d.join(' L') + ' Z'; });
}
}
lasso.extent = function () {
return lasso.coordinates.reduce(function(extent, point) {
return extent.extend(iD.geo.Extent(point));
}, iD.geo.Extent());
};
lasso.p = function(_) {
if (!arguments.length) return lasso;
lasso.coordinates.push(_);
draw();
return lasso;
};
lasso.close = function() {
if (group) {
group.call(Toggle(false, function() {
d3.select(this).remove();
}));
}
context.container().classed('lasso', false);
};
return lasso;
}
function RadialMenu(context, operations) {
var menu,
center = [0, 0],
tooltip;
var radialMenu = function(selection) {
if (!operations.length)
return;
selection.node().parentNode.focus();
function click(operation) {
d3.event.stopPropagation();
if (operation.disabled())
return;
operation();
radialMenu.close();
}
menu = selection.append('g')
.attr('class', 'radial-menu')
.attr('transform', 'translate(' + center + ')')
.attr('opacity', 0);
menu.transition()
.attr('opacity', 1);
var r = 50,
a = Math.PI / 4,
a0 = -Math.PI / 4,
a1 = a0 + (operations.length - 1) * a;
menu.append('path')
.attr('class', 'radial-menu-background')
.attr('d', 'M' + r * Math.sin(a0) + ',' +
r * Math.cos(a0) +
' A' + r + ',' + r + ' 0 ' + (operations.length > 5 ? '1' : '0') + ',0 ' +
(r * Math.sin(a1) + 1e-3) + ',' +
(r * Math.cos(a1) + 1e-3)) // Force positive-length path (#1305)
.attr('stroke-width', 50)
.attr('stroke-linecap', 'round');
var button = menu.selectAll()
.data(operations)
.enter()
.append('g')
.attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; })
.classed('disabled', function(d) { return d.disabled(); })
.attr('transform', function(d, i) {
return 'translate(' + iD.geo.roundCoords([
r * Math.sin(a0 + i * a),
r * Math.cos(a0 + i * a)]).join(',') + ')';
});
button.append('circle')
.attr('r', 15)
.on('click', click)
.on('mousedown', mousedown)
.on('mouseover', mouseover)
.on('mouseout', mouseout);
button.append('use')
.attr('transform', 'translate(-10,-10)')
.attr('width', '20')
.attr('height', '20')
.attr('xlink:href', function(d) { return '#operation-' + d.id; });
tooltip = d3.select(document.body)
.append('div')
.attr('class', 'tooltip-inner radial-menu-tooltip');
function mousedown() {
d3.event.stopPropagation(); // https://github.com/openstreetmap/iD/issues/1869
}
function mouseover(d, i) {
var rect = context.surfaceRect(),
angle = a0 + i * a,
top = rect.top + (r + 25) * Math.cos(angle) + center[1] + 'px',
left = rect.left + (r + 25) * Math.sin(angle) + center[0] + 'px',
bottom = rect.height - (r + 25) * Math.cos(angle) - center[1] + 'px',
right = rect.width - (r + 25) * Math.sin(angle) - center[0] + 'px';
tooltip
.style('top', null)
.style('left', null)
.style('bottom', null)
.style('right', null)
.style('display', 'block')
.html(iD.ui.tooltipHtml(d.tooltip(), d.keys[0]));
if (i === 0) {
tooltip
.style('right', right)
.style('top', top);
} else if (i >= 4) {
tooltip
.style('left', left)
.style('bottom', bottom);
} else {
tooltip
.style('left', left)
.style('top', top);
}
}
function mouseout() {
tooltip.style('display', 'none');
}
};
radialMenu.close = function() {
if (menu) {
menu
.style('pointer-events', 'none')
.transition()
.attr('opacity', 0)
.remove();
}
if (tooltip) {
tooltip.remove();
}
};
radialMenu.center = function(_) {
if (!arguments.length) return center;
center = _;
return radialMenu;
};
return radialMenu;
}
function SelectionList(context, selectedIDs) {
function selectEntity(entity) {
context.enter(iD.modes.Select(context, [entity.id]).suppressMenu(true));
}
function selectionList(selection) {
selection.classed('selection-list-pane', true);
var header = selection.append('div')
.attr('class', 'header fillL cf');
header.append('h3')
.text(t('inspector.multiselect'));
var listWrap = selection.append('div')
.attr('class', 'inspector-body');
var list = listWrap.append('div')
.attr('class', 'feature-list cf');
context.history().on('change.selection-list', drawList);
drawList();
function drawList() {
var entities = selectedIDs
.map(function(id) { return context.hasEntity(id); })
.filter(function(entity) { return entity; });
var items = list.selectAll('.feature-list-item')
.data(entities, iD.Entity.key);
var enter = items.enter().append('button')
.attr('class', 'feature-list-item')
.on('click', selectEntity);
// Enter
var label = enter.append('div')
.attr('class', 'label')
.call(iD.svg.Icon('', 'pre-text'));
label.append('span')
.attr('class', 'entity-type');
label.append('span')
.attr('class', 'entity-name');
// Update
items.selectAll('use')
.attr('href', function() {
var entity = this.parentNode.parentNode.__data__;
return '#icon-' + context.geometry(entity.id);
});
items.selectAll('.entity-type')
.text(function(entity) { return context.presets().match(entity, context.graph()).name(); });
items.selectAll('.entity-name')
.text(function(entity) { return iD.util.displayName(entity); });
// Exit
items.exit()
.remove();
}
}
return selectionList;
}
function Success(context) {
var dispatch = d3.dispatch('cancel'),
changeset;
function success(selection) {
var message = (changeset.comment || t('success.edited_osm')).substring(0, 130) +
' ' + context.connection().changesetURL(changeset.id);
var header = selection.append('div')
.attr('class', 'header fillL');
header.append('button')
.attr('class', 'fr')
.on('click', function() { dispatch.cancel(); })
.call(iD.svg.Icon('#icon-close'));
header.append('h3')
.text(t('success.just_edited'));
var body = selection.append('div')
.attr('class', 'body save-success fillL');
body.append('p')
.html(t('success.help_html'));
body.append('a')
.attr('class', 'details')
.attr('target', '_blank')
.attr('tabindex', -1)
.call(iD.svg.Icon('#icon-out-link', 'inline'))
.attr('href', t('success.help_link_url'))
.append('span')
.text(t('success.help_link_text'));
var changesetURL = context.connection().changesetURL(changeset.id);
body.append('a')
.attr('class', 'button col12 osm')
.attr('target', '_blank')
.attr('href', changesetURL)
.text(t('success.view_on_osm'));
var sharing = {
facebook: 'https://facebook.com/sharer/sharer.php?u=' + encodeURIComponent(changesetURL),
twitter: 'https://twitter.com/intent/tweet?source=webclient&text=' + encodeURIComponent(message),
google: 'https://plus.google.com/share?url=' + encodeURIComponent(changesetURL)
};
body.selectAll('.button.social')
.data(d3.entries(sharing))
.enter()
.append('a')
.attr('class', 'button social col4')
.attr('target', '_blank')
.attr('href', function(d) { return d.value; })
.call(bootstrap.tooltip()
.title(function(d) { return t('success.' + d.key); })
.placement('bottom'))
.each(function(d) { d3.select(this).call(iD.svg.Icon('#logo-' + d.key, 'social')); });
}
success.changeset = function(_) {
if (!arguments.length) return changeset;
changeset = _;
return success;
};
return d3.rebind(success, dispatch, 'on');
}
function History(context) {
var stack, index, tree,
imageryUsed = ['Bing'],
dispatch = d3.dispatch('change', 'undone', 'redone'),
lock = SessionMutex('lock');
function perform(actions) {
actions = Array.prototype.slice.call(actions);
var annotation;
if (!_.isFunction(_.last(actions))) {
annotation = actions.pop();
}
var graph = stack[index].graph;
for (var i = 0; i < actions.length; i++) {
graph = actions[i](graph);
}
return {
graph: graph,
annotation: annotation,
imageryUsed: imageryUsed
};
}
function change(previous) {
var difference = Difference(previous, history.graph());
dispatch.change(difference);
return difference;
}
// iD uses namespaced keys so multiple installations do not conflict
function getKey(n) {
return 'iD_' + window.location.origin + '_' + n;
}
var history = {
graph: function() {
return stack[index].graph;
},
base: function() {
return stack[0].graph;
},
merge: function(entities, extent) {
stack[0].graph.rebase(entities, _.map(stack, 'graph'), false);
tree.rebase(entities, false);
dispatch.change(undefined, extent);
},
perform: function() {
var previous = stack[index].graph;
stack = stack.slice(0, index + 1);
stack.push(perform(arguments));
index++;
return change(previous);
},
replace: function() {
var previous = stack[index].graph;
// assert(index == stack.length - 1)
stack[index] = perform(arguments);
return change(previous);
},
pop: function() {
var previous = stack[index].graph;
if (index > 0) {
index--;
stack.pop();
return change(previous);
}
},
// Same as calling pop and then perform
overwrite: function() {
var previous = stack[index].graph;
if (index > 0) {
index--;
stack.pop();
}
stack = stack.slice(0, index + 1);
stack.push(perform(arguments));
index++;
return change(previous);
},
undo: function() {
var previous = stack[index].graph;
// Pop to the next annotated state.
while (index > 0) {
index--;
if (stack[index].annotation) break;
}
dispatch.undone();
return change(previous);
},
redo: function() {
var previous = stack[index].graph;
while (index < stack.length - 1) {
index++;
if (stack[index].annotation) break;
}
dispatch.redone();
return change(previous);
},
undoAnnotation: function() {
var i = index;
while (i >= 0) {
if (stack[i].annotation) return stack[i].annotation;
i--;
}
},
redoAnnotation: function() {
var i = index + 1;
while (i <= stack.length - 1) {
if (stack[i].annotation) return stack[i].annotation;
i++;
}
},
intersects: function(extent) {
return tree.intersects(extent, stack[index].graph);
},
difference: function() {
var base = stack[0].graph,
head = stack[index].graph;
return Difference(base, head);
},
changes: function(action) {
var base = stack[0].graph,
head = stack[index].graph;
if (action) {
head = action(head);
}
var difference = Difference(base, head);
return {
modified: difference.modified(),
created: difference.created(),
deleted: difference.deleted()
};
},
validate: function(changes) {
return _(iD.validations)
.map(function(fn) { return fn()(changes, stack[index].graph); })
.flatten()
.value();
},
hasChanges: function() {
return this.difference().length() > 0;
},
imageryUsed: function(sources) {
if (sources) {
imageryUsed = sources;
return history;
} else {
return _(stack.slice(1, index + 1))
.map('imageryUsed')
.flatten()
.uniq()
.without(undefined, 'Custom')
.value();
}
},
reset: function() {
stack = [{graph: Graph()}];
index = 0;
tree = Tree(stack[0].graph);
dispatch.change();
return history;
},
toJSON: function() {
if (!this.hasChanges()) return;
var allEntities = {},
baseEntities = {},
base = stack[0];
var s = stack.map(function(i) {
var modified = [], deleted = [];
_.forEach(i.graph.entities, function(entity, id) {
if (entity) {
var key = Entity.key(entity);
allEntities[key] = entity;
modified.push(key);
} else {
deleted.push(id);
}
// make sure that the originals of changed or deleted entities get merged
// into the base of the stack after restoring the data from JSON.
if (id in base.graph.entities) {
baseEntities[id] = base.graph.entities[id];
}
// get originals of parent entities too
_.forEach(base.graph._parentWays[id], function(parentId) {
if (parentId in base.graph.entities) {
baseEntities[parentId] = base.graph.entities[parentId];
}
});
});
var x = {};
if (modified.length) x.modified = modified;
if (deleted.length) x.deleted = deleted;
if (i.imageryUsed) x.imageryUsed = i.imageryUsed;
if (i.annotation) x.annotation = i.annotation;
return x;
});
return JSON.stringify({
version: 3,
entities: _.values(allEntities),
baseEntities: _.values(baseEntities),
stack: s,
nextIDs: Entity.id.next,
index: index
});
},
fromJSON: function(json, loadChildNodes) {
var h = JSON.parse(json),
loadComplete = true;
Entity.id.next = h.nextIDs;
index = h.index;
if (h.version === 2 || h.version === 3) {
var allEntities = {};
h.entities.forEach(function(entity) {
allEntities[Entity.key(entity)] = Entity(entity);
});
if (h.version === 3) {
// This merges originals for changed entities into the base of
// the stack even if the current stack doesn't have them (for
// example when iD has been restarted in a different region)
var baseEntities = h.baseEntities.map(function(d) { return Entity(d); });
stack[0].graph.rebase(baseEntities, _.map(stack, 'graph'), true);
tree.rebase(baseEntities, true);
// When we restore a modified way, we also need to fetch any missing
// childnodes that would normally have been downloaded with it.. #2142
if (loadChildNodes) {
var missing = _(baseEntities)
.filter({ type: 'way' })
.map('nodes')
.flatten()
.uniq()
.reject(function(n) { return stack[0].graph.hasEntity(n); })
.value();
if (!_.isEmpty(missing)) {
loadComplete = false;
context.redrawEnable(false);
var loading = Loading(context).blocking(true);
context.container().call(loading);
var childNodesLoaded = function(err, result) {
if (!err) {
var visible = _.groupBy(result.data, 'visible');
if (!_.isEmpty(visible.true)) {
missing = _.difference(missing, _.map(visible.true, 'id'));
stack[0].graph.rebase(visible.true, _.map(stack, 'graph'), true);
tree.rebase(visible.true, true);
}
// fetch older versions of nodes that were deleted..
_.each(visible.false, function(entity) {
context.connection()
.loadEntityVersion(entity.id, +entity.version - 1, childNodesLoaded);
});
}
if (err || _.isEmpty(missing)) {
loading.close();
context.redrawEnable(true);
dispatch.change();
}
};
context.connection().loadMultiple(missing, childNodesLoaded);
}
}
}
stack = h.stack.map(function(d) {
var entities = {}, entity;
if (d.modified) {
d.modified.forEach(function(key) {
entity = allEntities[key];
entities[entity.id] = entity;
});
}
if (d.deleted) {
d.deleted.forEach(function(id) {
entities[id] = undefined;
});
}
return {
graph: Graph(stack[0].graph).load(entities),
annotation: d.annotation,
imageryUsed: d.imageryUsed
};
});
} else { // original version
stack = h.stack.map(function(d) {
var entities = {};
for (var i in d.entities) {
var entity = d.entities[i];
entities[i] = entity === 'undefined' ? undefined : Entity(entity);
}
d.graph = Graph(stack[0].graph).load(entities);
return d;
});
}
if (loadComplete) {
dispatch.change();
}
return history;
},
save: function() {
if (lock.locked()) context.storage(getKey('saved_history'), history.toJSON() || null);
return history;
},
clearSaved: function() {
context.debouncedSave.cancel();
if (lock.locked()) context.storage(getKey('saved_history'), null);
return history;
},
lock: function() {
return lock.lock();
},
unlock: function() {
lock.unlock();
},
// is iD not open in another window and it detects that
// there's a history stored in localStorage that's recoverable?
restorableChanges: function() {
return lock.locked() && !!context.storage(getKey('saved_history'));
},
// load history from a version stored in localStorage
restore: function() {
if (!lock.locked()) return;
var json = context.storage(getKey('saved_history'));
if (json) history.fromJSON(json, true);
},
_getKey: getKey
};
history.reset();
return d3.rebind(history, dispatch, 'on');
}
function Turn(turn) {
if (!(this instanceof Turn))
return new Turn(turn);
_.extend(this, turn);
}
function Intersection(graph, vertexId) {
var vertex = graph.entity(vertexId),
parentWays = graph.parentWays(vertex),
coincident = [],
highways = {};
function addHighway(way, adjacentNodeId) {
if (highways[adjacentNodeId]) {
coincident.push(adjacentNodeId);
} else {
highways[adjacentNodeId] = way;
}
}
// Pre-split ways that would need to be split in
// order to add a restriction. The real split will
// happen when the restriction is added.
parentWays.forEach(function(way) {
if (!way.tags.highway || way.isArea() || way.isDegenerate())
return;
var isFirst = (vertexId === way.first()),
isLast = (vertexId === way.last()),
isAffix = (isFirst || isLast),
isClosingNode = (isFirst && isLast);
if (isAffix && !isClosingNode) {
var index = (isFirst ? 1 : way.nodes.length - 2);
addHighway(way, way.nodes[index]);
} else {
var splitIndex, wayA, wayB, indexA, indexB;
if (isClosingNode) {
splitIndex = Math.ceil(way.nodes.length / 2); // split at midpoint
wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)});
wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
indexA = 1;
indexB = way.nodes.length - 2;
} else {
splitIndex = _.indexOf(way.nodes, vertex.id, 1); // split at vertexid
wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)});
wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
indexA = splitIndex - 1;
indexB = splitIndex + 1;
}
graph = graph.replace(wayA).replace(wayB);
addHighway(wayA, way.nodes[indexA]);
addHighway(wayB, way.nodes[indexB]);
}
});
// remove any ways from this intersection that are coincident
// (i.e. any adjacent node used by more than one intersecting way)
coincident.forEach(function (n) {
delete highways[n];
});
var intersection = {
highways: highways,
ways: _.values(highways),
graph: graph
};
intersection.adjacentNodeId = function(fromWayId) {
return _.find(_.keys(highways), function(k) {
return highways[k].id === fromWayId;
});
};
intersection.turns = function(fromNodeId) {
var start = highways[fromNodeId];
if (!start)
return [];
if (start.first() === vertex.id && start.tags.oneway === 'yes')
return [];
if (start.last() === vertex.id && start.tags.oneway === '-1')
return [];
function withRestriction(turn) {
graph.parentRelations(graph.entity(turn.from.way)).forEach(function(relation) {
if (relation.tags.type !== 'restriction')
return;
var f = relation.memberByRole('from'),
t = relation.memberByRole('to'),
v = relation.memberByRole('via');
if (f && f.id === turn.from.way &&
v && v.id === turn.via.node &&
t && t.id === turn.to.way) {
turn.restriction = relation.id;
} else if (/^only_/.test(relation.tags.restriction) &&
f && f.id === turn.from.way &&
v && v.id === turn.via.node &&
t && t.id !== turn.to.way) {
turn.restriction = relation.id;
turn.indirect_restriction = true;
}
});
return Turn(turn);
}
var from = {
node: fromNodeId,
way: start.id.split(/-(a|b)/)[0]
},
via = { node: vertex.id },
turns = [];
_.each(highways, function(end, adjacentNodeId) {
if (end === start)
return;
// backward
if (end.first() !== vertex.id && end.tags.oneway !== 'yes') {
turns.push(withRestriction({
from: from,
via: via,
to: {
node: adjacentNodeId,
way: end.id.split(/-(a|b)/)[0]
}
}));
}
// forward
if (end.last() !== vertex.id && end.tags.oneway !== '-1') {
turns.push(withRestriction({
from: from,
via: via,
to: {
node: adjacentNodeId,
way: end.id.split(/-(a|b)/)[0]
}
}));
}
});
// U-turn
if (start.tags.oneway !== 'yes' && start.tags.oneway !== '-1') {
turns.push(withRestriction({
from: from,
via: via,
to: from,
u: true
}));
}
return turns;
};
return intersection;
}
function inferRestriction(graph, from, via, to, projection) {
var fromWay = graph.entity(from.way),
fromNode = graph.entity(from.node),
toWay = graph.entity(to.way),
toNode = graph.entity(to.node),
viaNode = graph.entity(via.node),
fromOneWay = (fromWay.tags.oneway === 'yes' && fromWay.last() === via.node) ||
(fromWay.tags.oneway === '-1' && fromWay.first() === via.node),
toOneWay = (toWay.tags.oneway === 'yes' && toWay.first() === via.node) ||
(toWay.tags.oneway === '-1' && toWay.last() === via.node),
angle = getAngle(viaNode, fromNode, projection) -
getAngle(viaNode, toNode, projection);
angle = angle * 180 / Math.PI;
while (angle < 0)
angle += 360;
if (fromNode === toNode)
return 'no_u_turn';
if ((angle < 23 || angle > 336) && fromOneWay && toOneWay)
return 'no_u_turn';
if (angle < 158)
return 'no_right_turn';
if (angle > 202)
return 'no_left_turn';
return 'no_straight_on';
}
// For fixing up rendering of multipolygons with tags on the outer member.
// https://github.com/openstreetmap/iD/issues/613
function isSimpleMultipolygonOuterMember(entity, graph) {
if (entity.type !== 'way')
return false;
var parents = graph.parentRelations(entity);
if (parents.length !== 1)
return false;
var parent = parents[0];
if (!parent.isMultipolygon() || Object.keys(parent.tags).length > 1)
return false;
var members = parent.members, member;
for (var i = 0; i < members.length; i++) {
member = members[i];
if (member.id === entity.id && member.role && member.role !== 'outer')
return false; // Not outer member
if (member.id !== entity.id && (!member.role || member.role === 'outer'))
return false; // Not a simple multipolygon
}
return parent;
}
function simpleMultipolygonOuterMember(entity, graph) {
if (entity.type !== 'way')
return false;
var parents = graph.parentRelations(entity);
if (parents.length !== 1)
return false;
var parent = parents[0];
if (!parent.isMultipolygon() || Object.keys(parent.tags).length > 1)
return false;
var members = parent.members, member, outerMember;
for (var i = 0; i < members.length; i++) {
member = members[i];
if (!member.role || member.role === 'outer') {
if (outerMember)
return false; // Not a simple multipolygon
outerMember = member;
}
}
return outerMember && graph.hasEntity(outerMember.id);
}
// Join `array` into sequences of connecting ways.
//
// Segments which share identical start/end nodes will, as much as possible,
// be connected with each other.
//
// The return value is a nested array. Each constituent array contains elements
// of `array` which have been determined to connect. Each consitituent array
// also has a `nodes` property whose value is an ordered array of member nodes,
// with appropriate order reversal and start/end coordinate de-duplication.
//
// Members of `array` must have, at minimum, `type` and `id` properties.
// Thus either an array of `iD.Way`s or a relation member array may be
// used.
//
// If an member has a `tags` property, its tags will be reversed via
// `iD.actions.Reverse` in the output.
//
// Incomplete members (those for which `graph.hasEntity(element.id)` returns
// false) and non-way members are ignored.
//
function joinWays(array, graph) {
var joined = [], member, current, nodes, first, last, i, how, what;
array = array.filter(function(member) {
return member.type === 'way' && graph.hasEntity(member.id);
});
function resolve(member) {
return graph.childNodes(graph.entity(member.id));
}
function reverse(member) {
return member.tags ? iD.actions.Reverse(member.id, {reverseOneway: true})(graph).entity(member.id) : member;
}
while (array.length) {
member = array.shift();
current = [member];
current.nodes = nodes = resolve(member).slice();
joined.push(current);
while (array.length && _.first(nodes) !== _.last(nodes)) {
first = _.first(nodes);
last = _.last(nodes);
for (i = 0; i < array.length; i++) {
member = array[i];
what = resolve(member);
if (last === _.first(what)) {
how = nodes.push;
what = what.slice(1);
break;
} else if (last === _.last(what)) {
how = nodes.push;
what = what.slice(0, -1).reverse();
member = reverse(member);
break;
} else if (first === _.last(what)) {
how = nodes.unshift;
what = what.slice(0, -1);
break;
} else if (first === _.first(what)) {
how = nodes.unshift;
what = what.slice(1).reverse();
member = reverse(member);
break;
} else {
what = how = null;
}
}
if (!what)
break; // No more joinable ways.
how.apply(current, [member]);
how.apply(nodes, what);
array.splice(i, 1);
}
}
return joined;
}
/*
Bypasses features of D3's default projection stream pipeline that are unnecessary:
* Antimeridian clipping
* Spherical rotation
* Resampling
*/
function RawMercator() {
var project = d3.geo.mercator.raw,
k = 512 / Math.PI, // scale
x = 0, y = 0, // translate
clipExtent = [[0, 0], [0, 0]];
function projection(point) {
point = project(point[0] * Math.PI / 180, point[1] * Math.PI / 180);
return [point[0] * k + x, y - point[1] * k];
}
projection.invert = function(point) {
point = project.invert((point[0] - x) / k, (y - point[1]) / k);
return point && [point[0] * 180 / Math.PI, point[1] * 180 / Math.PI];
};
projection.scale = function(_) {
if (!arguments.length) return k;
k = +_;
return projection;
};
projection.translate = function(_) {
if (!arguments.length) return [x, y];
x = +_[0];
y = +_[1];
return projection;
};
projection.clipExtent = function(_) {
if (!arguments.length) return clipExtent;
clipExtent = _;
return projection;
};
projection.stream = d3.geo.transform({
point: function(x, y) {
x = projection([x, y]);
this.stream.point(x[0], x[1]);
}
}).stream;
return projection;
}
function roundCoords(c) {
return [Math.floor(c[0]), Math.floor(c[1])];
}
function interp(p1, p2, t) {
return [p1[0] + (p2[0] - p1[0]) * t,
p1[1] + (p2[1] - p1[1]) * t];
}
// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
// Returns a positive value, if OAB makes a counter-clockwise turn,
// negative for clockwise turn, and zero if the points are collinear.
function cross(o, a, b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
// http://jsperf.com/id-dist-optimization
function euclideanDistance(a, b) {
var x = a[0] - b[0], y = a[1] - b[1];
return Math.sqrt((x * x) + (y * y));
}
// using WGS84 polar radius (6356752.314245179 m)
// const = 2 * PI * r / 360
function latToMeters(dLat) {
return dLat * 110946.257617;
}
// using WGS84 equatorial radius (6378137.0 m)
// const = 2 * PI * r / 360
function lonToMeters(dLon, atLat) {
return Math.abs(atLat) >= 90 ? 0 :
dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180)));
}
// using WGS84 polar radius (6356752.314245179 m)
// const = 2 * PI * r / 360
function metersToLat(m) {
return m / 110946.257617;
}
// using WGS84 equatorial radius (6378137.0 m)
// const = 2 * PI * r / 360
function metersToLon(m, atLat) {
return Math.abs(atLat) >= 90 ? 0 :
m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180)));
}
function offsetToMeters(offset) {
var equatRadius = 6356752.314245179,
polarRadius = 6378137.0,
tileSize = 256;
return [
offset[0] * 2 * Math.PI * equatRadius / tileSize,
-offset[1] * 2 * Math.PI * polarRadius / tileSize
];
}
function metersToOffset(meters) {
var equatRadius = 6356752.314245179,
polarRadius = 6378137.0,
tileSize = 256;
return [
meters[0] * tileSize / (2 * Math.PI * equatRadius),
-meters[1] * tileSize / (2 * Math.PI * polarRadius)
];
}
// Equirectangular approximation of spherical distances on Earth
function sphericalDistance(a, b) {
var x = lonToMeters(a[0] - b[0], (a[1] + b[1]) / 2),
y = latToMeters(a[1] - b[1]);
return Math.sqrt((x * x) + (y * y));
}
function edgeEqual(a, b) {
return (a[0] === b[0] && a[1] === b[1]) ||
(a[0] === b[1] && a[1] === b[0]);
}
// Return the counterclockwise angle in the range (-pi, pi)
// between the positive X axis and the line intersecting a and b.
function getAngle(a, b, projection) {
a = projection(a.loc);
b = projection(b.loc);
return Math.atan2(b[1] - a[1], b[0] - a[0]);
}
// Choose the edge with the minimal distance from `point` to its orthogonal
// projection onto that edge, if such a projection exists, or the distance to
// the closest vertex on that edge. Returns an object with the `index` of the
// chosen edge, the chosen `loc` on that edge, and the `distance` to to it.
function chooseEdge(nodes, point, projection) {
var dist = euclideanDistance,
points = nodes.map(function(n) { return projection(n.loc); }),
min = Infinity,
idx, loc;
function dot(p, q) {
return p[0] * q[0] + p[1] * q[1];
}
for (var i = 0; i < points.length - 1; i++) {
var o = points[i],
s = [points[i + 1][0] - o[0],
points[i + 1][1] - o[1]],
v = [point[0] - o[0],
point[1] - o[1]],
proj = dot(v, s) / dot(s, s),
p;
if (proj < 0) {
p = o;
} else if (proj > 1) {
p = points[i + 1];
} else {
p = [o[0] + proj * s[0], o[1] + proj * s[1]];
}
var d = dist(p, point);
if (d < min) {
min = d;
idx = i + 1;
loc = projection.invert(p);
}
}
return {
index: idx,
distance: min,
loc: loc
};
}
// Return the intersection point of 2 line segments.
// From https://github.com/pgkelley4/line-segments-intersect
// This uses the vector cross product approach described below:
// http://stackoverflow.com/a/565282/786339
function lineIntersection(a, b) {
function subtractPoints(point1, point2) {
return [point1[0] - point2[0], point1[1] - point2[1]];
}
function crossProduct(point1, point2) {
return point1[0] * point2[1] - point1[1] * point2[0];
}
var p = [a[0][0], a[0][1]],
p2 = [a[1][0], a[1][1]],
q = [b[0][0], b[0][1]],
q2 = [b[1][0], b[1][1]],
r = subtractPoints(p2, p),
s = subtractPoints(q2, q),
uNumerator = crossProduct(subtractPoints(q, p), r),
denominator = crossProduct(r, s);
if (uNumerator && denominator) {
var u = uNumerator / denominator,
t = crossProduct(subtractPoints(q, p), s) / denominator;
if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) {
return interp(p, p2, t);
}
}
return null;
}
function pathIntersections(path1, path2) {
var intersections = [];
for (var i = 0; i < path1.length - 1; i++) {
for (var j = 0; j < path2.length - 1; j++) {
var a = [ path1[i], path1[i+1] ],
b = [ path2[j], path2[j+1] ],
hit = lineIntersection(a, b);
if (hit) intersections.push(hit);
}
}
return intersections;
}
// Return whether point is contained in polygon.
//
// `point` should be a 2-item array of coordinates.
// `polygon` should be an array of 2-item arrays of coordinates.
//
// From https://github.com/substack/point-in-polygon.
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
//
function pointInPolygon(point, polygon) {
var x = point[0],
y = point[1],
inside = false;
for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
var xi = polygon[i][0], yi = polygon[i][1];
var xj = polygon[j][0], yj = polygon[j][1];
var intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
function polygonContainsPolygon(outer, inner) {
return _.every(inner, function(point) {
return pointInPolygon(point, outer);
});
}
function polygonIntersectsPolygon(outer, inner, checkSegments) {
function testSegments(outer, inner) {
for (var i = 0; i < outer.length - 1; i++) {
for (var j = 0; j < inner.length - 1; j++) {
var a = [ outer[i], outer[i+1] ],
b = [ inner[j], inner[j+1] ];
if (lineIntersection(a, b)) return true;
}
}
return false;
}
function testPoints(outer, inner) {
return _.some(inner, function(point) {
return pointInPolygon(point, outer);
});
}
return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner));
}
function pathLength(path) {
var length = 0,
dx, dy;
for (var i = 0; i < path.length - 1; i++) {
dx = path[i][0] - path[i + 1][0];
dy = path[i][1] - path[i + 1][1];
length += Math.sqrt(dx * dx + dy * dy);
}
return length;
}
var geo = Object.freeze({
roundCoords: roundCoords,
interp: interp,
cross: cross,
euclideanDistance: euclideanDistance,
latToMeters: latToMeters,
lonToMeters: lonToMeters,
metersToLat: metersToLat,
metersToLon: metersToLon,
offsetToMeters: offsetToMeters,
metersToOffset: metersToOffset,
sphericalDistance: sphericalDistance,
edgeEqual: edgeEqual,
angle: getAngle,
chooseEdge: chooseEdge,
lineIntersection: lineIntersection,
pathIntersections: pathIntersections,
pointInPolygon: pointInPolygon,
polygonContainsPolygon: polygonContainsPolygon,
polygonIntersectsPolygon: polygonIntersectsPolygon,
pathLength: pathLength,
Extent: Extent,
Intersection: Intersection,
Turn: Turn,
inferRestriction: inferRestriction,
isSimpleMultipolygonOuterMember: isSimpleMultipolygonOuterMember,
simpleMultipolygonOuterMember: simpleMultipolygonOuterMember,
joinWays: joinWays,
RawMercator: RawMercator
});
function AddMember(relationId, member, memberIndex) {
return function(graph) {
var relation = graph.entity(relationId);
if (isNaN(memberIndex) && member.type === 'way') {
var members = relation.indexedMembers();
members.push(member);
var joined = joinWays(members, graph);
for (var i = 0; i < joined.length; i++) {
var segment = joined[i];
for (var j = 0; j < segment.length && segment.length >= 2; j++) {
if (segment[j] !== member)
continue;
if (j === 0) {
memberIndex = segment[j + 1].index;
} else if (j === segment.length - 1) {
memberIndex = segment[j - 1].index + 1;
} else {
memberIndex = Math.min(segment[j - 1].index + 1, segment[j + 1].index + 1);
}
}
}
}
return graph.replace(relation.addMember(member, memberIndex));
};
}
function AddMidpoint(midpoint, node) {
return function(graph) {
graph = graph.replace(node.move(midpoint.loc));
var parents = _.intersection(
graph.parentWays(graph.entity(midpoint.edge[0])),
graph.parentWays(graph.entity(midpoint.edge[1])));
parents.forEach(function(way) {
for (var i = 0; i < way.nodes.length - 1; i++) {
if (edgeEqual([way.nodes[i], way.nodes[i + 1]], midpoint.edge)) {
graph = graph.replace(graph.entity(way.id).addNode(node.id, i + 1));
// Add only one midpoint on doubled-back segments,
// turning them into self-intersections.
return;
}
}
});
return graph;
};
}
// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as
function AddVertex(wayId, nodeId, index) {
return function(graph) {
return graph.replace(graph.entity(wayId).addNode(nodeId, index));
};
}
function ChangeMember(relationId, member, memberIndex) {
return function(graph) {
return graph.replace(graph.entity(relationId).updateMember(member, memberIndex));
};
}
function ChangePreset(entityId, oldPreset, newPreset) {
return function(graph) {
var entity = graph.entity(entityId),
geometry = entity.geometry(graph),
tags = entity.tags;
if (oldPreset) tags = oldPreset.removeTags(tags, geometry);
if (newPreset) tags = newPreset.applyTags(tags, geometry);
return graph.replace(entity.update({tags: tags}));
};
}
function ChangeTags(entityId, tags) {
return function(graph) {
var entity = graph.entity(entityId);
return graph.replace(entity.update({tags: tags}));
};
}
function CircularizeAction(wayId
, projection, maxAngle) {
maxAngle = (maxAngle || 20) * Math.PI / 180;
var action = function(graph) {
var way = graph.entity(wayId);
if (!way.isConvex(graph)) {
graph = action.makeConvex(graph);
}
var nodes = _.uniq(graph.childNodes(way)),
keyNodes = nodes.filter(function(n) { return graph.parentWays(n).length !== 1; }),
points = nodes.map(function(n) { return projection(n.loc); }),
keyPoints = keyNodes.map(function(n) { return projection(n.loc); }),
centroid = (points.length === 2) ? interp(points[0], points[1], 0.5) : d3.geom.polygon(points).centroid(),
radius = d3.median(points, function(p) { return euclideanDistance(centroid, p); }),
sign = d3.geom.polygon(points).area() > 0 ? 1 : -1,
ids;
// we need atleast two key nodes for the algorithm to work
if (!keyNodes.length) {
keyNodes = [nodes[0]];
keyPoints = [points[0]];
}
if (keyNodes.length === 1) {
var index = nodes.indexOf(keyNodes[0]),
oppositeIndex = Math.floor((index + nodes.length / 2) % nodes.length);
keyNodes.push(nodes[oppositeIndex]);
keyPoints.push(points[oppositeIndex]);
}
// key points and nodes are those connected to the ways,
// they are projected onto the circle, inbetween nodes are moved
// to constant intervals between key nodes, extra inbetween nodes are
// added if necessary.
for (var i = 0; i < keyPoints.length; i++) {
var nextKeyNodeIndex = (i + 1) % keyNodes.length,
startNode = keyNodes[i],
endNode = keyNodes[nextKeyNodeIndex],
startNodeIndex = nodes.indexOf(startNode),
endNodeIndex = nodes.indexOf(endNode),
numberNewPoints = -1,
indexRange = endNodeIndex - startNodeIndex,
distance, totalAngle, eachAngle, startAngle, endAngle,
angle, loc, node, j,
inBetweenNodes = [];
if (indexRange < 0) {
indexRange += nodes.length;
}
// position this key node
distance = euclideanDistance(centroid, keyPoints[i]);
if (distance === 0) { distance = 1e-4; }
keyPoints[i] = [
centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius,
centroid[1] + (keyPoints[i][1] - centroid[1]) / distance * radius];
graph = graph.replace(keyNodes[i].move(projection.invert(keyPoints[i])));
// figure out the between delta angle we want to match to
startAngle = Math.atan2(keyPoints[i][1] - centroid[1], keyPoints[i][0] - centroid[0]);
endAngle = Math.atan2(keyPoints[nextKeyNodeIndex][1] - centroid[1], keyPoints[nextKeyNodeIndex][0] - centroid[0]);
totalAngle = endAngle - startAngle;
// detects looping around -pi/pi
if (totalAngle * sign > 0) {
totalAngle = -sign * (2 * Math.PI - Math.abs(totalAngle));
}
do {
numberNewPoints++;
eachAngle = totalAngle / (indexRange + numberNewPoints);
} while (Math.abs(eachAngle) > maxAngle);
// move existing points
for (j = 1; j < indexRange; j++) {
angle = startAngle + j * eachAngle;
loc = projection.invert([
centroid[0] + Math.cos(angle)*radius,
centroid[1] + Math.sin(angle)*radius]);
node = nodes[(j + startNodeIndex) % nodes.length].move(loc);
graph = graph.replace(node);
}
// add new inbetween nodes if necessary
for (j = 0; j < numberNewPoints; j++) {
angle = startAngle + (indexRange + j) * eachAngle;
loc = projection.invert([
centroid[0] + Math.cos(angle) * radius,
centroid[1] + Math.sin(angle) * radius]);
node = Node({loc: loc});
graph = graph.replace(node);
nodes.splice(endNodeIndex + j, 0, node);
inBetweenNodes.push(node.id);
}
// Check for other ways that share these keyNodes..
// If keyNodes are adjacent in both ways,
// we can add inBetween nodes to that shared way too..
if (indexRange === 1 && inBetweenNodes.length) {
var startIndex1 = way.nodes.lastIndexOf(startNode.id),
endIndex1 = way.nodes.lastIndexOf(endNode.id),
wayDirection1 = (endIndex1 - startIndex1);
if (wayDirection1 < -1) { wayDirection1 = 1; }
/* eslint-disable no-loop-func */
_.each(_.without(graph.parentWays(keyNodes[i]), way), function(sharedWay) {
if (sharedWay.areAdjacent(startNode.id, endNode.id)) {
var startIndex2 = sharedWay.nodes.lastIndexOf(startNode.id),
endIndex2 = sharedWay.nodes.lastIndexOf(endNode.id),
wayDirection2 = (endIndex2 - startIndex2),
insertAt = endIndex2;
if (wayDirection2 < -1) { wayDirection2 = 1; }
if (wayDirection1 !== wayDirection2) {
inBetweenNodes.reverse();
insertAt = startIndex2;
}
for (j = 0; j < inBetweenNodes.length; j++) {
sharedWay = sharedWay.addNode(inBetweenNodes[j], insertAt + j);
}
graph = graph.replace(sharedWay);
}
});
/* eslint-enable no-loop-func */
}
}
// update the way to have all the new nodes
ids = nodes.map(function(n) { return n.id; });
ids.push(ids[0]);
way = way.update({nodes: ids});
graph = graph.replace(way);
return graph;
};
action.makeConvex = function(graph) {
var way = graph.entity(wayId),
nodes = _.uniq(graph.childNodes(way)),
points = nodes.map(function(n) { return projection(n.loc); }),
sign = d3.geom.polygon(points).area() > 0 ? 1 : -1,
hull = d3.geom.hull(points);
// D3 convex hulls go counterclockwise..
if (sign === -1) {
nodes.reverse();
points.reverse();
}
for (var i = 0; i < hull.length - 1; i++) {
var startIndex = points.indexOf(hull[i]),
endIndex = points.indexOf(hull[i+1]),
indexRange = (endIndex - startIndex);
if (indexRange < 0) {
indexRange += nodes.length;
}
// move interior nodes to the surface of the convex hull..
for (var j = 1; j < indexRange; j++) {
var point = interp(hull[i], hull[i+1], j / indexRange),
node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point));
graph = graph.replace(node);
}
}
return graph;
};
action.disabled = function(graph) {
if (!graph.entity(wayId).isClosed())
return 'not_closed';
};
return action;
}
function DeleteMultiple(ids) {
var actions = {
way: DeleteWay,
node: DeleteNode,
relation: DeleteRelation
};
var action = function(graph) {
ids.forEach(function(id) {
if (graph.hasEntity(id)) { // It may have been deleted aready.
graph = actions[graph.entity(id).type](id)(graph);
}
});
return graph;
};
action.disabled = function(graph) {
for (var i = 0; i < ids.length; i++) {
var id = ids[i],
disabled = actions[graph.entity(id).type](id).disabled(graph);
if (disabled) return disabled;
}
};
return action;
}
// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteRelationAction.as
function DeleteRelation(relationId) {
function deleteEntity(entity, graph) {
return !graph.parentWays(entity).length &&
!graph.parentRelations(entity).length &&
!entity.hasInterestingTags();
}
var action = function(graph) {
var relation = graph.entity(relationId);
graph.parentRelations(relation)
.forEach(function(parent) {
parent = parent.removeMembersWithID(relationId);
graph = graph.replace(parent);
if (parent.isDegenerate()) {
graph = DeleteRelation(parent.id)(graph);
}
});
_.uniq(_.map(relation.members, 'id')).forEach(function(memberId) {
graph = graph.replace(relation.removeMembersWithID(memberId));
var entity = graph.entity(memberId);
if (deleteEntity(entity, graph)) {
graph = DeleteMultiple([memberId])(graph);
}
});
return graph.remove(relation);
};
action.disabled = function(graph) {
if (!graph.entity(relationId).isComplete(graph))
return 'incomplete_relation';
};
return action;
}
// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteWayAction.as
function DeleteWay(wayId) {
function deleteNode(node, graph) {
return !graph.parentWays(node).length &&
!graph.parentRelations(node).length &&
!node.hasInterestingTags();
}
var action = function(graph) {
var way = graph.entity(wayId);
graph.parentRelations(way)
.forEach(function(parent) {
parent = parent.removeMembersWithID(wayId);
graph = graph.replace(parent);
if (parent.isDegenerate()) {
graph = DeleteRelation(parent.id)(graph);
}
});
_.uniq(way.nodes).forEach(function(nodeId) {
graph = graph.replace(way.removeNode(nodeId));
var node = graph.entity(nodeId);
if (deleteNode(node, graph)) {
graph = graph.remove(node);
}
});
return graph.remove(way);
};
action.disabled = function(graph) {
var disabled = false;
graph.parentRelations(graph.entity(wayId)).forEach(function(parent) {
var type = parent.tags.type,
role = parent.memberById(wayId).role || 'outer';
if (type === 'route' || type === 'boundary' || (type === 'multipolygon' && role === 'outer')) {
disabled = 'part_of_relation';
}
});
return disabled;
};
return action;
}
// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteNodeAction.as
function DeleteNode(nodeId) {
var action = function(graph) {
var node = graph.entity(nodeId);
graph.parentWays(node)
.forEach(function(parent) {
parent = parent.removeNode(nodeId);
graph = graph.replace(parent);
if (parent.isDegenerate()) {
graph = DeleteWay(parent.id)(graph);
}
});
graph.parentRelations(node)
.forEach(function(parent) {
parent = parent.removeMembersWithID(nodeId);
graph = graph.replace(parent);
if (parent.isDegenerate()) {
graph = DeleteRelation(parent.id)(graph);
}
});
return graph.remove(node);
};
action.disabled = function() {
return false;
};
return action;
}
// Connect the ways at the given nodes.
//
// The last node will survive. All other nodes will be replaced with
// the surviving node in parent ways, and then removed.
//
// Tags and relation memberships of of non-surviving nodes are merged
// to the survivor.
//
// This is the inverse of `iD.actions.Disconnect`.
//
// Reference:
// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeNodesAction.as
// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/MergeNodesAction.java
//
function Connect(nodeIds) {
return function(graph) {
var survivor = graph.entity(_.last(nodeIds));
for (var i = 0; i < nodeIds.length - 1; i++) {
var node = graph.entity(nodeIds[i]);
/* eslint-disable no-loop-func */
graph.parentWays(node).forEach(function(parent) {
if (!parent.areAdjacent(node.id, survivor.id)) {
graph = graph.replace(parent.replaceNode(node.id, survivor.id));
}
});
graph.parentRelations(node).forEach(function(parent) {
graph = graph.replace(parent.replaceMember(node, survivor));
});
/* eslint-enable no-loop-func */
survivor = survivor.mergeTags(node.tags);
graph = DeleteNode(node.id)(graph);
}
graph = graph.replace(survivor);
return graph;
};
}
function CopyEntities(ids, fromGraph) {
var copies = {};
var action = function(graph) {
ids.forEach(function(id) {
fromGraph.entity(id).copy(fromGraph, copies);
});
for (var id in copies) {
graph = graph.replace(copies[id]);
}
return graph;
};
action.copies = function() {
return copies;
};
return action;
}
function DeleteMember(relationId, memberIndex) {
return function(graph) {
var relation = graph.entity(relationId)
.removeMember(memberIndex);
graph = graph.replace(relation);
if (relation.isDegenerate())
graph = DeleteRelation(relation.id)(graph);
return graph;
};
}
function DeprecateTags(entityId) {
return function(graph) {
var entity = graph.entity(entityId),
newtags = _.clone(entity.tags),
change = false,
rule;
// This handles deprecated tags with a single condition
for (var i = 0; i < iD.data.deprecated.length; i++) {
rule = iD.data.deprecated[i];
var match = _.toPairs(rule.old)[0],
replacements = rule.replace ? _.toPairs(rule.replace) : null;
if (entity.tags[match[0]] && match[1] === '*') {
var value = entity.tags[match[0]];
if (replacements && !newtags[replacements[0][0]]) {
newtags[replacements[0][0]] = value;
}
delete newtags[match[0]];
change = true;
} else if (entity.tags[match[0]] === match[1]) {
newtags = _.assign({}, rule.replace || {}, _.omit(newtags, match[0]));
change = true;
}
}
if (change) {
return graph.replace(entity.update({tags: newtags}));
} else {
return graph;
}
};
}
function DiscardTags(difference) {
return function(graph) {
function discardTags(entity) {
if (!_.isEmpty(entity.tags)) {
var tags = {};
_.each(entity.tags, function(v, k) {
if (v) tags[k] = v;
});
graph = graph.replace(entity.update({
tags: _.omit(tags, iD.data.discarded)
}));
}
}
difference.modified().forEach(discardTags);
difference.created().forEach(discardTags);
return graph;
};
}
function DisconnectAction(nodeId, newNodeId) {
var wayIds;
var action = function(graph) {
var node = graph.entity(nodeId),
connections = action.connections(graph);
connections.forEach(function(connection) {
var way = graph.entity(connection.wayID),
newNode = Node({id: newNodeId, loc: node.loc, tags: node.tags});
graph = graph.replace(newNode);
if (connection.index === 0 && way.isArea()) {
// replace shared node with shared node..
graph = graph.replace(way.replaceNode(way.nodes[0], newNode.id));
} else {
// replace shared node with multiple new nodes..
graph = graph.replace(way.updateNode(newNode.id, connection.index));
}
});
return graph;
};
action.connections = function(graph) {
var candidates = [],
keeping = false,
parentWays = graph.parentWays(graph.entity(nodeId));
parentWays.forEach(function(way) {
if (wayIds && wayIds.indexOf(way.id) === -1) {
keeping = true;
return;
}
if (way.isArea() && (way.nodes[0] === nodeId)) {
candidates.push({wayID: way.id, index: 0});
} else {
way.nodes.forEach(function(waynode, index) {
if (waynode === nodeId) {
candidates.push({wayID: way.id, index: index});
}
});
}
});
return keeping ? candidates : candidates.slice(1);
};
action.disabled = function(graph) {
var connections = action.connections(graph);
if (connections.length === 0 || (wayIds && wayIds.length !== connections.length))
return 'not_connected';
var parentWays = graph.parentWays(graph.entity(nodeId)),
seenRelationIds = {},
sharedRelation;
parentWays.forEach(function(way) {
if (wayIds && wayIds.indexOf(way.id) === -1)
return;
var relations = graph.parentRelations(way);
relations.forEach(function(relation) {
if (relation.id in seenRelationIds) {
sharedRelation = relation;
} else {
seenRelationIds[relation.id] = true;
}
});
});
if (sharedRelation)
return 'relation';
};
action.limitWays = function(_) {
if (!arguments.length) return wayIds;
wayIds = _;
return action;
};
return action;
}
function Join(ids) {
function groupEntitiesByGeometry(graph) {
var entities = ids.map(function(id) { return graph.entity(id); });
return _.extend({line: []}, _.groupBy(entities, function(entity) { return entity.geometry(graph); }));
}
var action = function(graph) {
var ways = ids.map(graph.entity, graph),
survivor = ways[0];
// Prefer to keep an existing way.
for (var i = 0; i < ways.length; i++) {
if (!ways[i].isNew()) {
survivor = ways[i];
break;
}
}
var joined = joinWays(ways, graph)[0];
survivor = survivor.update({nodes: _.map(joined.nodes, 'id')});
graph = graph.replace(survivor);
joined.forEach(function(way) {
if (way.id === survivor.id)
return;
graph.parentRelations(way).forEach(function(parent) {
graph = graph.replace(parent.replaceMember(way, survivor));
});
survivor = survivor.mergeTags(way.tags);
graph = graph.replace(survivor);
graph = DeleteWay(way.id)(graph);
});
return graph;
};
action.disabled = function(graph) {
var geometries = groupEntitiesByGeometry(graph);
if (ids.length < 2 || ids.length !== geometries.line.length)
return 'not_eligible';
var joined = joinWays(ids.map(graph.entity, graph), graph);
if (joined.length > 1)
return 'not_adjacent';
var nodeIds = _.map(joined[0].nodes, 'id').slice(1, -1),
relation,
tags = {},
conflicting = false;
joined[0].forEach(function(way) {
var parents = graph.parentRelations(way);
parents.forEach(function(parent) {
if (parent.isRestriction() && parent.members.some(function(m) { return nodeIds.indexOf(m.id) >= 0; }))
relation = parent;
});
for (var k in way.tags) {
if (!(k in tags)) {
tags[k] = way.tags[k];
} else if (tags[k] && interestingTag(k) && tags[k] !== way.tags[k]) {
conflicting = true;
}
}
});
if (relation)
return 'restriction';
if (conflicting)
return 'conflicting_tags';
};
return action;
}
function MergeAction(ids) {
function groupEntitiesByGeometry(graph) {
var entities = ids.map(function(id) { return graph.entity(id); });
return _.extend({point: [], area: [], line: [], relation: []},
_.groupBy(entities, function(entity) { return entity.geometry(graph); }));
}
var action = function(graph) {
var geometries = groupEntitiesByGeometry(graph),
target = geometries.area[0] || geometries.line[0],
points = geometries.point;
points.forEach(function(point) {
target = target.mergeTags(point.tags);
graph.parentRelations(point).forEach(function(parent) {
graph = graph.replace(parent.replaceMember(point, target));
});
graph = graph.remove(point);
});
graph = graph.replace(target);
return graph;
};
action.disabled = function(graph) {
var geometries = groupEntitiesByGeometry(graph);
if (geometries.point.length === 0 ||
(geometries.area.length + geometries.line.length) !== 1 ||
geometries.relation.length !== 0)
return 'not_eligible';
};
return action;
}
function MergePolygon(ids, newRelationId) {
function groupEntities(graph) {
var entities = ids.map(function (id) { return graph.entity(id); });
return _.extend({
closedWay: [],
multipolygon: [],
other: []
}, _.groupBy(entities, function(entity) {
if (entity.type === 'way' && entity.isClosed()) {
return 'closedWay';
} else if (entity.type === 'relation' && entity.isMultipolygon()) {
return 'multipolygon';
} else {
return 'other';
}
}));
}
var action = function(graph) {
var entities = groupEntities(graph);
// An array representing all the polygons that are part of the multipolygon.
//
// Each element is itself an array of objects with an id property, and has a
// locs property which is an array of the locations forming the polygon.
var polygons = entities.multipolygon.reduce(function(polygons, m) {
return polygons.concat(joinWays(m.members, graph));
}, []).concat(entities.closedWay.map(function(d) {
var member = [{id: d.id}];
member.nodes = graph.childNodes(d);
return member;
}));
// contained is an array of arrays of boolean values,
// where contained[j][k] is true iff the jth way is
// contained by the kth way.
var contained = polygons.map(function(w, i) {
return polygons.map(function(d, n) {
if (i === n) return null;
return polygonContainsPolygon(
_.map(d.nodes, 'loc'),
_.map(w.nodes, 'loc'));
});
});
// Sort all polygons as either outer or inner ways
var members = [],
outer = true;
while (polygons.length) {
extractUncontained(polygons);
polygons = polygons.filter(isContained);
contained = contained.filter(isContained).map(filterContained);
}
function isContained(d, i) {
return _.some(contained[i]);
}
function filterContained(d) {
return d.filter(isContained);
}
function extractUncontained(polygons) {
polygons.forEach(function(d, i) {
if (!isContained(d, i)) {
d.forEach(function(member) {
members.push({
type: 'way',
id: member.id,
role: outer ? 'outer' : 'inner'
});
});
}
});
outer = !outer;
}
// Move all tags to one relation
var relation = entities.multipolygon[0] ||
Relation({ id: newRelationId, tags: { type: 'multipolygon' }});
entities.multipolygon.slice(1).forEach(function(m) {
relation = relation.mergeTags(m.tags);
graph = graph.remove(m);
});
entities.closedWay.forEach(function(way) {
function isThisOuter(m) {
return m.id === way.id && m.role !== 'inner';
}
if (members.some(isThisOuter)) {
relation = relation.mergeTags(way.tags);
graph = graph.replace(way.update({ tags: {} }));
}
});
return graph.replace(relation.update({
members: members,
tags: _.omit(relation.tags, 'area')
}));
};
action.disabled = function(graph) {
var entities = groupEntities(graph);
if (entities.other.length > 0 ||
entities.closedWay.length + entities.multipolygon.length < 2)
return 'not_eligible';
if (!entities.multipolygon.every(function(r) { return r.isComplete(graph); }))
return 'incomplete_relation';
};
return action;
}
function MergeRemoteChanges(id, localGraph, remoteGraph, formatUser) {
var option = 'safe', // 'safe', 'force_local', 'force_remote'
conflicts = [];
function user(d) {
return _.isFunction(formatUser) ? formatUser(d) : d;
}
function mergeLocation(remote, target) {
function pointEqual(a, b) {
var epsilon = 1e-6;
return (Math.abs(a[0] - b[0]) < epsilon) && (Math.abs(a[1] - b[1]) < epsilon);
}
if (option === 'force_local' || pointEqual(target.loc, remote.loc)) {
return target;
}
if (option === 'force_remote') {
return target.update({loc: remote.loc});
}
conflicts.push(t('merge_remote_changes.conflict.location', { user: user(remote.user) }));
return target;
}
function mergeNodes(base, remote, target) {
if (option === 'force_local' || _.isEqual(target.nodes, remote.nodes)) {
return target;
}
if (option === 'force_remote') {
return target.update({nodes: remote.nodes});
}
var ccount = conflicts.length,
o = base.nodes || [],
a = target.nodes || [],
b = remote.nodes || [],
nodes = [],
hunks = Diff3.diff3_merge(a, o, b, true);
for (var i = 0; i < hunks.length; i++) {
var hunk = hunks[i];
if (hunk.ok) {
nodes.push.apply(nodes, hunk.ok);
} else {
// for all conflicts, we can assume c.a !== c.b
// because `diff3_merge` called with `true` option to exclude false conflicts..
var c = hunk.conflict;
if (_.isEqual(c.o, c.a)) { // only changed remotely
nodes.push.apply(nodes, c.b);
} else if (_.isEqual(c.o, c.b)) { // only changed locally
nodes.push.apply(nodes, c.a);
} else { // changed both locally and remotely
conflicts.push(t('merge_remote_changes.conflict.nodelist', { user: user(remote.user) }));
break;
}
}
}
return (conflicts.length === ccount) ? target.update({nodes: nodes}) : target;
}
function mergeChildren(targetWay, children, updates, graph) {
function isUsed(node, targetWay) {
var parentWays = _.map(graph.parentWays(node), 'id');
return node.hasInterestingTags() ||
_.without(parentWays, targetWay.id).length > 0 ||
graph.parentRelations(node).length > 0;
}
var ccount = conflicts.length;
for (var i = 0; i < children.length; i++) {
var id = children[i],
node = graph.hasEntity(id);
// remove unused childNodes..
if (targetWay.nodes.indexOf(id) === -1) {
if (node && !isUsed(node, targetWay)) {
updates.removeIds.push(id);
}
continue;
}
// restore used childNodes..
var local = localGraph.hasEntity(id),
remote = remoteGraph.hasEntity(id),
target;
if (option === 'force_remote' && remote && remote.visible) {
updates.replacements.push(remote);
} else if (option === 'force_local' && local) {
target = Entity(local);
if (remote) {
target = target.update({ version: remote.version });
}
updates.replacements.push(target);
} else if (option === 'safe' && local && remote && local.version !== remote.version) {
target = Entity(local, { version: remote.version });
if (remote.visible) {
target = mergeLocation(remote, target);
} else {
conflicts.push(t('merge_remote_changes.conflict.deleted', { user: user(remote.user) }));
}
if (conflicts.length !== ccount) break;
updates.replacements.push(target);
}
}
return targetWay;
}
function updateChildren(updates, graph) {
for (var i = 0; i < updates.replacements.length; i++) {
graph = graph.replace(updates.replacements[i]);
}
if (updates.removeIds.length) {
graph = DeleteMultiple(updates.removeIds)(graph);
}
return graph;
}
function mergeMembers(remote, target) {
if (option === 'force_local' || _.isEqual(target.members, remote.members)) {
return target;
}
if (option === 'force_remote') {
return target.update({members: remote.members});
}
conflicts.push(t('merge_remote_changes.conflict.memberlist', { user: user(remote.user) }));
return target;
}
function mergeTags(base, remote, target) {
function ignoreKey(k) {
return _.includes(iD.data.discarded, k);
}
if (option === 'force_local' || _.isEqual(target.tags, remote.tags)) {
return target;
}
if (option === 'force_remote') {
return target.update({tags: remote.tags});
}
var ccount = conflicts.length,
o = base.tags || {},
a = target.tags || {},
b = remote.tags || {},
keys = _.reject(_.union(_.keys(o), _.keys(a), _.keys(b)), ignoreKey),
tags = _.clone(a),
changed = false;
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
if (o[k] !== b[k] && a[k] !== b[k]) { // changed remotely..
if (o[k] !== a[k]) { // changed locally..
conflicts.push(t('merge_remote_changes.conflict.tags',
{ tag: k, local: a[k], remote: b[k], user: user(remote.user) }));
} else { // unchanged locally, accept remote change..
if (b.hasOwnProperty(k)) {
tags[k] = b[k];
} else {
delete tags[k];
}
changed = true;
}
}
}
return (changed && conflicts.length === ccount) ? target.update({tags: tags}) : target;
}
// `graph.base()` is the common ancestor of the two graphs.
// `localGraph` contains user's edits up to saving
// `remoteGraph` contains remote edits to modified nodes
// `graph` must be a descendent of `localGraph` and may include
// some conflict resolution actions performed on it.
//
// --- ... --- `localGraph` -- ... -- `graph`
// /
// `graph.base()` --- ... --- `remoteGraph`
//
var action = function(graph) {
var updates = { replacements: [], removeIds: [] },
base = graph.base().entities[id],
local = localGraph.entity(id),
remote = remoteGraph.entity(id),
target = Entity(local, { version: remote.version });
// delete/undelete
if (!remote.visible) {
if (option === 'force_remote') {
return DeleteMultiple([id])(graph);
} else if (option === 'force_local') {
if (target.type === 'way') {
target = mergeChildren(target, _.uniq(local.nodes), updates, graph);
graph = updateChildren(updates, graph);
}
return graph.replace(target);
} else {
conflicts.push(t('merge_remote_changes.conflict.deleted', { user: user(remote.user) }));
return graph; // do nothing
}
}
// merge
if (target.type === 'node') {
target = mergeLocation(remote, target);
} else if (target.type === 'way') {
// pull in any child nodes that may not be present locally..
graph.rebase(remoteGraph.childNodes(remote), [graph], false);
target = mergeNodes(base, remote, target);
target = mergeChildren(target, _.union(local.nodes, remote.nodes), updates, graph);
} else if (target.type === 'relation') {
target = mergeMembers(remote, target);
}
target = mergeTags(base, remote, target);
if (!conflicts.length) {
graph = updateChildren(updates, graph).replace(target);
}
return graph;
};
action.withOption = function(opt) {
option = opt;
return action;
};
action.conflicts = function() {
return conflicts;
};
return action;
}
function MoveAction(moveIds, tryDelta, projection, cache) {
var delta = tryDelta;
function vecAdd(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
function setupCache(graph) {
function canMove(nodeId) {
var parents = _.map(graph.parentWays(graph.entity(nodeId)), 'id');
if (parents.length < 3) return true;
// Don't move a vertex where >2 ways meet, unless all parentWays are moving too..
var parentsMoving = _.all(parents, function(id) { return cache.moving[id]; });
if (!parentsMoving) delete cache.moving[nodeId];
return parentsMoving;
}
function cacheEntities(ids) {
_.each(ids, function(id) {
if (cache.moving[id]) return;
cache.moving[id] = true;
var entity = graph.hasEntity(id);
if (!entity) return;
if (entity.type === 'node') {
cache.nodes.push(id);
cache.startLoc[id] = entity.loc;
} else if (entity.type === 'way') {
cache.ways.push(id);
cacheEntities(entity.nodes);
} else {
cacheEntities(_.map(entity.members, 'id'));
}
});
}
function cacheIntersections(ids) {
function isEndpoint(way, id) { return !way.isClosed() && !!way.affix(id); }
_.each(ids, function(id) {
// consider only intersections with 1 moved and 1 unmoved way.
_.each(graph.childNodes(graph.entity(id)), function(node) {
var parents = graph.parentWays(node);
if (parents.length !== 2) return;
var moved = graph.entity(id),
unmoved = _.find(parents, function(way) { return !cache.moving[way.id]; });
if (!unmoved) return;
// exclude ways that are overly connected..
if (_.intersection(moved.nodes, unmoved.nodes).length > 2) return;
if (moved.isArea() || unmoved.isArea()) return;
cache.intersection[node.id] = {
nodeId: node.id,
movedId: moved.id,
unmovedId: unmoved.id,
movedIsEP: isEndpoint(moved, node.id),
unmovedIsEP: isEndpoint(unmoved, node.id)
};
});
});
}
if (!cache) {
cache = {};
}
if (!cache.ok) {
cache.moving = {};
cache.intersection = {};
cache.replacedVertex = {};
cache.startLoc = {};
cache.nodes = [];
cache.ways = [];
cacheEntities(moveIds);
cacheIntersections(cache.ways);
cache.nodes = _.filter(cache.nodes, canMove);
cache.ok = true;
}
}
// Place a vertex where the moved vertex used to be, to preserve way shape..
function replaceMovedVertex(nodeId, wayId, graph, delta) {
var way = graph.entity(wayId),
moved = graph.entity(nodeId),
movedIndex = way.nodes.indexOf(nodeId),
len, prevIndex, nextIndex;
if (way.isClosed()) {
len = way.nodes.length - 1;
prevIndex = (movedIndex + len - 1) % len;
nextIndex = (movedIndex + len + 1) % len;
} else {
len = way.nodes.length;
prevIndex = movedIndex - 1;
nextIndex = movedIndex + 1;
}
var prev = graph.hasEntity(way.nodes[prevIndex]),
next = graph.hasEntity(way.nodes[nextIndex]);
// Don't add orig vertex at endpoint..
if (!prev || !next) return graph;
var key = wayId + '_' + nodeId,
orig = cache.replacedVertex[key];
if (!orig) {
orig = Node();
cache.replacedVertex[key] = orig;
cache.startLoc[orig.id] = cache.startLoc[nodeId];
}
var start, end;
if (delta) {
start = projection(cache.startLoc[nodeId]);
end = projection.invert(vecAdd(start, delta));
} else {
end = cache.startLoc[nodeId];
}
orig = orig.move(end);
var angle = Math.abs(getAngle(orig, prev, projection) -
getAngle(orig, next, projection)) * 180 / Math.PI;
// Don't add orig vertex if it would just make a straight line..
if (angle > 175 && angle < 185) return graph;
// Don't add orig vertex if another point is already nearby (within 10m)
if (sphericalDistance(prev.loc, orig.loc) < 10 ||
sphericalDistance(orig.loc, next.loc) < 10) return graph;
// moving forward or backward along way?
var p1 = [prev.loc, orig.loc, moved.loc, next.loc].map(projection),
p2 = [prev.loc, moved.loc, orig.loc, next.loc].map(projection),
d1 = pathLength(p1),
d2 = pathLength(p2),
insertAt = (d1 < d2) ? movedIndex : nextIndex;
// moving around closed loop?
if (way.isClosed() && insertAt === 0) insertAt = len;
way = way.addNode(orig.id, insertAt);
return graph.replace(orig).replace(way);
}
// Reorder nodes around intersections that have moved..
function unZorroIntersection(intersection, graph) {
var vertex = graph.entity(intersection.nodeId),
way1 = graph.entity(intersection.movedId),
way2 = graph.entity(intersection.unmovedId),
isEP1 = intersection.movedIsEP,
isEP2 = intersection.unmovedIsEP;
// don't move the vertex if it is the endpoint of both ways.
if (isEP1 && isEP2) return graph;
var nodes1 = _.without(graph.childNodes(way1), vertex),
nodes2 = _.without(graph.childNodes(way2), vertex);
if (way1.isClosed() && way1.first() === vertex.id) nodes1.push(nodes1[0]);
if (way2.isClosed() && way2.first() === vertex.id) nodes2.push(nodes2[0]);
var edge1 = !isEP1 && chooseEdge(nodes1, projection(vertex.loc), projection),
edge2 = !isEP2 && chooseEdge(nodes2, projection(vertex.loc), projection),
loc;
// snap vertex to nearest edge (or some point between them)..
if (!isEP1 && !isEP2) {
var epsilon = 1e-4, maxIter = 10;
for (var i = 0; i < maxIter; i++) {
loc = interp(edge1.loc, edge2.loc, 0.5);
edge1 = chooseEdge(nodes1, projection(loc), projection);
edge2 = chooseEdge(nodes2, projection(loc), projection);
if (Math.abs(edge1.distance - edge2.distance) < epsilon) break;
}
} else if (!isEP1) {
loc = edge1.loc;
} else {
loc = edge2.loc;
}
graph = graph.replace(vertex.move(loc));
// if zorro happened, reorder nodes..
if (!isEP1 && edge1.index !== way1.nodes.indexOf(vertex.id)) {
way1 = way1.removeNode(vertex.id).addNode(vertex.id, edge1.index);
graph = graph.replace(way1);
}
if (!isEP2 && edge2.index !== way2.nodes.indexOf(vertex.id)) {
way2 = way2.removeNode(vertex.id).addNode(vertex.id, edge2.index);
graph = graph.replace(way2);
}
return graph;
}
function cleanupIntersections(graph) {
_.each(cache.intersection, function(obj) {
graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, delta);
graph = replaceMovedVertex(obj.nodeId, obj.unmovedId, graph, null);
graph = unZorroIntersection(obj, graph);
});
return graph;
}
// check if moving way endpoint can cross an unmoved way, if so limit delta..
function limitDelta(graph) {
_.each(cache.intersection, function(obj) {
if (!obj.movedIsEP) return;
var node = graph.entity(obj.nodeId),
start = projection(node.loc),
end = vecAdd(start, delta),
movedNodes = graph.childNodes(graph.entity(obj.movedId)),
movedPath = _.map(_.map(movedNodes, 'loc'),
function(loc) { return vecAdd(projection(loc), delta); }),
unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId)),
unmovedPath = _.map(_.map(unmovedNodes, 'loc'), projection),
hits = pathIntersections(movedPath, unmovedPath);
for (var i = 0; i < hits.length; i++) {
if (_.isEqual(hits[i], end)) continue;
var edge = chooseEdge(unmovedNodes, end, projection);
delta = vecSub(projection(edge.loc), start);
}
});
}
var action = function(graph) {
if (delta[0] === 0 && delta[1] === 0) return graph;
setupCache(graph);
if (!_.isEmpty(cache.intersection)) {
limitDelta(graph);
}
_.each(cache.nodes, function(id) {
var node = graph.entity(id),
start = projection(node.loc),
end = vecAdd(start, delta);
graph = graph.replace(node.move(projection.invert(end)));
});
if (!_.isEmpty(cache.intersection)) {
graph = cleanupIntersections(graph);
}
return graph;
};
action.disabled = function(graph) {
function incompleteRelation(id) {
var entity = graph.entity(id);
return entity.type === 'relation' && !entity.isComplete(graph);
}
if (_.some(moveIds, incompleteRelation))
return 'incomplete_relation';
};
action.delta = function() {
return delta;
};
return action;
}
// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java
// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as
function MoveNode(nodeId, loc) {
return function(graph) {
return graph.replace(graph.entity(nodeId).move(loc));
};
}
function Noop() {
return function(graph) {
return graph;
};
}
/*
* Based on https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/potlatch2/tools/Quadrilateralise.as
*/
function OrthogonalizeAction(wayId, projection) {
var threshold = 12, // degrees within right or straight to alter
lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180),
upperThreshold = Math.cos(threshold * Math.PI / 180);
var action = function(graph) {
var way = graph.entity(wayId),
nodes = graph.childNodes(way),
points = _.uniq(nodes).map(function(n) { return projection(n.loc); }),
corner = {i: 0, dotp: 1},
epsilon = 1e-4,
i, j, score, motions;
if (nodes.length === 4) {
for (i = 0; i < 1000; i++) {
motions = points.map(calcMotion);
points[corner.i] = addPoints(points[corner.i],motions[corner.i]);
score = corner.dotp;
if (score < epsilon) {
break;
}
}
graph = graph.replace(graph.entity(nodes[corner.i].id)
.move(projection.invert(points[corner.i])));
} else {
var best,
originalPoints = _.clone(points);
score = Infinity;
for (i = 0; i < 1000; i++) {
motions = points.map(calcMotion);
for (j = 0; j < motions.length; j++) {
points[j] = addPoints(points[j],motions[j]);
}
var newScore = squareness(points);
if (newScore < score) {
best = _.clone(points);
score = newScore;
}
if (score < epsilon) {
break;
}
}
points = best;
for (i = 0; i < points.length; i++) {
// only move the points that actually moved
if (originalPoints[i][0] !== points[i][0] || originalPoints[i][1] !== points[i][1]) {
graph = graph.replace(graph.entity(nodes[i].id)
.move(projection.invert(points[i])));
}
}
// remove empty nodes on straight sections
for (i = 0; i < points.length; i++) {
var node = nodes[i];
if (graph.parentWays(node).length > 1 ||
graph.parentRelations(node).length ||
node.hasInterestingTags()) {
continue;
}
var dotp = normalizedDotProduct(i, points);
if (dotp < -1 + epsilon) {
graph = DeleteNode(nodes[i].id)(graph);
}
}
}
return graph;
function calcMotion(b, i, array) {
var a = array[(i - 1 + array.length) % array.length],
c = array[(i + 1) % array.length],
p = subtractPoints(a, b),
q = subtractPoints(c, b),
scale, dotp;
scale = 2 * Math.min(euclideanDistance(p, [0, 0]), euclideanDistance(q, [0, 0]));
p = normalizePoint(p, 1.0);
q = normalizePoint(q, 1.0);
dotp = filterDotProduct(p[0] * q[0] + p[1] * q[1]);
// nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270).
if (array.length > 3) {
if (dotp < -0.707106781186547) {
dotp += 1.0;
}
} else if (dotp && Math.abs(dotp) < corner.dotp) {
corner.i = i;
corner.dotp = Math.abs(dotp);
}
return normalizePoint(addPoints(p, q), 0.1 * dotp * scale);
}
};
function squareness(points) {
return points.reduce(function(sum, val, i, array) {
var dotp = normalizedDotProduct(i, array);
dotp = filterDotProduct(dotp);
return sum + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)));
}, 0);
}
function normalizedDotProduct(i, points) {
var a = points[(i - 1 + points.length) % points.length],
b = points[i],
c = points[(i + 1) % points.length],
p = subtractPoints(a, b),
q = subtractPoints(c, b);
p = normalizePoint(p, 1.0);
q = normalizePoint(q, 1.0);
return p[0] * q[0] + p[1] * q[1];
}
function subtractPoints(a, b) {
return [a[0] - b[0], a[1] - b[1]];
}
function addPoints(a, b) {
return [a[0] + b[0], a[1] + b[1]];
}
function normalizePoint(point, scale) {
var vector = [0, 0];
var length = Math.sqrt(point[0] * point[0] + point[1] * point[1]);
if (length !== 0) {
vector[0] = point[0] / length;
vector[1] = point[1] / length;
}
vector[0] *= scale;
vector[1] *= scale;
return vector;
}
function filterDotProduct(dotp) {
if (lowerThreshold > Math.abs(dotp) || Math.abs(dotp) > upperThreshold) {
return dotp;
}
return 0;
}
action.disabled = function(graph) {
var way = graph.entity(wayId),
nodes = graph.childNodes(way),
points = _.uniq(nodes).map(function(n) { return projection(n.loc); });
if (squareness(points)) {
return false;
}
return 'not_squarish';
};
return action;
}
// Split a way at the given node.
//
// Optionally, split only the given ways, if multiple ways share
// the given node.
//
// This is the inverse of `iD.actions.Join`.
//
// For testing convenience, accepts an ID to assign to the new way.
// Normally, this will be undefined and the way will automatically
// be assigned a new ID.
//
// Reference:
// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as
//
function SplitAction(nodeId, newWayIds) {
var wayIds;
// if the way is closed, we need to search for a partner node
// to split the way at.
//
// The following looks for a node that is both far away from
// the initial node in terms of way segment length and nearby
// in terms of beeline-distance. This assures that areas get
// split on the most "natural" points (independent of the number
// of nodes).
// For example: bone-shaped areas get split across their waist
// line, circles across the diameter.
function splitArea(nodes, idxA, graph) {
var lengths = new Array(nodes.length),
length,
i,
best = 0,
idxB;
function wrap(index) {
return Wrap(index, nodes.length);
}
function dist(nA, nB) {
return sphericalDistance(graph.entity(nA).loc, graph.entity(nB).loc);
}
// calculate lengths
length = 0;
for (i = wrap(idxA+1); i !== idxA; i = wrap(i+1)) {
length += dist(nodes[i], nodes[wrap(i-1)]);
lengths[i] = length;
}
length = 0;
for (i = wrap(idxA-1); i !== idxA; i = wrap(i-1)) {
length += dist(nodes[i], nodes[wrap(i+1)]);
if (length < lengths[i])
lengths[i] = length;
}
// determine best opposite node to split
for (i = 0; i < nodes.length; i++) {
var cost = lengths[i] / dist(nodes[idxA], nodes[i]);
if (cost > best) {
idxB = i;
best = cost;
}
}
return idxB;
}
function split(graph, wayA, newWayId) {
var wayB = Way({id: newWayId, tags: wayA.tags}),
nodesA,
nodesB,
isArea = wayA.isArea(),
isOuter = isSimpleMultipolygonOuterMember(wayA, graph);
if (wayA.isClosed()) {
var nodes = wayA.nodes.slice(0, -1),
idxA = _.indexOf(nodes, nodeId),
idxB = splitArea(nodes, idxA, graph);
if (idxB < idxA) {
nodesA = nodes.slice(idxA).concat(nodes.slice(0, idxB + 1));
nodesB = nodes.slice(idxB, idxA + 1);
} else {
nodesA = nodes.slice(idxA, idxB + 1);
nodesB = nodes.slice(idxB).concat(nodes.slice(0, idxA + 1));
}
} else {
var idx = _.indexOf(wayA.nodes, nodeId, 1);
nodesA = wayA.nodes.slice(0, idx + 1);
nodesB = wayA.nodes.slice(idx);
}
wayA = wayA.update({nodes: nodesA});
wayB = wayB.update({nodes: nodesB});
graph = graph.replace(wayA);
graph = graph.replace(wayB);
graph.parentRelations(wayA).forEach(function(relation) {
if (relation.isRestriction()) {
var via = relation.memberByRole('via');
if (via && wayB.contains(via.id)) {
relation = relation.updateMember({id: wayB.id}, relation.memberById(wayA.id).index);
graph = graph.replace(relation);
}
} else {
if (relation === isOuter) {
graph = graph.replace(relation.mergeTags(wayA.tags));
graph = graph.replace(wayA.update({tags: {}}));
graph = graph.replace(wayB.update({tags: {}}));
}
var member = {
id: wayB.id,
type: 'way',
role: relation.memberById(wayA.id).role
};
graph = AddMember(relation.id, member)(graph);
}
});
if (!isOuter && isArea) {
var multipolygon = Relation({
tags: _.extend({}, wayA.tags, {type: 'multipolygon'}),
members: [
{id: wayA.id, role: 'outer', type: 'way'},
{id: wayB.id, role: 'outer', type: 'way'}
]});
graph = graph.replace(multipolygon);
graph = graph.replace(wayA.update({tags: {}}));
graph = graph.replace(wayB.update({tags: {}}));
}
return graph;
}
var action = function(graph) {
var candidates = action.ways(graph);
for (var i = 0; i < candidates.length; i++) {
graph = split(graph, candidates[i], newWayIds && newWayIds[i]);
}
return graph;
};
action.ways = function(graph) {
var node = graph.entity(nodeId),
parents = graph.parentWays(node),
hasLines = _.some(parents, function(parent) { return parent.geometry(graph) === 'line'; });
return parents.filter(function(parent) {
if (wayIds && wayIds.indexOf(parent.id) === -1)
return false;
if (!wayIds && hasLines && parent.geometry(graph) !== 'line')
return false;
if (parent.isClosed()) {
return true;
}
for (var i = 1; i < parent.nodes.length - 1; i++) {
if (parent.nodes[i] === nodeId) {
return true;
}
}
return false;
});
};
action.disabled = function(graph) {
var candidates = action.ways(graph);
if (candidates.length === 0 || (wayIds && wayIds.length !== candidates.length))
return 'not_eligible';
};
action.limitWays = function(_) {
if (!arguments.length) return wayIds;
wayIds = _;
return action;
};
return action;
}
// Create a restriction relation for `turn`, which must have the following structure:
//
// {
// from: { node: <node ID>, way: <way ID> },
// via: { node: <node ID> },
// to: { node: <node ID>, way: <way ID> },
// restriction: <'no_right_turn', 'no_left_turn', etc.>
// }
//
// This specifies a restriction of type `restriction` when traveling from
// `from.node` in `from.way` toward `to.node` in `to.way` via `via.node`.
// (The action does not check that these entities form a valid intersection.)
//
// If `restriction` is not provided, it is automatically determined by
// inferRestriction.
//
// If necessary, the `from` and `to` ways are split. In these cases, `from.node`
// and `to.node` are used to determine which portion of the split ways become
// members of the restriction.
//
// For testing convenience, accepts an ID to assign to the new relation.
// Normally, this will be undefined and the relation will automatically
// be assigned a new ID.
//
function RestrictTurn(turn, projection, restrictionId) {
return function(graph) {
var from = graph.entity(turn.from.way),
via = graph.entity(turn.via.node),
to = graph.entity(turn.to.way);
function isClosingNode(way, nodeId) {
return nodeId === way.first() && nodeId === way.last();
}
function split(toOrFrom) {
var newID = toOrFrom.newID || Way().id;
graph = SplitAction(via.id, [newID])
.limitWays([toOrFrom.way])(graph);
var a = graph.entity(newID),
b = graph.entity(toOrFrom.way);
if (a.nodes.indexOf(toOrFrom.node) !== -1) {
return [a, b];
} else {
return [b, a];
}
}
if (!from.affix(via.id) || isClosingNode(from, via.id)) {
if (turn.from.node === turn.to.node) {
// U-turn
from = to = split(turn.from)[0];
} else if (turn.from.way === turn.to.way) {
// Straight-on or circular
var s = split(turn.from);
from = s[0];
to = s[1];
} else {
// Other
from = split(turn.from)[0];
}
}
if (!to.affix(via.id) || isClosingNode(to, via.id)) {
to = split(turn.to)[0];
}
return graph.replace(Relation({
id: restrictionId,
tags: {
type: 'restriction',
restriction: turn.restriction ||
inferRestriction(
graph,
turn.from,
turn.via,
turn.to,
projection)
},
members: [
{id: from.id, type: 'way', role: 'from'},
{id: via.id, type: 'node', role: 'via'},
{id: to.id, type: 'way', role: 'to'}
]
}));
};
}
/*
Order the nodes of a way in reverse order and reverse any direction dependent tags
other than `oneway`. (We assume that correcting a backwards oneway is the primary
reason for reversing a way.)
The following transforms are performed:
Keys:
*:right=* ⟺ *:left=*
*:forward=* ⟺ *:backward=*
direction=up ⟺ direction=down
incline=up ⟺ incline=down
*=right ⟺ *=left
Relation members:
role=forward ⟺ role=backward
role=north ⟺ role=south
role=east ⟺ role=west
In addition, numeric-valued `incline` tags are negated.
The JOSM implementation was used as a guide, but transformations that were of unclear benefit
or adjusted tags that don't seem to be used in practice were omitted.
References:
http://wiki.openstreetmap.org/wiki/Forward_%26_backward,_left_%26_right
http://wiki.openstreetmap.org/wiki/Key:direction#Steps
http://wiki.openstreetmap.org/wiki/Key:incline
http://wiki.openstreetmap.org/wiki/Route#Members
http://josm.openstreetmap.de/browser/josm/trunk/src/org/openstreetmap/josm/corrector/ReverseWayTagCorrector.java
*/
function ReverseAction(wayId, options) {
var replacements = [
[/:right$/, ':left'], [/:left$/, ':right'],
[/:forward$/, ':backward'], [/:backward$/, ':forward']
],
numeric = /^([+\-]?)(?=[\d.])/,
roleReversals = {
forward: 'backward',
backward: 'forward',
north: 'south',
south: 'north',
east: 'west',
west: 'east'
};
function reverseKey(key) {
for (var i = 0; i < replacements.length; ++i) {
var replacement = replacements[i];
if (replacement[0].test(key)) {
return key.replace(replacement[0], replacement[1]);
}
}
return key;
}
function reverseValue(key, value) {
if (key === 'incline' && numeric.test(value)) {
return value.replace(numeric, function(_, sign) { return sign === '-' ? '' : '-'; });
} else if (key === 'incline' || key === 'direction') {
return {up: 'down', down: 'up'}[value] || value;
} else if (options && options.reverseOneway && key === 'oneway') {
return {yes: '-1', '1': '-1', '-1': 'yes'}[value] || value;
} else {
return {left: 'right', right: 'left'}[value] || value;
}
}
return function(graph) {
var way = graph.entity(wayId),
nodes = way.nodes.slice().reverse(),
tags = {}, key, role;
for (key in way.tags) {
tags[reverseKey(key)] = reverseValue(key, way.tags[key]);
}
graph.parentRelations(way).forEach(function(relation) {
relation.members.forEach(function(member, index) {
if (member.id === way.id && (role = roleReversals[member.role])) {
relation = relation.updateMember({role: role}, index);
graph = graph.replace(relation);
}
});
});
return graph.replace(way.update({nodes: nodes, tags: tags}));
};
}
function Revert(id) {
var action = function(graph) {
var entity = graph.hasEntity(id),
base = graph.base().entities[id];
if (entity && !base) { // entity will be removed..
if (entity.type === 'node') {
graph.parentWays(entity)
.forEach(function(parent) {
parent = parent.removeNode(id);
graph = graph.replace(parent);
if (parent.isDegenerate()) {
graph = DeleteWay(parent.id)(graph);
}
});
}
graph.parentRelations(entity)
.forEach(function(parent) {
parent = parent.removeMembersWithID(id);
graph = graph.replace(parent);
if (parent.isDegenerate()) {
graph = DeleteRelation(parent.id)(graph);
}
});
}
return graph.revert(id);
};
return action;
}
function RotateWayAction(wayId, pivot, angle, projection) {
return function(graph) {
return graph.update(function(graph) {
var way = graph.entity(wayId);
_.uniq(way.nodes).forEach(function(id) {
var node = graph.entity(id),
point = projection(node.loc),
radial = [0,0];
radial[0] = point[0] - pivot[0];
radial[1] = point[1] - pivot[1];
point = [
radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + pivot[0],
radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + pivot[1]
];
graph = graph.replace(node.move(projection.invert(point)));
});
});
};
}
/*
* Based on https://github.com/openstreetmap/potlatch2/net/systemeD/potlatch2/tools/Straighten.as
*/
function StraightenAction(wayId, projection) {
function positionAlongWay(n, s, e) {
return ((n[0] - s[0]) * (e[0] - s[0]) + (n[1] - s[1]) * (e[1] - s[1]))/
(Math.pow(e[0] - s[0], 2) + Math.pow(e[1] - s[1], 2));
}
var action = function(graph) {
var way = graph.entity(wayId),
nodes = graph.childNodes(way),
points = nodes.map(function(n) { return projection(n.loc); }),
startPoint = points[0],
endPoint = points[points.length-1],
toDelete = [],
i;
for (i = 1; i < points.length-1; i++) {
var node = nodes[i],
point = points[i];
if (graph.parentWays(node).length > 1 ||
graph.parentRelations(node).length ||
node.hasInterestingTags()) {
var u = positionAlongWay(point, startPoint, endPoint),
p0 = startPoint[0] + u * (endPoint[0] - startPoint[0]),
p1 = startPoint[1] + u * (endPoint[1] - startPoint[1]);
graph = graph.replace(graph.entity(node.id)
.move(projection.invert([p0, p1])));
} else {
// safe to delete
if (toDelete.indexOf(node) === -1) {
toDelete.push(node);
}
}
}
for (i = 0; i < toDelete.length; i++) {
graph = DeleteNode(toDelete[i].id)(graph);
}
return graph;
};
action.disabled = function(graph) {
// check way isn't too bendy
var way = graph.entity(wayId),
nodes = graph.childNodes(way),
points = nodes.map(function(n) { return projection(n.loc); }),
startPoint = points[0],
endPoint = points[points.length-1],
threshold = 0.2 * Math.sqrt(Math.pow(startPoint[0] - endPoint[0], 2) + Math.pow(startPoint[1] - endPoint[1], 2)),
i;
if (threshold === 0) {
return 'too_bendy';
}
for (i = 1; i < points.length-1; i++) {
var point = points[i],
u = positionAlongWay(point, startPoint, endPoint),
p0 = startPoint[0] + u * (endPoint[0] - startPoint[0]),
p1 = startPoint[1] + u * (endPoint[1] - startPoint[1]),
dist = Math.sqrt(Math.pow(p0 - point[0], 2) + Math.pow(p1 - point[1], 2));
// to bendy if point is off by 20% of total start/end distance in projected space
if (isNaN(dist) || dist > threshold) {
return 'too_bendy';
}
}
};
return action;
}
// Remove the effects of `turn.restriction` on `turn`, which must have the
// following structure:
//
// {
// from: { node: <node ID>, way: <way ID> },
// via: { node: <node ID> },
// to: { node: <node ID>, way: <way ID> },
// restriction: <relation ID>
// }
//
// In the simple case, `restriction` is a reference to a `no_*` restriction
// on the turn itself. In this case, it is simply deleted.
//
// The more complex case is where `restriction` references an `only_*`
// restriction on a different turn in the same intersection. In that case,
// that restriction is also deleted, but at the same time restrictions on
// the turns other than the first two are created.
//
function UnrestrictTurn(turn) {
return function(graph) {
return DeleteRelation(turn.restriction)(graph);
};
}
var actions = Object.freeze({
AddEntity: AddEntity,
AddMember: AddMember,
AddMidpoint: AddMidpoint,
AddVertex: AddVertex,
ChangeMember: ChangeMember,
ChangePreset: ChangePreset,
ChangeTags: ChangeTags,
Circularize: CircularizeAction,
Connect: Connect,
CopyEntities: CopyEntities,
DeleteMember: DeleteMember,
DeleteMultiple: DeleteMultiple,
DeleteNode: DeleteNode,
DeleteRelation: DeleteRelation,
DeleteWay: DeleteWay,
DeprecateTags: DeprecateTags,
DiscardTags: DiscardTags,
Disconnect: DisconnectAction,
Join: Join,
Merge: MergeAction,
MergePolygon: MergePolygon,
MergeRemoteChanges: MergeRemoteChanges,
Move: MoveAction,
MoveNode: MoveNode,
Noop: Noop,
Orthogonalize: OrthogonalizeAction,
RestrictTurn: RestrictTurn,
Reverse: ReverseAction,
Revert: Revert,
RotateWay: RotateWayAction,
Split: SplitAction,
Straighten: StraightenAction,
UnrestrictTurn: UnrestrictTurn
});
function AddArea(context) {
var mode = {
id: 'add-area',
button: 'area',
title: t('modes.add_area.title'),
description: t('modes.add_area.description'),
key: '3'
};
var behavior = AddWay(context)
.tail(t('modes.add_area.tail'))
.on('start', start)
.on('startFromWay', startFromWay)
.on('startFromNode', startFromNode),
defaultTags = {area: 'yes'};
function start(loc) {
var graph = context.graph(),
node = Node({loc: loc}),
way = Way({tags: defaultTags});
context.perform(
AddEntity(node),
AddEntity(way),
AddVertex(way.id, node.id),
AddVertex(way.id, node.id));
context.enter(DrawArea(context, way.id, graph));
}
function startFromWay(loc, edge) {
var graph = context.graph(),
node = Node({loc: loc}),
way = Way({tags: defaultTags});
context.perform(
AddEntity(node),
AddEntity(way),
AddVertex(way.id, node.id),
AddVertex(way.id, node.id),
AddMidpoint({ loc: loc, edge: edge }, node));
context.enter(DrawArea(context, way.id, graph));
}
function startFromNode(node) {
var graph = context.graph(),
way = Way({tags: defaultTags});
context.perform(
AddEntity(way),
AddVertex(way.id, node.id),
AddVertex(way.id, node.id));
context.enter(DrawArea(context, way.id, graph));
}
mode.enter = function() {
context.install(behavior);
};
mode.exit = function() {
context.uninstall(behavior);
};
return mode;
}
function AddLine(context) {
var mode = {
id: 'add-line',
button: 'line',
title: t('modes.add_line.title'),
description: t('modes.add_line.description'),
key: '2'
};
var behavior = AddWay(context)
.tail(t('modes.add_line.tail'))
.on('start', start)
.on('startFromWay', startFromWay)
.on('startFromNode', startFromNode);
function start(loc) {
var baseGraph = context.graph(),
node = Node({loc: loc}),
way = Way();
context.perform(
AddEntity(node),
AddEntity(way),
AddVertex(way.id, node.id));
context.enter(DrawLine(context, way.id, baseGraph));
}
function startFromWay(loc, edge) {
var baseGraph = context.graph(),
node = Node({loc: loc}),
way = Way();
context.perform(
AddEntity(node),
AddEntity(way),
AddVertex(way.id, node.id),
AddMidpoint({ loc: loc, edge: edge }, node));
context.enter(DrawLine(context, way.id, baseGraph));
}
function startFromNode(node) {
var baseGraph = context.graph(),
way = Way();
context.perform(
AddEntity(way),
AddVertex(way.id, node.id));
context.enter(DrawLine(context, way.id, baseGraph));
}
mode.enter = function() {
context.install(behavior);
};
mode.exit = function() {
context.uninstall(behavior);
};
return mode;
}
function AddPoint(context) {
var mode = {
id: 'add-point',
button: 'point',
title: t('modes.add_point.title'),
description: t('modes.add_point.description'),
key: '1'
};
var behavior = Draw(context)
.tail(t('modes.add_point.tail'))
.on('click', add)
.on('clickWay', addWay)
.on('clickNode', addNode)
.on('cancel', cancel)
.on('finish', cancel);
function add(loc) {
var node = Node({loc: loc});
context.perform(
AddEntity(node),
t('operations.add.annotation.point'));
context.enter(
SelectMode(context, [node.id])
.suppressMenu(true)
.newFeature(true));
}
function addWay(loc) {
add(loc);
}
function addNode(node) {
add(node.loc);
}
function cancel() {
context.enter(Browse(context));
}
mode.enter = function() {
context.install(behavior);
};
mode.exit = function() {
context.uninstall(behavior);
};
return mode;
}
function Browse(context) {
var mode = {
button: 'browse',
id: 'browse',
title: t('modes.browse.title'),
description: t('modes.browse.description')
}, sidebar;
var behaviors = [
Paste(context),
Hover(context)
.on('hover', context.ui().sidebar.hover),
Select(context),
Lasso(context),
DragNode(context).behavior];
mode.enter = function() {
behaviors.forEach(function(behavior) {
context.install(behavior);
});
// Get focus on the body.
if (document.activeElement && document.activeElement.blur) {
document.activeElement.blur();
}
if (sidebar) {
context.ui().sidebar.show(sidebar);
} else {
context.ui().sidebar.select(null);
}
};
mode.exit = function() {
context.ui().sidebar.hover.cancel();
behaviors.forEach(function(behavior) {
context.uninstall(behavior);
});
if (sidebar) {
context.ui().sidebar.hide();
}
};
mode.sidebar = function(_) {
if (!arguments.length) return sidebar;
sidebar = _;
return mode;
};
return mode;
}
function DragNode(context) {
var mode = {
id: 'drag-node',
button: 'browse'
};
var nudgeInterval,
activeIDs,
wasMidpoint,
cancelled,
selectedIDs = [],
hover = Hover(context)
.altDisables(true)
.on('hover', context.ui().sidebar.hover),
edit = Edit(context);
function edge(point, size) {
var pad = [30, 100, 30, 100];
if (point[0] > size[0] - pad[0]) return [-10, 0];
else if (point[0] < pad[2]) return [10, 0];
else if (point[1] > size[1] - pad[1]) return [0, -10];
else if (point[1] < pad[3]) return [0, 10];
return null;
}
function startNudge(nudge) {
if (nudgeInterval) window.clearInterval(nudgeInterval);
nudgeInterval = window.setInterval(function() {
context.pan(nudge);
}, 50);
}
function stopNudge() {
if (nudgeInterval) window.clearInterval(nudgeInterval);
nudgeInterval = null;
}
function moveAnnotation(entity) {
return t('operations.move.annotation.' + entity.geometry(context.graph()));
}
function connectAnnotation(entity) {
return t('operations.connect.annotation.' + entity.geometry(context.graph()));
}
function origin(entity) {
return context.projection(entity.loc);
}
function start(entity) {
cancelled = d3.event.sourceEvent.shiftKey ||
context.features().hasHiddenConnections(entity, context.graph());
if (cancelled) return behavior.cancel();
wasMidpoint = entity.type === 'midpoint';
if (wasMidpoint) {
var midpoint = entity;
entity = Node();
context.perform(AddMidpoint(midpoint, entity));
var vertex = context.surface()
.selectAll('.' + entity.id);
behavior.target(vertex.node(), entity);
} else {
context.perform(
Noop());
}
activeIDs = _.map(context.graph().parentWays(entity), 'id');
activeIDs.push(entity.id);
context.enter(mode);
}
function datum() {
if (d3.event.sourceEvent.altKey) {
return {};
}
return d3.event.sourceEvent.target.__data__ || {};
}
// via https://gist.github.com/shawnbot/4166283
function childOf(p, c) {
if (p === c) return false;
while (c && c !== p) c = c.parentNode;
return c === p;
}
function move(entity) {
if (cancelled) return;
d3.event.sourceEvent.stopPropagation();
var nudge = childOf(context.container().node(),
d3.event.sourceEvent.toElement) &&
edge(d3.event.point, context.map().dimensions());
if (nudge) startNudge(nudge);
else stopNudge();
var loc = context.projection.invert(d3.event.point);
var d = datum();
if (d.type === 'node' && d.id !== entity.id) {
loc = d.loc;
} else if (d.type === 'way' && !d3.select(d3.event.sourceEvent.target).classed('fill')) {
loc = chooseEdge(context.childNodes(d), context.mouse(), context.projection).loc;
}
context.replace(
MoveNode(entity.id, loc),
moveAnnotation(entity));
}
function end(entity) {
if (cancelled) return;
var d = datum();
if (d.type === 'way') {
var choice = chooseEdge(context.childNodes(d), context.mouse(), context.projection);
context.replace(
AddMidpoint({ loc: choice.loc, edge: [d.nodes[choice.index - 1], d.nodes[choice.index]] }, entity),
connectAnnotation(d));
} else if (d.type === 'node' && d.id !== entity.id) {
context.replace(
Connect([d.id, entity.id]),
connectAnnotation(d));
} else if (wasMidpoint) {
context.replace(
Noop(),
t('operations.add.annotation.vertex'));
} else {
context.replace(
Noop(),
moveAnnotation(entity));
}
var reselection = selectedIDs.filter(function(id) {
return context.graph().hasEntity(id);
});
if (reselection.length) {
context.enter(
SelectMode(context, reselection)
.suppressMenu(true));
} else {
context.enter(Browse(context));
}
}
function cancel() {
behavior.cancel();
context.enter(Browse(context));
}
function setActiveElements() {
context.surface().selectAll(entitySelector(activeIDs))
.classed('active', true);
}
var behavior = drag()
.delegate('g.node, g.point, g.midpoint')
.surface(context.surface().node())
.origin(origin)
.on('start', start)
.on('move', move)
.on('end', end);
mode.enter = function() {
context.install(hover);
context.install(edit);
context.history()
.on('undone.drag-node', cancel);
context.map()
.on('drawn.drag-node', setActiveElements);
setActiveElements();
};
mode.exit = function() {
context.ui().sidebar.hover.cancel();
context.uninstall(hover);
context.uninstall(edit);
context.history()
.on('undone.drag-node', null);
context.map()
.on('drawn.drag-node', null);
context.surface()
.selectAll('.active')
.classed('active', false);
stopNudge();
};
mode.selectedIDs = function(_) {
if (!arguments.length) return selectedIDs;
selectedIDs = _;
return mode;
};
mode.behavior = behavior;
return mode;
}
function DrawArea(context, wayId, baseGraph) {
var mode = {
button: 'area',
id: 'draw-area'
};
var behavior;
mode.enter = function() {
var way = context.entity(wayId),
headId = way.nodes[way.nodes.length - 2],
tailId = way.first();
behavior = DrawWay(context, wayId, -1, mode, baseGraph)
.tail(t('modes.draw_area.tail'));
var addNode = behavior.addNode;
behavior.addNode = function(node) {
if (node.id === headId || node.id === tailId) {
behavior.finish();
} else {
addNode(node);
}
};
context.install(behavior);
};
mode.exit = function() {
context.uninstall(behavior);
};
mode.selectedIDs = function() {
return [wayId];
};
return mode;
}
function DrawLine(context, wayId, baseGraph, affix) {
var mode = {
button: 'line',
id: 'draw-line'
};
var behavior;
mode.enter = function() {
var way = context.entity(wayId),
index = (affix === 'prefix') ? 0 : undefined,
headId = (affix === 'prefix') ? way.first() : way.last();
behavior = DrawWay(context, wayId, index, mode, baseGraph)
.tail(t('modes.draw_line.tail'));
var addNode = behavior.addNode;
behavior.addNode = function(node) {
if (node.id === headId) {
behavior.finish();
} else {
addNode(node);
}
};
context.install(behavior);
};
mode.exit = function() {
context.uninstall(behavior);
};
mode.selectedIDs = function() {
return [wayId];
};
return mode;
}
function MoveMode(context, entityIDs, baseGraph) {
var mode = {
id: 'move',
button: 'browse'
};
var keybinding = d3.keybinding('move'),
edit = Edit(context),
annotation = entityIDs.length === 1 ?
t('operations.move.annotation.' + context.geometry(entityIDs[0])) :
t('operations.move.annotation.multiple'),
cache,
origin,
nudgeInterval;
function vecSub(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
function edge(point, size) {
var pad = [30, 100, 30, 100];
if (point[0] > size[0] - pad[0]) return [-10, 0];
else if (point[0] < pad[2]) return [10, 0];
else if (point[1] > size[1] - pad[1]) return [0, -10];
else if (point[1] < pad[3]) return [0, 10];
return null;
}
function startNudge(nudge) {
if (nudgeInterval) window.clearInterval(nudgeInterval);
nudgeInterval = window.setInterval(function() {
context.pan(nudge);
var currMouse = context.mouse(),
origMouse = context.projection(origin),
delta = vecSub(vecSub(currMouse, origMouse), nudge),
action = MoveAction(entityIDs, delta, context.projection, cache);
context.overwrite(action, annotation);
}, 50);
}
function stopNudge() {
if (nudgeInterval) window.clearInterval(nudgeInterval);
nudgeInterval = null;
}
function move() {
var currMouse = context.mouse(),
origMouse = context.projection(origin),
delta = vecSub(currMouse, origMouse),
action = MoveAction(entityIDs, delta, context.projection, cache);
context.overwrite(action, annotation);
var nudge = edge(currMouse, context.map().dimensions());
if (nudge) startNudge(nudge);
else stopNudge();
}
function finish() {
d3.event.stopPropagation();
context.enter(SelectMode(context, entityIDs).suppressMenu(true));
stopNudge();
}
function cancel() {
if (baseGraph) {
while (context.graph() !== baseGraph) context.pop();
context.enter(Browse(context));
} else {
context.pop();
context.enter(SelectMode(context, entityIDs).suppressMenu(true));
}
stopNudge();
}
function undone() {
context.enter(Browse(context));
}
mode.enter = function() {
origin = context.map().mouseCoordinates();
cache = {};
context.install(edit);
context.perform(
Noop(),
annotation);
context.surface()
.on('mousemove.move', move)
.on('click.move', finish);
context.history()
.on('undone.move', undone);
keybinding
.on('⎋', cancel)
.on('↩', finish);
d3.select(document)
.call(keybinding);
};
mode.exit = function() {
stopNudge();
context.uninstall(edit);
context.surface()
.on('mousemove.move', null)
.on('click.move', null);
context.history()
.on('undone.move', null);
keybinding.off();
};
return mode;
}
function RotateWay(context, wayId) {
var mode = {
id: 'rotate-way',
button: 'browse'
};
var keybinding = d3.keybinding('rotate-way'),
edit = Edit(context);
mode.enter = function() {
context.install(edit);
var annotation = t('operations.rotate.annotation.' + context.geometry(wayId)),
way = context.graph().entity(wayId),
nodes = _.uniq(context.graph().childNodes(way)),
points = nodes.map(function(n) { return context.projection(n.loc); }),
pivot = d3.geom.polygon(points).centroid(),
angle;
context.perform(
Noop(),
annotation);
function rotate() {
var mousePoint = context.mouse(),
newAngle = Math.atan2(mousePoint[1] - pivot[1], mousePoint[0] - pivot[0]);
if (typeof angle === 'undefined') angle = newAngle;
context.replace(
RotateWayAction(wayId, pivot, newAngle - angle, context.projection),
annotation);
angle = newAngle;
}
function finish() {
d3.event.stopPropagation();
context.enter(SelectMode(context, [wayId])
.suppressMenu(true));
}
function cancel() {
context.pop();
context.enter(SelectMode(context, [wayId])
.suppressMenu(true));
}
function undone() {
context.enter(Browse(context));
}
context.surface()
.on('mousemove.rotate-way', rotate)
.on('click.rotate-way', finish);
context.history()
.on('undone.rotate-way', undone);
keybinding
.on('⎋', cancel)
.on('↩', finish);
d3.select(document)
.call(keybinding);
};
mode.exit = function() {
context.uninstall(edit);
context.surface()
.on('mousemove.rotate-way', null)
.on('click.rotate-way', null);
context.history()
.on('undone.rotate-way', null);
keybinding.off();
};
return mode;
}
function Save$1(context) {
var ui = Commit(context)
.on('cancel', cancel)
.on('save', save);
function cancel() {
context.enter(Browse(context));
}
function save(e, tryAgain) {
function withChildNodes(ids, graph) {
return _.uniq(_.reduce(ids, function(result, id) {
var e = graph.entity(id);
if (e.type === 'way') {
try {
var cn = graph.childNodes(e);
result.push.apply(result, _.map(_.filter(cn, 'version'), 'id'));
} catch (err) {
/* eslint-disable no-console */
if (typeof console !== 'undefined') console.error(err);
/* eslint-enable no-console */
}
}
return result;
}, _.clone(ids)));
}
var loading = Loading(context).message(t('save.uploading')).blocking(true),
history = context.history(),
origChanges = history.changes(DiscardTags(history.difference())),
localGraph = context.graph(),
remoteGraph = Graph(history.base(), true),
modified = _.filter(history.difference().summary(), {changeType: 'modified'}),
toCheck = _.map(_.map(modified, 'entity'), 'id'),
toLoad = withChildNodes(toCheck, localGraph),
conflicts = [],
errors = [];
if (!tryAgain) history.perform(Noop()); // checkpoint
context.container().call(loading);
if (toCheck.length) {
context.connection().loadMultiple(toLoad, loaded);
} else {
finalize();
}
// Reload modified entities into an alternate graph and check for conflicts..
function loaded(err, result) {
if (errors.length) return;
if (err) {
errors.push({
msg: err.responseText,
details: [ t('save.status_code', { code: err.status }) ]
});
showErrors();
} else {
var loadMore = [];
_.each(result.data, function(entity) {
remoteGraph.replace(entity);
toLoad = _.without(toLoad, entity.id);
// Because loadMultiple doesn't download /full like loadEntity,
// need to also load children that aren't already being checked..
if (!entity.visible) return;
if (entity.type === 'way') {
loadMore.push.apply(loadMore,
_.difference(entity.nodes, toCheck, toLoad, loadMore));
} else if (entity.type === 'relation' && entity.isMultipolygon()) {
loadMore.push.apply(loadMore,
_.difference(_.map(entity.members, 'id'), toCheck, toLoad, loadMore));
}
});
if (loadMore.length) {
toLoad.push.apply(toLoad, loadMore);
context.connection().loadMultiple(loadMore, loaded);
}
if (!toLoad.length) {
checkConflicts();
}
}
}
function checkConflicts() {
function choice(id, text, action) {
return { id: id, text: text, action: function() { history.replace(action); } };
}
function formatUser(d) {
return '<a href="' + context.connection().userURL(d) + '" target="_blank">' + d + '</a>';
}
function entityName(entity) {
return displayName(entity) || (displayType(entity.id) + ' ' + entity.id);
}
function compareVersions(local, remote) {
if (local.version !== remote.version) return false;
if (local.type === 'way') {
var children = _.union(local.nodes, remote.nodes);
for (var i = 0; i < children.length; i++) {
var a = localGraph.hasEntity(children[i]),
b = remoteGraph.hasEntity(children[i]);
if (a && b && a.version !== b.version) return false;
}
}
return true;
}
_.each(toCheck, function(id) {
var local = localGraph.entity(id),
remote = remoteGraph.entity(id);
if (compareVersions(local, remote)) return;
var action = MergeRemoteChanges,
merge = action(id, localGraph, remoteGraph, formatUser);
history.replace(merge);
var mergeConflicts = merge.conflicts();
if (!mergeConflicts.length) return; // merged safely
var forceLocal = action(id, localGraph, remoteGraph).withOption('force_local'),
forceRemote = action(id, localGraph, remoteGraph).withOption('force_remote'),
keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore')),
keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete'));
conflicts.push({
id: id,
name: entityName(local),
details: mergeConflicts,
chosen: 1,
choices: [
choice(id, keepMine, forceLocal),
choice(id, keepTheirs, forceRemote)
]
});
});
finalize();
}
function finalize() {
if (conflicts.length) {
conflicts.sort(function(a,b) { return b.id.localeCompare(a.id); });
showConflicts();
} else if (errors.length) {
showErrors();
} else {
var changes = history.changes(DiscardTags(history.difference()));
if (changes.modified.length || changes.created.length || changes.deleted.length) {
context.connection().putChangeset(
changes,
e.comment,
history.imageryUsed(),
function(err, changeset_id) {
if (err) {
errors.push({
msg: err.responseText,
details: [ t('save.status_code', { code: err.status }) ]
});
showErrors();
} else {
history.clearSaved();
success(e, changeset_id);
// Add delay to allow for postgres replication #1646 #2678
window.setTimeout(function() {
loading.close();
context.flush();
}, 2500);
}
});
} else { // changes were insignificant or reverted by user
loading.close();
context.flush();
cancel();
}
}
}
function showConflicts() {
var selection = context.container()
.select('#sidebar')
.append('div')
.attr('class','sidebar-component');
loading.close();
selection.call(Conflicts(context)
.list(conflicts)
.on('download', function() {
var data = JXON.stringify(context.connection().osmChangeJXON('CHANGEME', origChanges)),
win = window.open('data:text/xml,' + encodeURIComponent(data), '_blank');
win.focus();
})
.on('cancel', function() {
history.pop();
selection.remove();
})
.on('save', function() {
for (var i = 0; i < conflicts.length; i++) {
if (conflicts[i].chosen === 1) { // user chose "keep theirs"
var entity = context.hasEntity(conflicts[i].id);
if (entity && entity.type === 'way') {
var children = _.uniq(entity.nodes);
for (var j = 0; j < children.length; j++) {
history.replace(Revert(children[j]));
}
}
history.replace(Revert(conflicts[i].id));
}
}
selection.remove();
save(e, true);
})
);
}
function showErrors() {
var selection = confirm(context.container());
history.pop();
loading.close();
selection
.select('.modal-section.header')
.append('h3')
.text(t('save.error'));
addErrors(selection, errors);
selection.okButton();
}
function addErrors(selection, data) {
var message = selection
.select('.modal-section.message-text');
var items = message
.selectAll('.error-container')
.data(data);
var enter = items.enter()
.append('div')
.attr('class', 'error-container');
enter
.append('a')
.attr('class', 'error-description')
.attr('href', '#')
.classed('hide-toggle', true)
.text(function(d) { return d.msg || t('save.unknown_error_details'); })
.on('click', function() {
var error = d3.select(this),
detail = d3.select(this.nextElementSibling),
exp = error.classed('expanded');
detail.style('display', exp ? 'none' : 'block');
error.classed('expanded', !exp);
d3.event.preventDefault();
});
var details = enter
.append('div')
.attr('class', 'error-detail-container')
.style('display', 'none');
details
.append('ul')
.attr('class', 'error-detail-list')
.selectAll('li')
.data(function(d) { return d.details || []; })
.enter()
.append('li')
.attr('class', 'error-detail-item')
.text(function(d) { return d; });
items.exit()
.remove();
}
}
function success(e, changeset_id) {
context.enter(Browse(context)
.sidebar(Success(context)
.changeset({
id: changeset_id,
comment: e.comment
})
.on('cancel', function() {
context.ui().sidebar.hide();
})));
}
var mode = {
id: 'save'
};
mode.enter = function() {
context.connection().authenticate(function(err) {
if (err) {
cancel();
} else {
context.ui().sidebar.show(ui);
}
});
};
mode.exit = function() {
context.ui().sidebar.hide();
};
return mode;
}
function Circularize(selectedIDs, context) {
var entityId = selectedIDs[0],
entity = context.entity(entityId),
extent = entity.extent(context.graph()),
geometry = context.geometry(entityId),
action = CircularizeAction(entityId, context.projection);
var operation = function() {
var annotation = t('operations.circularize.annotation.' + geometry);
context.perform(action, annotation);
};
operation.available = function() {
return selectedIDs.length === 1 &&
entity.type === 'way' &&
_.uniq(entity.nodes).length > 1;
};
operation.disabled = function() {
var reason;
if (extent.percentContainedIn(context.extent()) < 0.8) {
reason = 'too_large';
} else if (context.hasHiddenConnections(entityId)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.circularize.' + disable) :
t('operations.circularize.description.' + geometry);
};
operation.id = 'circularize';
operation.keys = [t('operations.circularize.key')];
operation.title = t('operations.circularize.title');
return operation;
}
function Continue(selectedIDs, context) {
var graph = context.graph(),
entities = selectedIDs.map(function(id) { return graph.entity(id); }),
geometries = _.extend({line: [], vertex: []},
_.groupBy(entities, function(entity) { return entity.geometry(graph); })),
vertex = geometries.vertex[0];
function candidateWays() {
return graph.parentWays(vertex).filter(function(parent) {
return parent.geometry(graph) === 'line' &&
parent.affix(vertex.id) &&
(geometries.line.length === 0 || geometries.line[0] === parent);
});
}
var operation = function() {
var candidate = candidateWays()[0];
context.enter(DrawLine(
context,
candidate.id,
context.graph(),
candidate.affix(vertex.id)));
};
operation.available = function() {
return geometries.vertex.length === 1 && geometries.line.length <= 1 &&
!context.features().hasHiddenConnections(vertex, context.graph());
};
operation.disabled = function() {
var candidates = candidateWays();
if (candidates.length === 0)
return 'not_eligible';
if (candidates.length > 1)
return 'multiple';
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.continue.' + disable) :
t('operations.continue.description');
};
operation.id = 'continue';
operation.keys = [t('operations.continue.key')];
operation.title = t('operations.continue.title');
return operation;
}
function Delete(selectedIDs, context) {
var action = DeleteMultiple(selectedIDs);
var operation = function() {
var annotation,
nextSelectedID;
if (selectedIDs.length > 1) {
annotation = t('operations.delete.annotation.multiple', {n: selectedIDs.length});
} else {
var id = selectedIDs[0],
entity = context.entity(id),
geometry = context.geometry(id),
parents = context.graph().parentWays(entity),
parent = parents[0];
annotation = t('operations.delete.annotation.' + geometry);
// Select the next closest node in the way.
if (geometry === 'vertex' && parents.length === 1 && parent.nodes.length > 2) {
var nodes = parent.nodes,
i = nodes.indexOf(id);
if (i === 0) {
i++;
} else if (i === nodes.length - 1) {
i--;
} else {
var a = sphericalDistance(entity.loc, context.entity(nodes[i - 1]).loc),
b = sphericalDistance(entity.loc, context.entity(nodes[i + 1]).loc);
i = a < b ? i - 1 : i + 1;
}
nextSelectedID = nodes[i];
}
}
if (nextSelectedID && context.hasEntity(nextSelectedID)) {
context.enter(SelectMode(context, [nextSelectedID]));
} else {
context.enter(Browse(context));
}
context.perform(
action,
annotation);
};
operation.available = function() {
return true;
};
operation.disabled = function() {
var reason;
if (_.some(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.delete.' + disable) :
t('operations.delete.description');
};
operation.id = 'delete';
operation.keys = [iD.ui.cmd('⌘⌫'), iD.ui.cmd('⌘⌦')];
operation.title = t('operations.delete.title');
return operation;
}
function Disconnect(selectedIDs, context) {
var vertices = _.filter(selectedIDs, function vertex(entityId) {
return context.geometry(entityId) === 'vertex';
});
var entityId = vertices[0],
action = DisconnectAction(entityId);
if (selectedIDs.length > 1) {
action.limitWays(_.without(selectedIDs, entityId));
}
var operation = function() {
context.perform(action, t('operations.disconnect.annotation'));
};
operation.available = function() {
return vertices.length === 1;
};
operation.disabled = function() {
var reason;
if (_.some(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.disconnect.' + disable) :
t('operations.disconnect.description');
};
operation.id = 'disconnect';
operation.keys = [t('operations.disconnect.key')];
operation.title = t('operations.disconnect.title');
return operation;
}
function Merge(selectedIDs, context) {
var join = Join(selectedIDs),
merge = MergeAction(selectedIDs),
mergePolygon = MergePolygon(selectedIDs);
var operation = function() {
var annotation = t('operations.merge.annotation', {n: selectedIDs.length}),
action;
if (!join.disabled(context.graph())) {
action = join;
} else if (!merge.disabled(context.graph())) {
action = merge;
} else {
action = mergePolygon;
}
context.perform(action, annotation);
context.enter(SelectMode(context, selectedIDs.filter(function(id) { return context.hasEntity(id); }))
.suppressMenu(true));
};
operation.available = function() {
return selectedIDs.length >= 2;
};
operation.disabled = function() {
return join.disabled(context.graph()) &&
merge.disabled(context.graph()) &&
mergePolygon.disabled(context.graph());
};
operation.tooltip = function() {
var j = join.disabled(context.graph()),
m = merge.disabled(context.graph()),
p = mergePolygon.disabled(context.graph());
if (j === 'restriction' && m && p)
return t('operations.merge.restriction', {relation: context.presets().item('type/restriction').name()});
if (p === 'incomplete_relation' && j && m)
return t('operations.merge.incomplete_relation');
if (j && m && p)
return t('operations.merge.' + j);
return t('operations.merge.description');
};
operation.id = 'merge';
operation.keys = [t('operations.merge.key')];
operation.title = t('operations.merge.title');
return operation;
}
function Move(selectedIDs, context) {
var extent = selectedIDs.reduce(function(extent, id) {
return extent.extend(context.entity(id).extent(context.graph()));
}, Extent());
var operation = function() {
context.enter(MoveMode(context, selectedIDs));
};
operation.available = function() {
return selectedIDs.length > 1 ||
context.entity(selectedIDs[0]).type !== 'node';
};
operation.disabled = function() {
var reason;
if (extent.area() && extent.percentContainedIn(context.extent()) < 0.8) {
reason = 'too_large';
} else if (_.some(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
}
return MoveAction(selectedIDs).disabled(context.graph()) || reason;
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.move.' + disable) :
t('operations.move.description');
};
operation.id = 'move';
operation.keys = [t('operations.move.key')];
operation.title = t('operations.move.title');
return operation;
}
function Orthogonalize(selectedIDs, context) {
var entityId = selectedIDs[0],
entity = context.entity(entityId),
extent = entity.extent(context.graph()),
geometry = context.geometry(entityId),
action = OrthogonalizeAction(entityId, context.projection);
var operation = function() {
var annotation = t('operations.orthogonalize.annotation.' + geometry);
context.perform(action, annotation);
};
operation.available = function() {
return selectedIDs.length === 1 &&
entity.type === 'way' &&
entity.isClosed() &&
_.uniq(entity.nodes).length > 2;
};
operation.disabled = function() {
var reason;
if (extent.percentContainedIn(context.extent()) < 0.8) {
reason = 'too_large';
} else if (context.hasHiddenConnections(entityId)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.orthogonalize.' + disable) :
t('operations.orthogonalize.description.' + geometry);
};
operation.id = 'orthogonalize';
operation.keys = [t('operations.orthogonalize.key')];
operation.title = t('operations.orthogonalize.title');
return operation;
}
function Reverse(selectedIDs, context) {
var entityId = selectedIDs[0];
var operation = function() {
context.perform(
ReverseAction(entityId),
t('operations.reverse.annotation'));
};
operation.available = function() {
return selectedIDs.length === 1 &&
context.geometry(entityId) === 'line';
};
operation.disabled = function() {
return false;
};
operation.tooltip = function() {
return t('operations.reverse.description');
};
operation.id = 'reverse';
operation.keys = [t('operations.reverse.key')];
operation.title = t('operations.reverse.title');
return operation;
}
function Rotate(selectedIDs, context) {
var entityId = selectedIDs[0],
entity = context.entity(entityId),
extent = entity.extent(context.graph()),
geometry = context.geometry(entityId);
var operation = function() {
context.enter(RotateWay(context, entityId));
};
operation.available = function() {
if (selectedIDs.length !== 1 || entity.type !== 'way')
return false;
if (geometry === 'area')
return true;
if (entity.isClosed() &&
context.graph().parentRelations(entity).some(function(r) { return r.isMultipolygon(); }))
return true;
return false;
};
operation.disabled = function() {
if (extent.percentContainedIn(context.extent()) < 0.8) {
return 'too_large';
} else if (context.hasHiddenConnections(entityId)) {
return 'connected_to_hidden';
} else {
return false;
}
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.rotate.' + disable) :
t('operations.rotate.description');
};
operation.id = 'rotate';
operation.keys = [t('operations.rotate.key')];
operation.title = t('operations.rotate.title');
return operation;
}
function Split(selectedIDs, context) {
var vertices = _.filter(selectedIDs, function vertex(entityId) {
return context.geometry(entityId) === 'vertex';
});
var entityId = vertices[0],
action = SplitAction(entityId);
if (selectedIDs.length > 1) {
action.limitWays(_.without(selectedIDs, entityId));
}
var operation = function() {
var annotation;
var ways = action.ways(context.graph());
if (ways.length === 1) {
annotation = t('operations.split.annotation.' + context.geometry(ways[0].id));
} else {
annotation = t('operations.split.annotation.multiple', {n: ways.length});
}
var difference = context.perform(action, annotation);
context.enter(SelectMode(context, difference.extantIDs()));
};
operation.available = function() {
return vertices.length === 1;
};
operation.disabled = function() {
var reason;
if (_.some(selectedIDs, context.hasHiddenConnections)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {
var disable = operation.disabled();
if (disable) {
return t('operations.split.' + disable);
}
var ways = action.ways(context.graph());
if (ways.length === 1) {
return t('operations.split.description.' + context.geometry(ways[0].id));
} else {
return t('operations.split.description.multiple');
}
};
operation.id = 'split';
operation.keys = [t('operations.split.key')];
operation.title = t('operations.split.title');
return operation;
}
function Straighten(selectedIDs, context) {
var entityId = selectedIDs[0],
action = StraightenAction(entityId, context.projection);
function operation() {
var annotation = t('operations.straighten.annotation');
context.perform(action, annotation);
}
operation.available = function() {
var entity = context.entity(entityId);
return selectedIDs.length === 1 &&
entity.type === 'way' &&
!entity.isClosed() &&
_.uniq(entity.nodes).length > 2;
};
operation.disabled = function() {
var reason;
if (context.hasHiddenConnections(entityId)) {
reason = 'connected_to_hidden';
}
return action.disabled(context.graph()) || reason;
};
operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.straighten.' + disable) :
t('operations.straighten.description');
};
operation.id = 'straighten';
operation.keys = [t('operations.straighten.key')];
operation.title = t('operations.straighten.title');
return operation;
}
var Operations = Object.freeze({
Circularize: Circularize,
Continue: Continue,
Delete: Delete,
Disconnect: Disconnect,
Merge: Merge,
Move: Move,
Orthogonalize: Orthogonalize,
Reverse: Reverse,
Rotate: Rotate,
Split: Split,
Straighten: Straighten
});
function SelectMode(context, selectedIDs) {
var mode = {
id: 'select',
button: 'browse'
};
var keybinding = d3.keybinding('select'),
timeout = null,
behaviors = [
Copy(context),
Paste(context),
Breathe(context),
Hover(context),
Select(context),
Lasso(context),
DragNode(context)
.selectedIDs(selectedIDs)
.behavior],
inspector,
radialMenu,
newFeature = false,
suppressMenu = false;
var wrap = context.container()
.select('.inspector-wrap');
function singular() {
if (selectedIDs.length === 1) {
return context.hasEntity(selectedIDs[0]);
}
}
function closeMenu() {
if (radialMenu) {
context.surface().call(radialMenu.close);
}
}
function positionMenu() {
if (suppressMenu || !radialMenu) { return; }
var entity = singular();
if (entity && context.geometry(entity.id) === 'relation') {
suppressMenu = true;
} else if (entity && entity.type === 'node') {
radialMenu.center(context.projection(entity.loc));
} else {
var point = context.mouse(),
viewport = Extent(context.projection.clipExtent()).polygon();
if (pointInPolygon(point, viewport)) {
radialMenu.center(point);
} else {
suppressMenu = true;
}
}
}
function showMenu() {
closeMenu();
if (!suppressMenu && radialMenu) {
context.surface().call(radialMenu);
}
}
function toggleMenu() {
if (d3.select('.radial-menu').empty()) {
showMenu();
} else {
closeMenu();
}
}
mode.selectedIDs = function() {
return selectedIDs;
};
mode.reselect = function() {
var surfaceNode = context.surface().node();
if (surfaceNode.focus) { // FF doesn't support it
surfaceNode.focus();
}
positionMenu();
showMenu();
};
mode.newFeature = function(_) {
if (!arguments.length) return newFeature;
newFeature = _;
return mode;
};
mode.suppressMenu = function(_) {
if (!arguments.length) return suppressMenu;
suppressMenu = _;
return mode;
};
mode.enter = function() {
function update() {
closeMenu();
if (_.some(selectedIDs, function(id) { return !context.hasEntity(id); })) {
// Exit mode if selected entity gets undone
context.enter(Browse(context));
}
}
function dblclick() {
var target = d3.select(d3.event.target),
datum = target.datum();
if (datum instanceof Way && !target.classed('fill')) {
var choice = chooseEdge(context.childNodes(datum), context.mouse(), context.projection),
node = Node();
var prev = datum.nodes[choice.index - 1],
next = datum.nodes[choice.index];
context.perform(
AddMidpoint({loc: choice.loc, edge: [prev, next]}, node),
t('operations.add.annotation.vertex'));
d3.event.preventDefault();
d3.event.stopPropagation();
}
}
function selectElements(drawn) {
var entity = singular();
if (entity && context.geometry(entity.id) === 'relation') {
suppressMenu = true;
return;
}
var selection = context.surface()
.selectAll(entityOrMemberSelector(selectedIDs, context.graph()));
if (selection.empty()) {
if (drawn) { // Exit mode if selected DOM elements have disappeared..
context.enter(Browse(context));
}
} else {
selection
.classed('selected', true);
}
}
function esc() {
if (!context.inIntro()) {
context.enter(Browse(context));
}
}
behaviors.forEach(function(behavior) {
context.install(behavior);
});
var operations = _.without(d3.values(Operations), Delete)
.map(function(o) { return o(selectedIDs, context); })
.filter(function(o) { return o.available(); });
operations.unshift(Delete(selectedIDs, context));
keybinding
.on('⎋', esc, true)
.on('space', toggleMenu);
operations.forEach(function(operation) {
operation.keys.forEach(function(key) {
keybinding.on(key, function() {
if (!(context.inIntro() || operation.disabled())) {
operation();
}
});
});
});
d3.select(document)
.call(keybinding);
radialMenu = RadialMenu(context, operations);
context.ui().sidebar
.select(singular() ? singular().id : null, newFeature);
context.history()
.on('undone.select', update)
.on('redone.select', update);
context.map()
.on('move.select', closeMenu)
.on('drawn.select', selectElements);
selectElements();
var show = d3.event && !suppressMenu;
if (show) {
positionMenu();
}
timeout = window.setTimeout(function() {
if (show) {
showMenu();
}
context.surface()
.on('dblclick.select', dblclick);
}, 200);
if (selectedIDs.length > 1) {
var entities = SelectionList(context, selectedIDs);
context.ui().sidebar.show(entities);
}
};
mode.exit = function() {
if (timeout) window.clearTimeout(timeout);
if (inspector) wrap.call(inspector.close);
behaviors.forEach(function(behavior) {
context.uninstall(behavior);
});
keybinding.off();
closeMenu();
radialMenu = undefined;
context.history()
.on('undone.select', null)
.on('redone.select', null);
context.surface()
.on('dblclick.select', null)
.selectAll('.selected')
.classed('selected', false);
context.map().on('drawn.select', null);
context.ui().sidebar.hide();
};
return mode;
}
var modes = Object.freeze({
AddArea: AddArea,
AddLine: AddLine,
AddPoint: AddPoint,
Browse: Browse,
DragNode: DragNode,
DrawArea: DrawArea,
DrawLine: DrawLine,
Move: MoveMode,
RotateWay: RotateWay,
Save: Save$1,
Select: SelectMode
});
function Edit(context) {
function edit() {
context.map()
.minzoom(context.minEditableZoom());
}
edit.off = function() {
context.map()
.minzoom(0);
};
return edit;
}
/*
The hover behavior adds the `.hover` class on mouseover to all elements to which
the identical datum is bound, and removes it on mouseout.
The :hover pseudo-class is insufficient for iD's purposes because a datum's visual
representation may consist of several elements scattered throughout the DOM hierarchy.
Only one of these elements can have the :hover pseudo-class, but all of them will
have the .hover class.
*/
function Hover() {
var dispatch = d3.dispatch('hover'),
selection,
altDisables,
target;
function keydown() {
if (altDisables && d3.event.keyCode === d3.keybinding.modifierCodes.alt) {
dispatch.hover(null);
selection.selectAll('.hover')
.classed('hover-suppressed', true)
.classed('hover', false);
}
}
function keyup() {
if (altDisables && d3.event.keyCode === d3.keybinding.modifierCodes.alt) {
dispatch.hover(target ? target.id : null);
selection.selectAll('.hover-suppressed')
.classed('hover-suppressed', false)
.classed('hover', true);
}
}
var hover = function(__) {
selection = __;
function enter(d) {
if (d === target) return;
target = d;
selection.selectAll('.hover')
.classed('hover', false);
selection.selectAll('.hover-suppressed')
.classed('hover-suppressed', false);
if (target instanceof Entity) {
var selector = '.' + target.id;
if (target.type === 'relation') {
target.members.forEach(function(member) {
selector += ', .' + member.id;
});
}
var suppressed = altDisables && d3.event && d3.event.altKey;
selection.selectAll(selector)
.classed(suppressed ? 'hover-suppressed' : 'hover', true);
dispatch.hover(target.id);
} else {
dispatch.hover(null);
}
}
var down;
function mouseover() {
if (down) return;
var target = d3.event.target;
enter(target ? target.__data__ : null);
}
function mouseout() {
if (down) return;
var target = d3.event.relatedTarget;
enter(target ? target.__data__ : null);
}
function mousedown() {
down = true;
d3.select(window)
.on('mouseup.hover', mouseup);
}
function mouseup() {
down = false;
}
selection
.on('mouseover.hover', mouseover)
.on('mouseout.hover', mouseout)
.on('mousedown.hover', mousedown)
.on('mouseup.hover', mouseup);
d3.select(window)
.on('keydown.hover', keydown)
.on('keyup.hover', keyup);
};
hover.off = function(selection) {
selection.selectAll('.hover')
.classed('hover', false);
selection.selectAll('.hover-suppressed')
.classed('hover-suppressed', false);
selection
.on('mouseover.hover', null)
.on('mouseout.hover', null)
.on('mousedown.hover', null)
.on('mouseup.hover', null);
d3.select(window)
.on('keydown.hover', null)
.on('keyup.hover', null)
.on('mouseup.hover', null);
};
hover.altDisables = function(_) {
if (!arguments.length) return altDisables;
altDisables = _;
return hover;
};
return d3.rebind(hover, dispatch, 'on');
}
function Tail() {
var text,
container,
xmargin = 25,
tooltipSize = [0, 0],
selectionSize = [0, 0];
function tail(selection) {
if (!text) return;
d3.select(window)
.on('resize.tail', function() { selectionSize = selection.dimensions(); });
function show() {
container.style('display', 'block');
tooltipSize = container.dimensions();
}
function mousemove() {
if (container.style('display') === 'none') show();
var xoffset = ((d3.event.clientX + tooltipSize[0] + xmargin) > selectionSize[0]) ?
-tooltipSize[0] - xmargin : xmargin;
container.classed('left', xoffset > 0);
setTransform(container, d3.event.clientX + xoffset, d3.event.clientY);
}
function mouseleave() {
if (d3.event.relatedTarget !== container.node()) {
container.style('display', 'none');
}
}
function mouseenter() {
if (d3.event.relatedTarget !== container.node()) {
show();
}
}
container = d3.select(document.body)
.append('div')
.style('display', 'none')
.attr('class', 'tail tooltip-inner');
container.append('div')
.text(text);
selection
.on('mousemove.tail', mousemove)
.on('mouseenter.tail', mouseenter)
.on('mouseleave.tail', mouseleave);
container
.on('mousemove.tail', mousemove);
tooltipSize = container.dimensions();
selectionSize = selection.dimensions();
}
tail.off = function(selection) {
if (!text) return;
container
.on('mousemove.tail', null)
.remove();
selection
.on('mousemove.tail', null)
.on('mouseenter.tail', null)
.on('mouseleave.tail', null);
d3.select(window)
.on('resize.tail', null);
};
tail.text = function(_) {
if (!arguments.length) return text;
text = _;
return tail;
};
return tail;
}
function Draw(context) {
var event = d3.dispatch('move', 'click', 'clickWay',
'clickNode', 'undo', 'cancel', 'finish'),
keybinding = d3.keybinding('draw'),
hover = Hover(context)
.altDisables(true)
.on('hover', context.ui().sidebar.hover),
tail = Tail(),
edit = Edit(context),
closeTolerance = 4,
tolerance = 12,
mouseLeave = false,
lastMouse = null,
cached = Draw;
function datum() {
if (d3.event.altKey) return {};
if (d3.event.type === 'keydown') {
return (lastMouse && lastMouse.target.__data__) || {};
} else {
return d3.event.target.__data__ || {};
}
}
function mousedown() {
function point() {
var p = context.container().node();
return touchId !== null ? d3.touches(p).filter(function(p) {
return p.identifier === touchId;
})[0] : d3.mouse(p);
}
var element = d3.select(this),
touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null,
t1 = +new Date(),
p1 = point();
element.on('mousemove.draw', null);
d3.select(window).on('mouseup.draw', function() {
var t2 = +new Date(),
p2 = point(),
dist = euclideanDistance(p1, p2);
element.on('mousemove.draw', mousemove);
d3.select(window).on('mouseup.draw', null);
if (dist < closeTolerance || (dist < tolerance && (t2 - t1) < 500)) {
// Prevent a quick second click
d3.select(window).on('click.draw-block', function() {
d3.event.stopPropagation();
}, true);
context.map().dblclickEnable(false);
window.setTimeout(function() {
context.map().dblclickEnable(true);
d3.select(window).on('click.draw-block', null);
}, 500);
click();
}
});
}
function mousemove() {
lastMouse = d3.event;
event.move(datum());
}
function mouseenter() {
mouseLeave = false;
}
function mouseleave() {
mouseLeave = true;
}
function click() {
var d = datum();
if (d.type === 'way') {
var dims = context.map().dimensions(),
mouse = context.mouse(),
pad = 5,
trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad &&
mouse[1] > pad && mouse[1] < dims[1] - pad;
if (trySnap) {
var choice = chooseEdge(context.childNodes(d), context.mouse(), context.projection),
edge = [d.nodes[choice.index - 1], d.nodes[choice.index]];
event.clickWay(choice.loc, edge);
} else {
event.click(context.map().mouseCoordinates());
}
} else if (d.type === 'node') {
event.clickNode(d);
} else {
event.click(context.map().mouseCoordinates());
}
}
function space() {
var currSpace = context.mouse();
if (cached.disableSpace && cached.lastSpace) {
var dist = euclideanDistance(cached.lastSpace, currSpace);
if (dist > tolerance) {
cached.disableSpace = false;
}
}
if (cached.disableSpace || mouseLeave || !lastMouse) return;
// user must move mouse or release space bar to allow another click
cached.lastSpace = currSpace;
cached.disableSpace = true;
d3.select(window).on('keyup.space-block', function() {
cached.disableSpace = false;
d3.select(window).on('keyup.space-block', null);
});
d3.event.preventDefault();
click();
}
function backspace() {
d3.event.preventDefault();
event.undo();
}
function del() {
d3.event.preventDefault();
event.cancel();
}
function ret() {
d3.event.preventDefault();
event.finish();
}
function draw(selection) {
context.install(hover);
context.install(edit);
if (!context.inIntro() && !cached.usedTails[tail.text()]) {
context.install(tail);
}
keybinding
.on('⌫', backspace)
.on('⌦', del)
.on('⎋', ret)
.on('↩', ret)
.on('space', space)
.on('⌥space', space);
selection
.on('mouseenter.draw', mouseenter)
.on('mouseleave.draw', mouseleave)
.on('mousedown.draw', mousedown)
.on('mousemove.draw', mousemove);
d3.select(document)
.call(keybinding);
return draw;
}
draw.off = function(selection) {
context.ui().sidebar.hover.cancel();
context.uninstall(hover);
context.uninstall(edit);
if (!context.inIntro() && !cached.usedTails[tail.text()]) {
context.uninstall(tail);
cached.usedTails[tail.text()] = true;
}
selection
.on('mouseenter.draw', null)
.on('mouseleave.draw', null)
.on('mousedown.draw', null)
.on('mousemove.draw', null);
d3.select(window)
.on('mouseup.draw', null);
// note: keyup.space-block, click.draw-block should remain
d3.select(document)
.call(keybinding.off);
};
draw.tail = function(_) {
tail.text(_);
return draw;
};
return d3.rebind(draw, event, 'on');
}
Draw.usedTails = {};
Draw.disableSpace = false;
Draw.lastSpace = null;
function AddWay(context) {
var event = d3.dispatch('start', 'startFromWay', 'startFromNode'),
draw = Draw(context);
var addWay = function(surface) {
draw.on('click', event.start)
.on('clickWay', event.startFromWay)
.on('clickNode', event.startFromNode)
.on('cancel', addWay.cancel)
.on('finish', addWay.cancel);
context.map()
.dblclickEnable(false);
surface.call(draw);
};
addWay.off = function(surface) {
surface.call(draw.off);
};
addWay.cancel = function() {
window.setTimeout(function() {
context.map().dblclickEnable(true);
}, 1000);
context.enter(Browse(context));
};
addWay.tail = function(text) {
draw.tail(text);
return addWay;
};
return d3.rebind(addWay, event, 'on');
}
function Breathe(){
var duration = 800,
selector = '.selected.shadow, .selected .shadow',
selected = d3.select(null),
classed = '',
params = {},
done;
function reset(selection) {
selection
.style('stroke-opacity', null)
.style('stroke-width', null)
.style('fill-opacity', null)
.style('r', null);
}
function setAnimationParams(transition, fromTo) {
transition
.style('stroke-opacity', function(d) { return params[d.id][fromTo].opacity; })
.style('stroke-width', function(d) { return params[d.id][fromTo].width; })
.style('fill-opacity', function(d) { return params[d.id][fromTo].opacity; })
.style('r', function(d) { return params[d.id][fromTo].width; });
}
function calcAnimationParams(selection) {
selection
.call(reset)
.each(function(d) {
var s = d3.select(this),
tag = s.node().tagName,
p = {'from': {}, 'to': {}},
opacity, width;
// determine base opacity and width
if (tag === 'circle') {
opacity = parseFloat(s.style('fill-opacity') || 0.5);
width = parseFloat(s.style('r') || 15.5);
} else {
opacity = parseFloat(s.style('stroke-opacity') || 0.7);
width = parseFloat(s.style('stroke-width') || 10);
}
// calculate from/to interpolation params..
p.tag = tag;
p.from.opacity = opacity * 0.6;
p.to.opacity = opacity * 1.25;
p.from.width = width * 0.9;
p.to.width = width * (tag === 'circle' ? 1.5 : 1.25);
params[d.id] = p;
});
}
function run(surface, fromTo) {
var toFrom = (fromTo === 'from' ? 'to': 'from'),
currSelected = surface.selectAll(selector),
currClassed = surface.attr('class'),
n = 0;
if (done || currSelected.empty()) {
selected.call(reset);
return;
}
if (!_.isEqual(currSelected, selected) || currClassed !== classed) {
selected.call(reset);
classed = currClassed;
selected = currSelected.call(calcAnimationParams);
}
selected
.transition()
.call(setAnimationParams, fromTo)
.duration(duration)
.each(function() { ++n; })
.each('end', function() {
if (!--n) { // call once
surface.call(run, toFrom);
}
});
}
var breathe = function(surface) {
done = false;
d3.timer(function() {
if (done) return true;
var currSelected = surface.selectAll(selector);
if (currSelected.empty()) return false;
surface.call(run, 'from');
return true;
}, 200);
};
breathe.off = function() {
done = true;
d3.timer.flush();
selected
.transition()
.call(reset)
.duration(0);
};
return breathe;
}
function Copy(context) {
var keybinding = d3.keybinding('copy');
function groupEntities(ids, graph) {
var entities = ids.map(function (id) { return graph.entity(id); });
return _.extend({relation: [], way: [], node: []},
_.groupBy(entities, function(entity) { return entity.type; }));
}
function getDescendants(id, graph, descendants) {
var entity = graph.entity(id),
i, children;
descendants = descendants || {};
if (entity.type === 'relation') {
children = _.map(entity.members, 'id');
} else if (entity.type === 'way') {
children = entity.nodes;
} else {
children = [];
}
for (i = 0; i < children.length; i++) {
if (!descendants[children[i]]) {
descendants[children[i]] = true;
descendants = getDescendants(children[i], graph, descendants);
}
}
return descendants;
}
function doCopy() {
d3.event.preventDefault();
if (context.inIntro()) return;
var graph = context.graph(),
selected = groupEntities(context.selectedIDs(), graph),
canCopy = [],
skip = {},
i, entity;
for (i = 0; i < selected.relation.length; i++) {
entity = selected.relation[i];
if (!skip[entity.id] && entity.isComplete(graph)) {
canCopy.push(entity.id);
skip = getDescendants(entity.id, graph, skip);
}
}
for (i = 0; i < selected.way.length; i++) {
entity = selected.way[i];
if (!skip[entity.id]) {
canCopy.push(entity.id);
skip = getDescendants(entity.id, graph, skip);
}
}
for (i = 0; i < selected.node.length; i++) {
entity = selected.node[i];
if (!skip[entity.id]) {
canCopy.push(entity.id);
}
}
context.copyIDs(canCopy);
}
function copy() {
keybinding.on(cmd('⌘C'), doCopy);
d3.select(document).call(keybinding);
return copy;
}
copy.off = function() {
d3.select(document).call(keybinding.off);
};
return copy;
}
/*
`iD.behavior.drag` is like `d3.behavior.drag`, with the following differences:
* The `origin` function is expected to return an [x, y] tuple rather than an
{x, y} object.
* The events are `start`, `move`, and `end`.
(https://github.com/mbostock/d3/issues/563)
* The `start` event is not dispatched until the first cursor movement occurs.
(https://github.com/mbostock/d3/pull/368)
* The `move` event has a `point` and `delta` [x, y] tuple properties rather
than `x`, `y`, `dx`, and `dy` properties.
* The `end` event is not dispatched if no movement occurs.
* An `off` function is available that unbinds the drag's internal event handlers.
* Delegation is supported via the `delegate` function.
*/
function drag() {
function d3_eventCancel() {
d3.event.stopPropagation();
d3.event.preventDefault();
}
var event = d3.dispatch('start', 'move', 'end'),
origin = null,
selector = '',
filter = null,
event_, target, surface;
event.of = function(thiz, argumentz) {
return function(e1) {
var e0 = e1.sourceEvent = d3.event;
e1.target = drag;
d3.event = e1;
try {
event[e1.type].apply(thiz, argumentz);
} finally {
d3.event = e0;
}
};
};
var d3_event_userSelectProperty = prefixCSSProperty('UserSelect'),
d3_event_userSelectSuppress = d3_event_userSelectProperty ?
function () {
var selection = d3.selection(),
select = selection.style(d3_event_userSelectProperty);
selection.style(d3_event_userSelectProperty, 'none');
return function () {
selection.style(d3_event_userSelectProperty, select);
};
} :
function (type) {
var w = d3.select(window).on('selectstart.' + type, d3_eventCancel);
return function () {
w.on('selectstart.' + type, null);
};
};
function mousedown() {
target = this;
event_ = event.of(target, arguments);
var eventTarget = d3.event.target,
touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null,
offset,
origin_ = point(),
started = false,
selectEnable = d3_event_userSelectSuppress(touchId !== null ? 'drag-' + touchId : 'drag');
var w = d3.select(window)
.on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', dragmove)
.on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', dragend, true);
if (origin) {
offset = origin.apply(target, arguments);
offset = [offset[0] - origin_[0], offset[1] - origin_[1]];
} else {
offset = [0, 0];
}
if (touchId === null) d3.event.stopPropagation();
function point() {
var p = target.parentNode || surface;
return touchId !== null ? d3.touches(p).filter(function(p) {
return p.identifier === touchId;
})[0] : d3.mouse(p);
}
function dragmove() {
var p = point(),
dx = p[0] - origin_[0],
dy = p[1] - origin_[1];
if (dx === 0 && dy === 0)
return;
if (!started) {
started = true;
event_({
type: 'start'
});
}
origin_ = p;
d3_eventCancel();
event_({
type: 'move',
point: [p[0] + offset[0], p[1] + offset[1]],
delta: [dx, dy]
});
}
function dragend() {
if (started) {
event_({
type: 'end'
});
d3_eventCancel();
if (d3.event.target === eventTarget) w.on('click.drag', click, true);
}
w.on(touchId !== null ? 'touchmove.drag-' + touchId : 'mousemove.drag', null)
.on(touchId !== null ? 'touchend.drag-' + touchId : 'mouseup.drag', null);
selectEnable();
}
function click() {
d3_eventCancel();
w.on('click.drag', null);
}
}
function drag(selection) {
var matchesSelector = prefixDOMProperty('matchesSelector'),
delegate = mousedown;
if (selector) {
delegate = function() {
var root = this,
target = d3.event.target;
for (; target && target !== root; target = target.parentNode) {
if (target[matchesSelector](selector) &&
(!filter || filter(target.__data__))) {
return mousedown.call(target, target.__data__);
}
}
};
}
selection.on('mousedown.drag' + selector, delegate)
.on('touchstart.drag' + selector, delegate);
}
drag.off = function(selection) {
selection.on('mousedown.drag' + selector, null)
.on('touchstart.drag' + selector, null);
};
drag.delegate = function(_) {
if (!arguments.length) return selector;
selector = _;
return drag;
};
drag.filter = function(_) {
if (!arguments.length) return origin;
filter = _;
return drag;
};
drag.origin = function (_) {
if (!arguments.length) return origin;
origin = _;
return drag;
};
drag.cancel = function() {
d3.select(window)
.on('mousemove.drag', null)
.on('mouseup.drag', null);
return drag;
};
drag.target = function() {
if (!arguments.length) return target;
target = arguments[0];
event_ = event.of(target, Array.prototype.slice.call(arguments, 1));
return drag;
};
drag.surface = function() {
if (!arguments.length) return surface;
surface = arguments[0];
return drag;
};
return d3.rebind(drag, event, 'on');
}
function DrawWay(context, wayId, index, mode, baseGraph) {
var way = context.entity(wayId),
isArea = context.geometry(wayId) === 'area',
finished = false,
annotation = t((way.isDegenerate() ?
'operations.start.annotation.' :
'operations.continue.annotation.') + context.geometry(wayId)),
draw = Draw(context);
var startIndex = typeof index === 'undefined' ? way.nodes.length - 1 : 0,
start = Node({loc: context.graph().entity(way.nodes[startIndex]).loc}),
end = Node({loc: context.map().mouseCoordinates()}),
segment = Way({
nodes: typeof index === 'undefined' ? [start.id, end.id] : [end.id, start.id],
tags: _.clone(way.tags)
});
var f = context[way.isDegenerate() ? 'replace' : 'perform'];
if (isArea) {
f(AddEntity(end),
AddVertex(wayId, end.id, index));
} else {
f(AddEntity(start),
AddEntity(end),
AddEntity(segment));
}
function move(datum) {
var loc;
if (datum.type === 'node' && datum.id !== end.id) {
loc = datum.loc;
} else if (datum.type === 'way' && datum.id !== segment.id) {
var dims = context.map().dimensions(),
mouse = context.mouse(),
pad = 5,
trySnap = mouse[0] > pad && mouse[0] < dims[0] - pad &&
mouse[1] > pad && mouse[1] < dims[1] - pad;
if (trySnap) {
loc = chooseEdge(context.childNodes(datum), context.mouse(), context.projection).loc;
}
}
if (!loc) {
loc = context.map().mouseCoordinates();
}
context.replace(MoveNode(end.id, loc));
}
function undone() {
finished = true;
context.enter(Browse(context));
}
function setActiveElements() {
var active = isArea ? [wayId, end.id] : [segment.id, start.id, end.id];
context.surface().selectAll(entitySelector(active))
.classed('active', true);
}
var drawWay = function(surface) {
draw.on('move', move)
.on('click', drawWay.add)
.on('clickWay', drawWay.addWay)
.on('clickNode', drawWay.addNode)
.on('undo', context.undo)
.on('cancel', drawWay.cancel)
.on('finish', drawWay.finish);
context.map()
.dblclickEnable(false)
.on('drawn.draw', setActiveElements);
setActiveElements();
surface.call(draw);
context.history()
.on('undone.draw', undone);
};
drawWay.off = function(surface) {
if (!finished)
context.pop();
context.map()
.on('drawn.draw', null);
surface.call(draw.off)
.selectAll('.active')
.classed('active', false);
context.history()
.on('undone.draw', null);
};
function ReplaceTemporaryNode(newNode) {
return function(graph) {
if (isArea) {
return graph
.replace(way.addNode(newNode.id, index))
.remove(end);
} else {
return graph
.replace(graph.entity(wayId).addNode(newNode.id, index))
.remove(end)
.remove(segment)
.remove(start);
}
};
}
// Accept the current position of the temporary node and continue drawing.
drawWay.add = function(loc) {
// prevent duplicate nodes
var last = context.hasEntity(way.nodes[way.nodes.length - (isArea ? 2 : 1)]);
if (last && last.loc[0] === loc[0] && last.loc[1] === loc[1]) return;
var newNode = Node({loc: loc});
context.replace(
AddEntity(newNode),
ReplaceTemporaryNode(newNode),
annotation);
finished = true;
context.enter(mode);
};
// Connect the way to an existing way.
drawWay.addWay = function(loc, edge) {
var previousEdge = startIndex ?
[way.nodes[startIndex], way.nodes[startIndex - 1]] :
[way.nodes[0], way.nodes[1]];
// Avoid creating duplicate segments
if (!isArea && edgeEqual(edge, previousEdge))
return;
var newNode = Node({ loc: loc });
context.perform(
AddMidpoint({ loc: loc, edge: edge}, newNode),
ReplaceTemporaryNode(newNode),
annotation);
finished = true;
context.enter(mode);
};
// Connect the way to an existing node and continue drawing.
drawWay.addNode = function(node) {
// Avoid creating duplicate segments
if (way.areAdjacent(node.id, way.nodes[way.nodes.length - 1])) return;
context.perform(
ReplaceTemporaryNode(node),
annotation);
finished = true;
context.enter(mode);
};
// Finish the draw operation, removing the temporary node. If the way has enough
// nodes to be valid, it's selected. Otherwise, return to browse mode.
drawWay.finish = function() {
context.pop();
finished = true;
window.setTimeout(function() {
context.map().dblclickEnable(true);
}, 1000);
if (context.hasEntity(wayId)) {
context.enter(
SelectMode(context, [wayId])
.suppressMenu(true)
.newFeature(true));
} else {
context.enter(Browse(context));
}
};
// Cancel the draw operation and return to browse, deleting everything drawn.
drawWay.cancel = function() {
context.perform(
d3.functor(baseGraph),
t('operations.cancel_draw.annotation'));
window.setTimeout(function() {
context.map().dblclickEnable(true);
}, 1000);
finished = true;
context.enter(Browse(context));
};
drawWay.tail = function(text) {
draw.tail(text);
return drawWay;
};
return drawWay;
}
function Hash(context) {
var s0 = null, // cached location.hash
lat = 90 - 1e-8; // allowable latitude range
var parser = function(map, s) {
var q = stringQs(s);
var args = (q.map || '').split('/').map(Number);
if (args.length < 3 || args.some(isNaN)) {
return true; // replace bogus hash
} else if (s !== formatter(map).slice(1)) {
map.centerZoom([args[1],
Math.min(lat, Math.max(-lat, args[2]))], args[0]);
}
};
var formatter = function(map) {
var mode = context.mode(),
center = map.center(),
zoom = map.zoom(),
precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)),
q = _.omit(stringQs(location.hash.substring(1)), 'comment'),
newParams = {};
if (mode && mode.id === 'browse') {
delete q.id;
} else {
var selected = context.selectedIDs().filter(function(id) {
return !context.entity(id).isNew();
});
if (selected.length) {
newParams.id = selected.join(',');
}
}
newParams.map = zoom.toFixed(2) +
'/' + center[0].toFixed(precision) +
'/' + center[1].toFixed(precision);
return '#' + qsString(_.assign(q, newParams), true);
};
function update() {
if (context.inIntro()) return;
var s1 = formatter(context.map());
if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map!
}
var throttledUpdate = _.throttle(update, 500);
function hashchange() {
if (location.hash === s0) return; // ignore spurious hashchange events
if (parser(context.map(), (s0 = location.hash).substring(1))) {
update(); // replace bogus hash
}
}
function hash() {
context.map()
.on('move.hash', throttledUpdate);
context
.on('enter.hash', throttledUpdate);
d3.select(window)
.on('hashchange.hash', hashchange);
if (location.hash) {
var q = stringQs(location.hash.substring(1));
if (q.id) context.zoomToEntity(q.id.split(',')[0], !q.map);
if (q.comment) context.storage('comment', q.comment);
hashchange();
if (q.map) hash.hadHash = true;
}
}
hash.off = function() {
throttledUpdate.cancel();
context.map()
.on('move.hash', null);
context
.on('enter.hash', null);
d3.select(window)
.on('hashchange.hash', null);
location.hash = '';
};
return hash;
}
function Lasso(context) {
var behavior = function(selection) {
var lasso;
function mousedown() {
var button = 0; // left
if (d3.event.button === button && d3.event.shiftKey === true) {
lasso = null;
selection
.on('mousemove.lasso', mousemove)
.on('mouseup.lasso', mouseup);
d3.event.stopPropagation();
}
}
function mousemove() {
if (!lasso) {
lasso = uiLasso(context);
context.surface().call(lasso);
}
lasso.p(context.mouse());
}
function normalize(a, b) {
return [
[Math.min(a[0], b[0]), Math.min(a[1], b[1])],
[Math.max(a[0], b[0]), Math.max(a[1], b[1])]];
}
function lassoed() {
if (!lasso) return [];
var graph = context.graph(),
bounds = lasso.extent().map(context.projection.invert),
extent = Extent(normalize(bounds[0], bounds[1]));
return _.map(context.intersects(extent).filter(function(entity) {
return entity.type === 'node' &&
pointInPolygon(context.projection(entity.loc), lasso.coordinates) &&
!context.features().isHidden(entity, graph, entity.geometry(graph));
}), 'id');
}
function mouseup() {
selection
.on('mousemove.lasso', null)
.on('mouseup.lasso', null);
if (!lasso) return;
var ids = lassoed();
lasso.close();
if (ids.length) {
context.enter(SelectMode(context, ids));
}
}
selection
.on('mousedown.lasso', mousedown);
};
behavior.off = function(selection) {
selection.on('mousedown.lasso', null);
};
return behavior;
}
function Paste(context) {
var keybinding = d3.keybinding('paste');
function omitTag(v, k) {
return (
k === 'phone' ||
k === 'fax' ||
k === 'email' ||
k === 'website' ||
k === 'url' ||
k === 'note' ||
k === 'description' ||
k.indexOf('name') !== -1 ||
k.indexOf('wiki') === 0 ||
k.indexOf('addr:') === 0 ||
k.indexOf('contact:') === 0
);
}
function doPaste() {
d3.event.preventDefault();
if (context.inIntro()) return;
var baseGraph = context.graph(),
mouse = context.mouse(),
projection = context.projection,
viewport = Extent(projection.clipExtent()).polygon();
if (!pointInPolygon(mouse, viewport)) return;
var extent = Extent(),
oldIDs = context.copyIDs(),
oldGraph = context.copyGraph(),
newIDs = [];
if (!oldIDs.length) return;
var action = CopyEntities(oldIDs, oldGraph);
context.perform(action);
var copies = action.copies();
for (var id in copies) {
var oldEntity = oldGraph.entity(id),
newEntity = copies[id];
extent._extend(oldEntity.extent(oldGraph));
newIDs.push(newEntity.id);
context.perform(ChangeTags(newEntity.id, _.omit(newEntity.tags, omitTag)));
}
// Put pasted objects where mouse pointer is..
var center = projection(extent.center()),
delta = [ mouse[0] - center[0], mouse[1] - center[1] ];
context.perform(MoveAction(newIDs, delta, projection));
context.enter(MoveMode(context, newIDs, baseGraph));
}
function paste() {
keybinding.on(cmd('⌘V'), doPaste);
d3.select(document).call(keybinding);
return paste;
}
paste.off = function() {
d3.select(document).call(keybinding.off);
};
return paste;
}
function Select(context) {
function keydown() {
if (d3.event && d3.event.shiftKey) {
context.surface()
.classed('behavior-multiselect', true);
}
}
function keyup() {
if (!d3.event || !d3.event.shiftKey) {
context.surface()
.classed('behavior-multiselect', false);
}
}
function click() {
var datum = d3.event.target.__data__,
lasso = d3.select('#surface .lasso').node(),
mode = context.mode();
if (!(datum instanceof Entity)) {
if (!d3.event.shiftKey && !lasso && mode.id !== 'browse')
context.enter(Browse(context));
} else if (!d3.event.shiftKey && !lasso) {
// Avoid re-entering Select mode with same entity.
if (context.selectedIDs().length !== 1 || context.selectedIDs()[0] !== datum.id) {
context.enter(SelectMode(context, [datum.id]));
} else {
mode.suppressMenu(false).reselect();
}
} else if (context.selectedIDs().indexOf(datum.id) >= 0) {
var selectedIDs = _.without(context.selectedIDs(), datum.id);
context.enter(selectedIDs.length ?
SelectMode(context, selectedIDs) :
Browse(context));
} else {
context.enter(SelectMode(context, context.selectedIDs().concat([datum.id])));
}
}
var behavior = function(selection) {
d3.select(window)
.on('keydown.select', keydown)
.on('keyup.select', keyup);
selection.on('click.select', click);
keydown();
};
behavior.off = function(selection) {
d3.select(window)
.on('keydown.select', null)
.on('keyup.select', null);
selection.on('click.select', null);
keyup();
};
return behavior;
}
var behavior = Object.freeze({
AddWay: AddWay,
Breathe: Breathe,
Copy: Copy,
drag: drag,
DrawWay: DrawWay,
Draw: Draw,
Edit: Edit,
Hash: Hash,
Hover: Hover,
Lasso: Lasso,
Paste: Paste,
Select: Select,
Tail: Tail
});
function Collection(collection) {
var maxSearchResults = 50,
maxSuggestionResults = 10;
var presets = {
collection: collection,
item: function(id) {
return _.find(collection, function(d) {
return d.id === id;
});
},
matchGeometry: function(geometry) {
return Collection(collection.filter(function(d) {
return d.matchGeometry(geometry);
}));
},
search: function(value, geometry) {
if (!value) return this;
value = value.toLowerCase();
var searchable = _.filter(collection, function(a) {
return a.searchable !== false && a.suggestion !== true;
}),
suggestions = _.filter(collection, function(a) {
return a.suggestion === true;
});
function leading(a) {
var index = a.indexOf(value);
return index === 0 || a[index - 1] === ' ';
}
// matches value to preset.name
var leading_name = _.filter(searchable, function(a) {
return leading(a.name().toLowerCase());
}).sort(function(a, b) {
var i = a.name().toLowerCase().indexOf(value) - b.name().toLowerCase().indexOf(value);
if (i === 0) return a.name().length - b.name().length;
else return i;
});
// matches value to preset.terms values
var leading_terms = _.filter(searchable, function(a) {
return _.some(a.terms() || [], leading);
});
// matches value to preset.tags values
var leading_tag_values = _.filter(searchable, function(a) {
return _.some(_.without(_.values(a.tags || {}), '*'), leading);
});
// finds close matches to value in preset.name
var levenstein_name = searchable.map(function(a) {
return {
preset: a,
dist: editDistance(value, a.name().toLowerCase())
};
}).filter(function(a) {
return a.dist + Math.min(value.length - a.preset.name().length, 0) < 3;
}).sort(function(a, b) {
return a.dist - b.dist;
}).map(function(a) {
return a.preset;
});
// finds close matches to value in preset.terms
var leventstein_terms = _.filter(searchable, function(a) {
return _.some(a.terms() || [], function(b) {
return editDistance(value, b) + Math.min(value.length - b.length, 0) < 3;
});
});
function suggestionName(name) {
var nameArray = name.split(' - ');
if (nameArray.length > 1) {
name = nameArray.slice(0, nameArray.length-1).join(' - ');
}
return name.toLowerCase();
}
var leading_suggestions = _.filter(suggestions, function(a) {
return leading(suggestionName(a.name()));
}).sort(function(a, b) {
a = suggestionName(a.name());
b = suggestionName(b.name());
var i = a.indexOf(value) - b.indexOf(value);
if (i === 0) return a.length - b.length;
else return i;
});
var leven_suggestions = suggestions.map(function(a) {
return {
preset: a,
dist: editDistance(value, suggestionName(a.name()))
};
}).filter(function(a) {
return a.dist + Math.min(value.length - suggestionName(a.preset.name()).length, 0) < 1;
}).sort(function(a, b) {
return a.dist - b.dist;
}).map(function(a) {
return a.preset;
});
var other = presets.item(geometry);
var results = leading_name.concat(
leading_terms,
leading_tag_values,
leading_suggestions.slice(0, maxSuggestionResults+5),
levenstein_name,
leventstein_terms,
leven_suggestions.slice(0, maxSuggestionResults)
).slice(0, maxSearchResults-1);
return Collection(_.uniq(
results.concat(other)
));
}
};
return presets;
}
function Category(id, category, all) {
category = _.clone(category);
category.id = id;
category.members = Collection(category.members.map(function(id) {
return all.item(id);
}));
category.matchGeometry = function(geometry) {
return category.geometry.indexOf(geometry) >= 0;
};
category.matchScore = function() { return -1; };
category.name = function() {
return t('presets.categories.' + id + '.name', {'default': id});
};
category.terms = function() {
return [];
};
return category;
}
function Field(id, field) {
field = _.clone(field);
field.id = id;
field.matchGeometry = function(geometry) {
return !field.geometry || field.geometry === geometry;
};
field.t = function(scope, options) {
return t('presets.fields.' + id + '.' + scope, options);
};
field.label = function() {
return field.t('label', {'default': id});
};
var placeholder = field.placeholder;
field.placeholder = function() {
return field.t('placeholder', {'default': placeholder});
};
return field;
}
function Preset(id, preset, fields) {
preset = _.clone(preset);
preset.id = id;
preset.fields = (preset.fields || []).map(getFields);
preset.geometry = (preset.geometry || []);
function getFields(f) {
return fields[f];
}
preset.matchGeometry = function(geometry) {
return preset.geometry.indexOf(geometry) >= 0;
};
var matchScore = preset.matchScore || 1;
preset.matchScore = function(entity) {
var tags = preset.tags,
score = 0;
for (var t in tags) {
if (entity.tags[t] === tags[t]) {
score += matchScore;
} else if (tags[t] === '*' && t in entity.tags) {
score += matchScore / 2;
} else {
return -1;
}
}
return score;
};
preset.t = function(scope, options) {
return t('presets.presets.' + id + '.' + scope, options);
};
var name = preset.name;
preset.name = function() {
if (preset.suggestion) {
id = id.split('/');
id = id[0] + '/' + id[1];
return name + ' - ' + t('presets.presets.' + id + '.name');
}
return preset.t('name', {'default': name});
};
preset.terms = function() {
return preset.t('terms', {'default': ''}).toLowerCase().trim().split(/\s*,+\s*/);
};
preset.isFallback = function() {
var tagCount = Object.keys(preset.tags).length;
return tagCount === 0 || (tagCount === 1 && preset.tags.hasOwnProperty('area'));
};
preset.reference = function(geometry) {
var key = Object.keys(preset.tags)[0],
value = preset.tags[key];
if (geometry === 'relation' && key === 'type') {
return { rtype: value };
} else if (value === '*') {
return { key: key };
} else {
return { key: key, value: value };
}
};
var removeTags = preset.removeTags || preset.tags;
preset.removeTags = function(tags, geometry) {
tags = _.omit(tags, _.keys(removeTags));
for (var f in preset.fields) {
var field = preset.fields[f];
if (field.matchGeometry(geometry) && field.default === tags[field.key]) {
delete tags[field.key];
}
}
delete tags.area;
return tags;
};
var applyTags = preset.addTags || preset.tags;
preset.applyTags = function(tags, geometry) {
var k;
tags = _.clone(tags);
for (k in applyTags) {
if (applyTags[k] === '*') {
tags[k] = 'yes';
} else {
tags[k] = applyTags[k];
}
}
// Add area=yes if necessary.
// This is necessary if the geometry is already an area (e.g. user drew an area) AND any of:
// 1. chosen preset could be either an area or a line (`barrier=city_wall`)
// 2. chosen preset doesn't have a key in areaKeys (`railway=station`)
if (geometry === 'area') {
var needsAreaTag = true;
if (preset.geometry.indexOf('line') === -1) {
for (k in applyTags) {
if (k in iD.areaKeys) {
needsAreaTag = false;
break;
}
}
}
if (needsAreaTag) {
tags.area = 'yes';
}
}
for (var f in preset.fields) {
var field = preset.fields[f];
if (field.matchGeometry(geometry) && field.key && !tags[field.key] && field.default) {
tags[field.key] = field.default;
}
}
return tags;
};
return preset;
}
function presets$1() {
// an iD.presets.Collection with methods for
// loading new data and returning defaults
var all = Collection([]),
defaults = { area: all, line: all, point: all, vertex: all, relation: all },
fields = {},
universal = [],
recent = Collection([]);
// Index of presets by (geometry, tag key).
var index = {
point: {},
vertex: {},
line: {},
area: {},
relation: {}
};
all.match = function(entity, resolver) {
var geometry = entity.geometry(resolver),
geometryMatches = index[geometry],
best = -1,
match;
for (var k in entity.tags) {
var keyMatches = geometryMatches[k];
if (!keyMatches) continue;
for (var i = 0; i < keyMatches.length; i++) {
var score = keyMatches[i].matchScore(entity);
if (score > best) {
best = score;
match = keyMatches[i];
}
}
}
return match || all.item(geometry);
};
// Because of the open nature of tagging, iD will never have a complete
// list of tags used in OSM, so we want it to have logic like "assume
// that a closed way with an amenity tag is an area, unless the amenity
// is one of these specific types". This function computes a structure
// that allows testing of such conditions, based on the presets designated
// as as supporting (or not supporting) the area geometry.
//
// The returned object L is a whitelist/blacklist of tags. A closed way
// with a tag (k, v) is considered to be an area if `k in L && !(v in L[k])`
// (see `Way#isArea()`). In other words, the keys of L form the whitelist,
// and the subkeys form the blacklist.
all.areaKeys = function() {
var areaKeys = {},
ignore = ['barrier', 'highway', 'footway', 'railway', 'type'],
presets = _.reject(all.collection, 'suggestion');
// whitelist
presets.forEach(function(d) {
for (var key in d.tags) break;
if (!key) return;
if (ignore.indexOf(key) !== -1) return;
if (d.geometry.indexOf('area') !== -1) {
areaKeys[key] = areaKeys[key] || {};
}
});
// blacklist
presets.forEach(function(d) {
for (var key in d.tags) break;
if (!key) return;
if (ignore.indexOf(key) !== -1) return;
var value = d.tags[key];
if (d.geometry.indexOf('area') === -1 &&
d.geometry.indexOf('line') !== -1 &&
key in areaKeys && value !== '*') {
areaKeys[key][value] = true;
}
});
return areaKeys;
};
all.load = function(d) {
if (d.fields) {
_.forEach(d.fields, function(d, id) {
fields[id] = Field(id, d);
if (d.universal) universal.push(fields[id]);
});
}
if (d.presets) {
_.forEach(d.presets, function(d, id) {
all.collection.push(Preset(id, d, fields));
});
}
if (d.categories) {
_.forEach(d.categories, function(d, id) {
all.collection.push(Category(id, d, all));
});
}
if (d.defaults) {
var getItem = _.bind(all.item, all);
defaults = {
area: Collection(d.defaults.area.map(getItem)),
line: Collection(d.defaults.line.map(getItem)),
point: Collection(d.defaults.point.map(getItem)),
vertex: Collection(d.defaults.vertex.map(getItem)),
relation: Collection(d.defaults.relation.map(getItem))
};
}
for (var i = 0; i < all.collection.length; i++) {
var preset = all.collection[i],
geometry = preset.geometry;
for (var j = 0; j < geometry.length; j++) {
var g = index[geometry[j]];
for (var k in preset.tags) {
(g[k] = g[k] || []).push(preset);
}
}
}
return all;
};
all.field = function(id) {
return fields[id];
};
all.universal = function() {
return universal;
};
all.defaults = function(geometry, n) {
var rec = recent.matchGeometry(geometry).collection.slice(0, 4),
def = _.uniq(rec.concat(defaults[geometry].collection)).slice(0, n - 1);
return Collection(_.uniq(rec.concat(def).concat(all.item(geometry))));
};
all.choose = function(preset) {
if (!preset.isFallback()) {
recent = Collection(_.uniq([preset].concat(recent.collection)));
}
return all;
};
return all;
}
var presets = Object.freeze({
Category: Category,
Collection: Collection,
Field: Field,
Preset: Preset,
presets: presets$1
});
function DeprecatedTag() {
var validation = function(changes) {
var warnings = [];
for (var i = 0; i < changes.created.length; i++) {
var change = changes.created[i],
deprecatedTags = change.deprecatedTags();
if (!_.isEmpty(deprecatedTags)) {
var tags = tagText({ tags: deprecatedTags });
warnings.push({
id: 'deprecated_tags',
message: t('validations.deprecated_tags', { tags: tags }),
entity: change
});
}
}
return warnings;
};
return validation;
}
function ManyDeletions() {
var threshold = 100;
var validation = function(changes) {
var warnings = [];
if (changes.deleted.length > threshold) {
warnings.push({
id: 'many_deletions',
message: t('validations.many_deletions', { n: changes.deleted.length })
});
}
return warnings;
};
return validation;
}
function MissingTag() {
// Slightly stricter check than Entity#isUsed (#3091)
function hasTags(entity, graph) {
return _.without(Object.keys(entity.tags), 'area', 'name').length > 0 ||
graph.parentRelations(entity).length > 0;
}
var validation = function(changes, graph) {
var warnings = [];
for (var i = 0; i < changes.created.length; i++) {
var change = changes.created[i],
geometry = change.geometry(graph);
if ((geometry === 'point' || geometry === 'line' || geometry === 'area') && !hasTags(change, graph)) {
warnings.push({
id: 'missing_tag',
message: t('validations.untagged_' + geometry),
tooltip: t('validations.untagged_' + geometry + '_tooltip'),
entity: change
});
}
}
return warnings;
};
return validation;
}
function TagSuggestsArea() {
// https://github.com/openstreetmap/josm/blob/mirror/src/org/
// openstreetmap/josm/data/validation/tests/UnclosedWays.java#L80
function tagSuggestsArea(tags) {
if (_.isEmpty(tags)) return false;
var presence = ['landuse', 'amenities', 'tourism', 'shop'];
for (var i = 0; i < presence.length; i++) {
if (tags[presence[i]] !== undefined) {
return presence[i] + '=' + tags[presence[i]];
}
}
if (tags.building && tags.building === 'yes') return 'building=yes';
}
var validation = function(changes, graph) {
var warnings = [];
for (var i = 0; i < changes.created.length; i++) {
var change = changes.created[i],
geometry = change.geometry(graph),
suggestion = (geometry === 'line' ? tagSuggestsArea(change.tags) : undefined);
if (suggestion) {
warnings.push({
id: 'tag_suggests_area',
message: t('validations.tag_suggests_area', { tag: suggestion }),
entity: change
});
}
}
return warnings;
};
return validation;
}
var validations = Object.freeze({
DeprecatedTag: DeprecatedTag,
ManyDeletions: ManyDeletions,
MissingTag: MissingTag,
TagSuggestsArea: TagSuggestsArea
});
function BackgroundSource(data) {
var source = _.clone(data),
offset = [0, 0],
name = source.name,
best = !!source.best;
source.scaleExtent = data.scaleExtent || [0, 20];
source.overzoom = data.overzoom !== false;
source.offset = function(_) {
if (!arguments.length) return offset;
offset = _;
return source;
};
source.nudge = function(_, zoomlevel) {
offset[0] += _[0] / Math.pow(2, zoomlevel);
offset[1] += _[1] / Math.pow(2, zoomlevel);
return source;
};
source.name = function() {
return name;
};
source.best = function() {
return best;
};
source.area = function() {
if (!data.polygon) return Number.MAX_VALUE; // worldwide
var area = d3.geo.area({ type: 'MultiPolygon', coordinates: [ data.polygon ] });
return isNaN(area) ? 0 : area;
};
source.imageryUsed = function() {
return source.id || name;
};
source.url = function(coord) {
return data.template
.replace('{x}', coord[0])
.replace('{y}', coord[1])
// TMS-flipped y coordinate
.replace(/\{[t-]y\}/, Math.pow(2, coord[2]) - coord[1] - 1)
.replace(/\{z(oom)?\}/, coord[2])
.replace(/\{switch:([^}]+)\}/, function(s, r) {
var subdomains = r.split(',');
return subdomains[(coord[0] + coord[1]) % subdomains.length];
})
.replace('{u}', function() {
var u = '';
for (var zoom = coord[2]; zoom > 0; zoom--) {
var b = 0;
var mask = 1 << (zoom - 1);
if ((coord[0] & mask) !== 0) b++;
if ((coord[1] & mask) !== 0) b += 2;
u += b.toString();
}
return u;
});
};
source.intersects = function(extent) {
extent = extent.polygon();
return !data.polygon || data.polygon.some(function(polygon) {
return polygonIntersectsPolygon(polygon, extent, true);
});
};
source.validZoom = function(z) {
return source.scaleExtent[0] <= z &&
(source.overzoom || source.scaleExtent[1] > z);
};
source.isLocatorOverlay = function() {
return name === 'Locator Overlay';
};
source.copyrightNotices = function() {};
return source;
}
BackgroundSource.Bing = function(data, dispatch) {
// http://msdn.microsoft.com/en-us/library/ff701716.aspx
// http://msdn.microsoft.com/en-us/library/ff701701.aspx
data.template = 'https://ecn.t{switch:0,1,2,3}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=587&mkt=en-gb&n=z';
var bing = BackgroundSource(data),
key = 'Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU', // Same as P2 and JOSM
url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?include=ImageryProviders&key=' +
key + '&jsonp={callback}',
providers = [];
d3.jsonp(url, function(json) {
providers = json.resourceSets[0].resources[0].imageryProviders.map(function(provider) {
return {
attribution: provider.attribution,
areas: provider.coverageAreas.map(function(area) {
return {
zoom: [area.zoomMin, area.zoomMax],
extent: Extent([area.bbox[1], area.bbox[0]], [area.bbox[3], area.bbox[2]])
};
})
};
});
dispatch.change();
});
bing.copyrightNotices = function(zoom, extent) {
zoom = Math.min(zoom, 21);
return providers.filter(function(provider) {
return _.some(provider.areas, function(area) {
return extent.intersects(area.extent) &&
area.zoom[0] <= zoom &&
area.zoom[1] >= zoom;
});
}).map(function(provider) {
return provider.attribution;
}).join(', ');
};
bing.logo = 'bing_maps.png';
bing.terms_url = 'https://blog.openstreetmap.org/2010/11/30/microsoft-imagery-details';
return bing;
};
BackgroundSource.None = function() {
var source = BackgroundSource({id: 'none', template: ''});
source.name = function() {
return t('background.none');
};
source.imageryUsed = function() {
return 'None';
};
source.area = function() {
return -1;
};
return source;
};
BackgroundSource.Custom = function(template) {
var source = BackgroundSource({id: 'custom', template: template});
source.name = function() {
return t('background.custom');
};
source.imageryUsed = function() {
return 'Custom (' + template + ')';
};
source.area = function() {
return -2;
};
return source;
};
function TileLayer(context) {
var tileSize = 256,
tile = d3.geo.tile(),
projection,
cache = {},
tileOrigin,
z,
transformProp = prefixCSSProperty('Transform'),
source = d3.functor('');
// blacklist overlay tiles around Null Island..
function nearNullIsland(x, y, z) {
if (z >= 7) {
var center = Math.pow(2, z - 1),
width = Math.pow(2, z - 6),
min = center - (width / 2),
max = center + (width / 2) - 1;
return x >= min && x <= max && y >= min && y <= max;
}
return false;
}
function tileSizeAtZoom(d, z) {
var epsilon = 0.002;
return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + epsilon;
}
function atZoom(t, distance) {
var power = Math.pow(2, distance);
return [
Math.floor(t[0] * power),
Math.floor(t[1] * power),
t[2] + distance];
}
function lookUp(d) {
for (var up = -1; up > -d[2]; up--) {
var tile = atZoom(d, up);
if (cache[source.url(tile)] !== false) {
return tile;
}
}
}
function uniqueBy(a, n) {
var o = [], seen = {};
for (var i = 0; i < a.length; i++) {
if (seen[a[i][n]] === undefined) {
o.push(a[i]);
seen[a[i][n]] = true;
}
}
return o;
}
function addSource(d) {
d.push(source.url(d));
return d;
}
// Update tiles based on current state of `projection`.
function background(selection) {
tile.scale(projection.scale() * 2 * Math.PI)
.translate(projection.translate());
tileOrigin = [
projection.scale() * Math.PI - projection.translate()[0],
projection.scale() * Math.PI - projection.translate()[1]];
z = Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.log(2) - 8, 0);
render(selection);
}
// Derive the tiles onscreen, remove those offscreen and position them.
// Important that this part not depend on `projection` because it's
// rentered when tiles load/error (see #644).
function render(selection) {
var requests = [];
var showDebug = context.getDebug('tile') && !source.overlay;
if (source.validZoom(z)) {
tile().forEach(function(d) {
addSource(d);
if (d[3] === '') return;
if (typeof d[3] !== 'string') return; // Workaround for chrome crash https://github.com/openstreetmap/iD/issues/2295
requests.push(d);
if (cache[d[3]] === false && lookUp(d)) {
requests.push(addSource(lookUp(d)));
}
});
requests = uniqueBy(requests, 3).filter(function(r) {
if (!!source.overlay && nearNullIsland(r[0], r[1], r[2])) {
return false;
}
// don't re-request tiles which have failed in the past
return cache[r[3]] !== false;
});
}
var pixelOffset = [
source.offset()[0] * Math.pow(2, z),
source.offset()[1] * Math.pow(2, z)
];
function load(d) {
cache[d[3]] = true;
d3.select(this)
.on('error', null)
.on('load', null)
.classed('tile-loaded', true);
render(selection);
}
function error(d) {
cache[d[3]] = false;
d3.select(this)
.on('error', null)
.on('load', null)
.remove();
render(selection);
}
function imageTransform(d) {
var _ts = tileSize * Math.pow(2, z - d[2]);
var scale = tileSizeAtZoom(d, z);
return 'translate(' +
((d[0] * _ts) - tileOrigin[0] + pixelOffset[0]) + 'px,' +
((d[1] * _ts) - tileOrigin[1] + pixelOffset[1]) + 'px)' +
'scale(' + scale + ',' + scale + ')';
}
function debugTransform(d) {
var _ts = tileSize * Math.pow(2, z - d[2]);
var scale = tileSizeAtZoom(d, z);
return 'translate(' +
((d[0] * _ts) - tileOrigin[0] + pixelOffset[0] + scale * (tileSize / 4)) + 'px,' +
((d[1] * _ts) - tileOrigin[1] + pixelOffset[1] + scale * (tileSize / 2)) + 'px)';
}
var image = selection
.selectAll('img')
.data(requests, function(d) { return d[3]; });
image.exit()
.style(transformProp, imageTransform)
.classed('tile-removing', true)
.each(function() {
var tile = d3.select(this);
window.setTimeout(function() {
if (tile.classed('tile-removing')) {
tile.remove();
}
}, 300);
});
image.enter().append('img')
.attr('class', 'tile')
.attr('src', function(d) { return d[3]; })
.on('error', error)
.on('load', load);
image
.style(transformProp, imageTransform)
.classed('tile-debug', showDebug)
.classed('tile-removing', false);
var debug = selection.selectAll('.tile-label-debug')
.data(showDebug ? requests : [], function(d) { return d[3]; });
debug.exit()
.remove();
debug.enter()
.append('div')
.attr('class', 'tile-label-debug');
debug
.text(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; })
.style(transformProp, debugTransform);
}
background.projection = function(_) {
if (!arguments.length) return projection;
projection = _;
return background;
};
background.dimensions = function(_) {
if (!arguments.length) return tile.size();
tile.size(_);
return background;
};
background.source = function(_) {
if (!arguments.length) return source;
source = _;
cache = {};
tile.scaleExtent(source.scaleExtent);
return background;
};
return background;
}
function Background$1(context) {
var dispatch = d3.dispatch('change'),
baseLayer = TileLayer(context).projection(context.projection),
overlayLayers = [],
backgroundSources;
function findSource(id) {
return _.find(backgroundSources, function(d) {
return d.id && d.id === id;
});
}
function background(selection) {
var base = selection.selectAll('.layer-background')
.data([0]);
base.enter()
.insert('div', '.layer-data')
.attr('class', 'layer layer-background');
base.call(baseLayer);
var overlays = selection.selectAll('.layer-overlay')
.data(overlayLayers, function(d) { return d.source().name(); });
overlays.enter()
.insert('div', '.layer-data')
.attr('class', 'layer layer-overlay');
overlays.each(function(layer) {
d3.select(this).call(layer);
});
overlays.exit()
.remove();
}
background.updateImagery = function() {
var b = background.baseLayerSource(),
o = overlayLayers.map(function (d) { return d.source().id; }).join(','),
meters = offsetToMeters(b.offset()),
epsilon = 0.01,
x = +meters[0].toFixed(2),
y = +meters[1].toFixed(2),
q = stringQs(location.hash.substring(1));
var id = b.id;
if (id === 'custom') {
id = 'custom:' + b.template;
}
if (id) {
q.background = id;
} else {
delete q.background;
}
if (o) {
q.overlays = o;
} else {
delete q.overlays;
}
if (Math.abs(x) > epsilon || Math.abs(y) > epsilon) {
q.offset = x + ',' + y;
} else {
delete q.offset;
}
location.replace('#' + qsString(q, true));
var imageryUsed = [b.imageryUsed()];
overlayLayers.forEach(function (d) {
var source = d.source();
if (!source.isLocatorOverlay()) {
imageryUsed.push(source.imageryUsed());
}
});
var gpx = context.layers().layer('gpx');
if (gpx && gpx.enabled() && gpx.hasGpx()) {
imageryUsed.push('Local GPX');
}
var mapillary_images = context.layers().layer('mapillary-images');
if (mapillary_images && mapillary_images.enabled()) {
imageryUsed.push('Mapillary Images');
}
var mapillary_signs = context.layers().layer('mapillary-signs');
if (mapillary_signs && mapillary_signs.enabled()) {
imageryUsed.push('Mapillary Signs');
}
context.history().imageryUsed(imageryUsed);
};
background.sources = function(extent) {
return backgroundSources.filter(function(source) {
return source.intersects(extent);
});
};
background.dimensions = function(_) {
baseLayer.dimensions(_);
overlayLayers.forEach(function(layer) {
layer.dimensions(_);
});
};
background.baseLayerSource = function(d) {
if (!arguments.length) return baseLayer.source();
baseLayer.source(d);
dispatch.change();
background.updateImagery();
return background;
};
background.bing = function() {
background.baseLayerSource(findSource('Bing'));
};
background.showsLayer = function(d) {
return d === baseLayer.source() ||
(d.id === 'custom' && baseLayer.source().id === 'custom') ||
overlayLayers.some(function(l) { return l.source() === d; });
};
background.overlayLayerSources = function() {
return overlayLayers.map(function (l) { return l.source(); });
};
background.toggleOverlayLayer = function(d) {
var layer;
for (var i = 0; i < overlayLayers.length; i++) {
layer = overlayLayers[i];
if (layer.source() === d) {
overlayLayers.splice(i, 1);
dispatch.change();
background.updateImagery();
return;
}
}
layer = TileLayer(context)
.source(d)
.projection(context.projection)
.dimensions(baseLayer.dimensions());
overlayLayers.push(layer);
dispatch.change();
background.updateImagery();
};
background.nudge = function(d, zoom) {
baseLayer.source().nudge(d, zoom);
dispatch.change();
background.updateImagery();
return background;
};
background.offset = function(d) {
if (!arguments.length) return baseLayer.source().offset();
baseLayer.source().offset(d);
dispatch.change();
background.updateImagery();
return background;
};
background.load = function(imagery) {
function parseMap(qmap) {
if (!qmap) return false;
var args = qmap.split('/').map(Number);
if (args.length < 3 || args.some(isNaN)) return false;
return Extent([args[1], args[2]]);
}
var q = stringQs(location.hash.substring(1)),
chosen = q.background || q.layer,
extent = parseMap(q.map),
best;
backgroundSources = imagery.map(function(source) {
if (source.type === 'bing') {
return BackgroundSource.Bing(source, dispatch);
} else {
return BackgroundSource(source);
}
});
backgroundSources.unshift(BackgroundSource.None());
if (!chosen && extent) {
best = _.find(this.sources(extent), function(s) { return s.best(); });
}
if (chosen && chosen.indexOf('custom:') === 0) {
background.baseLayerSource(BackgroundSource.Custom(chosen.replace(/^custom:/, '')));
} else {
background.baseLayerSource(findSource(chosen) || best || findSource('Bing') || backgroundSources[1] || backgroundSources[0]);
}
var locator = _.find(backgroundSources, function(d) {
return d.overlay && d.default;
});
if (locator) {
background.toggleOverlayLayer(locator);
}
var overlays = (q.overlays || '').split(',');
overlays.forEach(function(overlay) {
overlay = findSource(overlay);
if (overlay) {
background.toggleOverlayLayer(overlay);
}
});
if (q.gpx) {
var gpx = context.layers().layer('gpx');
if (gpx) {
gpx.url(q.gpx);
}
}
if (q.offset) {
var offset = q.offset.replace(/;/g, ',').split(',').map(function(n) {
return !isNaN(n) && n;
});
if (offset.length === 2) {
background.offset(metersToOffset(offset));
}
}
};
return d3.rebind(background, dispatch, 'on');
}
function Features(context) {
var traffic_roads = {
'motorway': true,
'motorway_link': true,
'trunk': true,
'trunk_link': true,
'primary': true,
'primary_link': true,
'secondary': true,
'secondary_link': true,
'tertiary': true,
'tertiary_link': true,
'residential': true,
'unclassified': true,
'living_street': true
};
var service_roads = {
'service': true,
'road': true,
'track': true
};
var paths = {
'path': true,
'footway': true,
'cycleway': true,
'bridleway': true,
'steps': true,
'pedestrian': true,
'corridor': true
};
var past_futures = {
'proposed': true,
'construction': true,
'abandoned': true,
'dismantled': true,
'disused': true,
'razed': true,
'demolished': true,
'obliterated': true
};
var dispatch = d3.dispatch('change', 'redraw'),
_cullFactor = 1,
_cache = {},
_features = {},
_stats = {},
_keys = [],
_hidden = [];
function update() {
_hidden = features.hidden();
dispatch.change();
dispatch.redraw();
}
function defineFeature(k, filter, max) {
_keys.push(k);
_features[k] = {
filter: filter,
enabled: true, // whether the user wants it enabled..
count: 0,
currentMax: (max || Infinity),
defaultMax: (max || Infinity),
enable: function() { this.enabled = true; this.currentMax = this.defaultMax; },
disable: function() { this.enabled = false; this.currentMax = 0; },
hidden: function() { return !context.editable() || this.count > this.currentMax * _cullFactor; },
autoHidden: function() { return this.hidden() && this.currentMax > 0; }
};
}
defineFeature('points', function isPoint(entity, resolver, geometry) {
return geometry === 'point';
}, 200);
defineFeature('traffic_roads', function isTrafficRoad(entity) {
return traffic_roads[entity.tags.highway];
});
defineFeature('service_roads', function isServiceRoad(entity) {
return service_roads[entity.tags.highway];
});
defineFeature('paths', function isPath(entity) {
return paths[entity.tags.highway];
});
defineFeature('buildings', function isBuilding(entity) {
return (
!!entity.tags['building:part'] ||
(!!entity.tags.building && entity.tags.building !== 'no') ||
entity.tags.amenity === 'shelter' ||
entity.tags.parking === 'multi-storey' ||
entity.tags.parking === 'sheds' ||
entity.tags.parking === 'carports' ||
entity.tags.parking === 'garage_boxes'
);
}, 250);
defineFeature('landuse', function isLanduse(entity, resolver, geometry) {
return geometry === 'area' &&
!_features.buildings.filter(entity) &&
!_features.water.filter(entity);
});
defineFeature('boundaries', function isBoundary(entity) {
return !!entity.tags.boundary;
});
defineFeature('water', function isWater(entity) {
return (
!!entity.tags.waterway ||
entity.tags.natural === 'water' ||
entity.tags.natural === 'coastline' ||
entity.tags.natural === 'bay' ||
entity.tags.landuse === 'pond' ||
entity.tags.landuse === 'basin' ||
entity.tags.landuse === 'reservoir' ||
entity.tags.landuse === 'salt_pond'
);
});
defineFeature('rail', function isRail(entity) {
return (
!!entity.tags.railway ||
entity.tags.landuse === 'railway'
) && !(
traffic_roads[entity.tags.highway] ||
service_roads[entity.tags.highway] ||
paths[entity.tags.highway]
);
});
defineFeature('power', function isPower(entity) {
return !!entity.tags.power;
});
// contains a past/future tag, but not in active use as a road/path/cycleway/etc..
defineFeature('past_future', function isPastFuture(entity) {
if (
traffic_roads[entity.tags.highway] ||
service_roads[entity.tags.highway] ||
paths[entity.tags.highway]
) { return false; }
var strings = Object.keys(entity.tags);
for (var i = 0; i < strings.length; i++) {
var s = strings[i];
if (past_futures[s] || past_futures[entity.tags[s]]) { return true; }
}
return false;
});
// Lines or areas that don't match another feature filter.
// IMPORTANT: The 'others' feature must be the last one defined,
// so that code in getMatches can skip this test if `hasMatch = true`
defineFeature('others', function isOther(entity, resolver, geometry) {
return (geometry === 'line' || geometry === 'area');
});
function features() {}
features.features = function() {
return _features;
};
features.keys = function() {
return _keys;
};
features.enabled = function(k) {
if (!arguments.length) {
return _.filter(_keys, function(k) { return _features[k].enabled; });
}
return _features[k] && _features[k].enabled;
};
features.disabled = function(k) {
if (!arguments.length) {
return _.reject(_keys, function(k) { return _features[k].enabled; });
}
return _features[k] && !_features[k].enabled;
};
features.hidden = function(k) {
if (!arguments.length) {
return _.filter(_keys, function(k) { return _features[k].hidden(); });
}
return _features[k] && _features[k].hidden();
};
features.autoHidden = function(k) {
if (!arguments.length) {
return _.filter(_keys, function(k) { return _features[k].autoHidden(); });
}
return _features[k] && _features[k].autoHidden();
};
features.enable = function(k) {
if (_features[k] && !_features[k].enabled) {
_features[k].enable();
update();
}
};
features.disable = function(k) {
if (_features[k] && _features[k].enabled) {
_features[k].disable();
update();
}
};
features.toggle = function(k) {
if (_features[k]) {
(function(f) { return f.enabled ? f.disable() : f.enable(); }(_features[k]));
update();
}
};
features.resetStats = function() {
_.each(_features, function(f) { f.count = 0; });
dispatch.change();
};
features.gatherStats = function(d, resolver, dimensions) {
var needsRedraw = false,
type = _.groupBy(d, function(ent) { return ent.type; }),
entities = [].concat(type.relation || [], type.way || [], type.node || []),
currHidden, geometry, matches;
_.each(_features, function(f) { f.count = 0; });
// adjust the threshold for point/building culling based on viewport size..
// a _cullFactor of 1 corresponds to a 1000x1000px viewport..
_cullFactor = dimensions[0] * dimensions[1] / 1000000;
for (var i = 0; i < entities.length; i++) {
geometry = entities[i].geometry(resolver);
if (!(geometry === 'vertex' || geometry === 'relation')) {
matches = Object.keys(features.getMatches(entities[i], resolver, geometry));
for (var j = 0; j < matches.length; j++) {
_features[matches[j]].count++;
}
}
}
currHidden = features.hidden();
if (currHidden !== _hidden) {
_hidden = currHidden;
needsRedraw = true;
dispatch.change();
}
return needsRedraw;
};
features.stats = function() {
_.each(_keys, function(k) { _stats[k] = _features[k].count; });
return _stats;
};
features.clear = function(d) {
for (var i = 0; i < d.length; i++) {
features.clearEntity(d[i]);
}
};
features.clearEntity = function(entity) {
delete _cache[Entity.key(entity)];
};
features.reset = function() {
_cache = {};
};
features.getMatches = function(entity, resolver, geometry) {
if (geometry === 'vertex' || geometry === 'relation') return {};
var ent = Entity.key(entity);
if (!_cache[ent]) {
_cache[ent] = {};
}
if (!_cache[ent].matches) {
var matches = {},
hasMatch = false;
for (var i = 0; i < _keys.length; i++) {
if (_keys[i] === 'others') {
if (hasMatch) continue;
// Multipolygon members:
// If an entity...
// 1. is a way that hasn't matched other "interesting" feature rules,
// 2. and it belongs to a single parent multipolygon relation
// ...then match whatever feature rules the parent multipolygon has matched.
// see #2548, #2887
//
// IMPORTANT:
// For this to work, getMatches must be called on relations before ways.
//
if (entity.type === 'way') {
var parents = features.getParents(entity, resolver, geometry);
if (parents.length === 1 && parents[0].isMultipolygon()) {
var pkey = Entity.key(parents[0]);
if (_cache[pkey] && _cache[pkey].matches) {
matches = _.clone(_cache[pkey].matches);
continue;
}
}
}
}
if (_features[_keys[i]].filter(entity, resolver, geometry)) {
matches[_keys[i]] = hasMatch = true;
}
}
_cache[ent].matches = matches;
}
return _cache[ent].matches;
};
features.getParents = function(entity, resolver, geometry) {
if (geometry === 'point') return [];
var ent = Entity.key(entity);
if (!_cache[ent]) {
_cache[ent] = {};
}
if (!_cache[ent].parents) {
var parents = [];
if (geometry === 'vertex') {
parents = resolver.parentWays(entity);
} else { // 'line', 'area', 'relation'
parents = resolver.parentRelations(entity);
}
_cache[ent].parents = parents;
}
return _cache[ent].parents;
};
features.isHiddenFeature = function(entity, resolver, geometry) {
if (!_hidden.length) return false;
if (!entity.version) return false;
var matches = features.getMatches(entity, resolver, geometry);
for (var i = 0; i < _hidden.length; i++) {
if (matches[_hidden[i]]) return true;
}
return false;
};
features.isHiddenChild = function(entity, resolver, geometry) {
if (!_hidden.length) return false;
if (!entity.version || geometry === 'point') return false;
var parents = features.getParents(entity, resolver, geometry);
if (!parents.length) return false;
for (var i = 0; i < parents.length; i++) {
if (!features.isHidden(parents[i], resolver, parents[i].geometry(resolver))) {
return false;
}
}
return true;
};
features.hasHiddenConnections = function(entity, resolver) {
if (!_hidden.length) return false;
var childNodes, connections;
if (entity.type === 'midpoint') {
childNodes = [resolver.entity(entity.edge[0]), resolver.entity(entity.edge[1])];
connections = [];
} else {
childNodes = entity.nodes ? resolver.childNodes(entity) : [];
connections = features.getParents(entity, resolver, entity.geometry(resolver));
}
// gather ways connected to child nodes..
connections = _.reduce(childNodes, function(result, e) {
return resolver.isShared(e) ? _.union(result, resolver.parentWays(e)) : result;
}, connections);
return connections.length ? _.some(connections, function(e) {
return features.isHidden(e, resolver, e.geometry(resolver));
}) : false;
};
features.isHidden = function(entity, resolver, geometry) {
if (!_hidden.length) return false;
if (!entity.version) return false;
var fn = (geometry === 'vertex' ? features.isHiddenChild : features.isHiddenFeature);
return fn(entity, resolver, geometry);
};
features.filter = function(d, resolver) {
if (!_hidden.length) return d;
var result = [];
for (var i = 0; i < d.length; i++) {
var entity = d[i];
if (!features.isHidden(entity, resolver, entity.geometry(resolver))) {
result.push(entity);
}
}
return result;
};
return d3.rebind(features, dispatch, 'on');
}
function Areas(projection) {
// Patterns only work in Firefox when set directly on element.
// (This is not a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=750632)
var patterns = {
wetland: 'wetland',
beach: 'beach',
scrub: 'scrub',
construction: 'construction',
military: 'construction',
cemetery: 'cemetery',
grave_yard: 'cemetery',
meadow: 'meadow',
farm: 'farmland',
farmland: 'farmland',
orchard: 'orchard'
};
var patternKeys = ['landuse', 'natural', 'amenity'];
function setPattern(d) {
for (var i = 0; i < patternKeys.length; i++) {
if (patterns.hasOwnProperty(d.tags[patternKeys[i]])) {
this.style.fill = this.style.stroke = 'url("#pattern-' + patterns[d.tags[patternKeys[i]]] + '")';
return;
}
}
this.style.fill = this.style.stroke = '';
}
return function drawAreas(surface, graph, entities, filter) {
var path = iD.svg.Path(projection, graph, true),
areas = {},
multipolygon;
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
if (entity.geometry(graph) !== 'area') continue;
multipolygon = iD.geo.isSimpleMultipolygonOuterMember(entity, graph);
if (multipolygon) {
areas[multipolygon.id] = {
entity: multipolygon.mergeTags(entity.tags),
area: Math.abs(entity.area(graph))
};
} else if (!areas[entity.id]) {
areas[entity.id] = {
entity: entity,
area: Math.abs(entity.area(graph))
};
}
}
areas = d3.values(areas).filter(function hasPath(a) { return path(a.entity); });
areas.sort(function areaSort(a, b) { return b.area - a.area; });
areas = _.map(areas, 'entity');
var strokes = areas.filter(function(area) {
return area.type === 'way';
});
var data = {
clip: areas,
shadow: strokes,
stroke: strokes,
fill: areas
};
var clipPaths = surface.selectAll('defs').selectAll('.clipPath')
.filter(filter)
.data(data.clip, iD.Entity.key);
clipPaths.enter()
.append('clipPath')
.attr('class', 'clipPath')
.attr('id', function(entity) { return entity.id + '-clippath'; })
.append('path');
clipPaths.selectAll('path')
.attr('d', path);
clipPaths.exit()
.remove();
var areagroup = surface
.selectAll('.layer-areas')
.selectAll('g.areagroup')
.data(['fill', 'shadow', 'stroke']);
areagroup.enter()
.append('g')
.attr('class', function(d) { return 'layer areagroup area-' + d; });
var paths = areagroup
.selectAll('path')
.filter(filter)
.data(function(layer) { return data[layer]; }, iD.Entity.key);
// Remove exiting areas first, so they aren't included in the `fills`
// array used for sorting below (https://github.com/openstreetmap/iD/issues/1903).
paths.exit()
.remove();
var fills = surface.selectAll('.area-fill path.area')[0];
var bisect = d3.bisector(function(node) {
return -node.__data__.area(graph);
}).left;
function sortedByArea(entity) {
if (this.__data__ === 'fill') {
return fills[bisect(fills, -entity.area(graph))];
}
}
paths.enter()
.insert('path', sortedByArea)
.each(function(entity) {
var layer = this.parentNode.__data__;
this.setAttribute('class', entity.type + ' area ' + layer + ' ' + entity.id);
if (layer === 'fill') {
this.setAttribute('clip-path', 'url(#' + entity.id + '-clippath)');
setPattern.apply(this, arguments);
}
})
.call(iD.svg.TagClasses());
paths
.attr('d', path);
};
}
function Labels(projection, context) {
var path = d3.geo.path().projection(projection);
// Replace with dict and iterate over entities tags instead?
var label_stack = [
['line', 'aeroway'],
['line', 'highway'],
['line', 'railway'],
['line', 'waterway'],
['area', 'aeroway'],
['area', 'amenity'],
['area', 'building'],
['area', 'historic'],
['area', 'leisure'],
['area', 'man_made'],
['area', 'natural'],
['area', 'shop'],
['area', 'tourism'],
['point', 'aeroway'],
['point', 'amenity'],
['point', 'building'],
['point', 'historic'],
['point', 'leisure'],
['point', 'man_made'],
['point', 'natural'],
['point', 'shop'],
['point', 'tourism'],
['line', 'name'],
['area', 'name'],
['point', 'name']
];
var default_size = 12;
var font_sizes = label_stack.map(function(d) {
var style = iD.util.getStyle('text.' + d[0] + '.tag-' + d[1]),
m = style && style.cssText.match('font-size: ([0-9]{1,2})px;');
if (m) return parseInt(m[1], 10);
style = iD.util.getStyle('text.' + d[0]);
m = style && style.cssText.match('font-size: ([0-9]{1,2})px;');
if (m) return parseInt(m[1], 10);
return default_size;
});
var iconSize = 18;
var pointOffsets = [
[15, -11, 'start'], // right
[10, -11, 'start'], // unused right now
[-15, -11, 'end']
];
var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70, 25,
75, 20, 80, 15, 95, 10, 90, 5, 95];
var noIcons = ['building', 'landuse', 'natural'];
function blacklisted(preset) {
return _.some(noIcons, function(s) {
return preset.id.indexOf(s) >= 0;
});
}
function get(array, prop) {
return function(d, i) { return array[i][prop]; };
}
var textWidthCache = {};
function textWidth(text, size, elem) {
var c = textWidthCache[size];
if (!c) c = textWidthCache[size] = {};
if (c[text]) {
return c[text];
} else if (elem) {
c[text] = elem.getComputedTextLength();
return c[text];
} else {
var str = encodeURIComponent(text).match(/%[CDEFcdef]/g);
if (str === null) {
return size / 3 * 2 * text.length;
} else {
return size / 3 * (2 * text.length + str.length);
}
}
}
function drawLineLabels(group, entities, filter, classes, labels) {
var texts = group.selectAll('text.' + classes)
.filter(filter)
.data(entities, iD.Entity.key);
texts.enter()
.append('text')
.attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; })
.append('textPath')
.attr('class', 'textpath');
texts.selectAll('.textpath')
.filter(filter)
.data(entities, iD.Entity.key)
.attr({
'startOffset': '50%',
'xlink:href': function(d) { return '#labelpath-' + d.id; }
})
.text(iD.util.displayName);
texts.exit().remove();
}
function drawLinePaths(group, entities, filter, classes, labels) {
var halos = group.selectAll('path')
.filter(filter)
.data(entities, iD.Entity.key);
halos.enter()
.append('path')
.style('stroke-width', get(labels, 'font-size'))
.attr('id', function(d) { return 'labelpath-' + d.id; })
.attr('class', classes);
halos.attr('d', get(labels, 'lineString'));
halos.exit().remove();
}
function drawPointLabels(group, entities, filter, classes, labels) {
var texts = group.selectAll('text.' + classes)
.filter(filter)
.data(entities, iD.Entity.key);
texts.enter()
.append('text')
.attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; });
texts.attr('x', get(labels, 'x'))
.attr('y', get(labels, 'y'))
.style('text-anchor', get(labels, 'textAnchor'))
.text(iD.util.displayName)
.each(function(d, i) { textWidth(iD.util.displayName(d), labels[i].height, this); });
texts.exit().remove();
return texts;
}
function drawAreaLabels(group, entities, filter, classes, labels) {
entities = entities.filter(hasText);
labels = labels.filter(hasText);
return drawPointLabels(group, entities, filter, classes, labels);
function hasText(d, i) {
return labels[i].hasOwnProperty('x') && labels[i].hasOwnProperty('y');
}
}
function drawAreaIcons(group, entities, filter, classes, labels) {
var icons = group.selectAll('use')
.filter(filter)
.data(entities, iD.Entity.key);
icons.enter()
.append('use')
.attr('class', 'icon areaicon')
.attr('width', '18px')
.attr('height', '18px');
icons.attr('transform', get(labels, 'transform'))
.attr('xlink:href', function(d) {
var icon = context.presets().match(d, context.graph()).icon;
return '#' + icon + (icon === 'hairdresser' ? '-24': '-18'); // workaround: maki hairdresser-18 broken?
});
icons.exit().remove();
}
function reverse(p) {
var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]);
return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2);
}
function lineString(nodes) {
return 'M' + nodes.join('L');
}
function subpath(nodes, from, to) {
function segmentLength(i) {
var dx = nodes[i][0] - nodes[i + 1][0];
var dy = nodes[i][1] - nodes[i + 1][1];
return Math.sqrt(dx * dx + dy * dy);
}
var sofar = 0,
start, end, i0, i1;
for (var i = 0; i < nodes.length - 1; i++) {
var current = segmentLength(i);
var portion;
if (!start && sofar + current >= from) {
portion = (from - sofar) / current;
start = [
nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]),
nodes[i][1] + portion * (nodes[i + 1][1] - nodes[i][1])
];
i0 = i + 1;
}
if (!end && sofar + current >= to) {
portion = (to - sofar) / current;
end = [
nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]),
nodes[i][1] + portion * (nodes[i + 1][1] - nodes[i][1])
];
i1 = i + 1;
}
sofar += current;
}
var ret = nodes.slice(i0, i1);
ret.unshift(start);
ret.push(end);
return ret;
}
function hideOnMouseover() {
var layers = d3.select(this)
.selectAll('.layer-label, .layer-halo');
layers.selectAll('.proximate')
.classed('proximate', false);
var mouse = context.mouse(),
pad = 50,
rect = [mouse[0] - pad, mouse[1] - pad, mouse[0] + pad, mouse[1] + pad],
ids = _.map(rtree.search(rect), 'id');
if (!ids.length) return;
layers.selectAll('.' + ids.join(', .'))
.classed('proximate', true);
}
var rtree = rbush$1(),
rectangles = {};
function drawLabels(surface, graph, entities, filter, dimensions, fullRedraw) {
var hidePoints = !surface.selectAll('.node.point').node();
var labelable = [], i, k, entity;
for (i = 0; i < label_stack.length; i++) labelable.push([]);
if (fullRedraw) {
rtree.clear();
rectangles = {};
} else {
for (i = 0; i < entities.length; i++) {
rtree.remove(rectangles[entities[i].id]);
}
}
// Split entities into groups specified by label_stack
for (i = 0; i < entities.length; i++) {
entity = entities[i];
var geometry = entity.geometry(graph);
if (geometry === 'vertex')
continue;
if (hidePoints && geometry === 'point')
continue;
var preset = geometry === 'area' && context.presets().match(entity, graph),
icon = preset && !blacklisted(preset) && preset.icon;
if (!icon && !iD.util.displayName(entity))
continue;
for (k = 0; k < label_stack.length; k++) {
if (geometry === label_stack[k][0] && entity.tags[label_stack[k][1]]) {
labelable[k].push(entity);
break;
}
}
}
var positions = {
point: [],
line: [],
area: []
};
var labelled = {
point: [],
line: [],
area: []
};
// Try and find a valid label for labellable entities
for (k = 0; k < labelable.length; k++) {
var font_size = font_sizes[k];
for (i = 0; i < labelable[k].length; i++) {
entity = labelable[k][i];
var name = iD.util.displayName(entity),
width = name && textWidth(name, font_size),
p;
if (entity.geometry(graph) === 'point') {
p = getPointLabel(entity, width, font_size);
} else if (entity.geometry(graph) === 'line') {
p = getLineLabel(entity, width, font_size);
} else if (entity.geometry(graph) === 'area') {
p = getAreaLabel(entity, width, font_size);
}
if (p) {
p.classes = entity.geometry(graph) + ' tag-' + label_stack[k][1];
positions[entity.geometry(graph)].push(p);
labelled[entity.geometry(graph)].push(entity);
}
}
}
function getPointLabel(entity, width, height) {
var coord = projection(entity.loc),
m = 5, // margin
offset = pointOffsets[0],
p = {
height: height,
width: width,
x: coord[0] + offset[0],
y: coord[1] + offset[1],
textAnchor: offset[2]
};
var rect = [p.x - m, p.y - m, p.x + width + m, p.y + height + m];
if (tryInsert(rect, entity.id)) return p;
}
function getLineLabel(entity, width, height) {
var nodes = _.map(graph.childNodes(entity), 'loc').map(projection),
length = iD.geo.pathLength(nodes);
if (length < width + 20) return;
for (var i = 0; i < lineOffsets.length; i++) {
var offset = lineOffsets[i],
middle = offset / 100 * length,
start = middle - width/2;
if (start < 0 || start + width > length) continue;
var sub = subpath(nodes, start, start + width),
rev = reverse(sub),
rect = [
Math.min(sub[0][0], sub[sub.length - 1][0]) - 10,
Math.min(sub[0][1], sub[sub.length - 1][1]) - 10,
Math.max(sub[0][0], sub[sub.length - 1][0]) + 20,
Math.max(sub[0][1], sub[sub.length - 1][1]) + 30
];
if (rev) sub = sub.reverse();
if (tryInsert(rect, entity.id)) return {
'font-size': height + 2,
lineString: lineString(sub),
startOffset: offset + '%'
};
}
}
function getAreaLabel(entity, width, height) {
var centroid = path.centroid(entity.asGeoJSON(graph, true)),
extent = entity.extent(graph),
entitywidth = projection(extent[1])[0] - projection(extent[0])[0],
rect;
if (isNaN(centroid[0]) || entitywidth < 20) return;
var iconX = centroid[0] - (iconSize/2),
iconY = centroid[1] - (iconSize/2),
textOffset = iconSize + 5;
var p = {
transform: 'translate(' + iconX + ',' + iconY + ')'
};
if (width && entitywidth >= width + 20) {
p.x = centroid[0];
p.y = centroid[1] + textOffset;
p.textAnchor = 'middle';
p.height = height;
rect = [p.x - width/2, p.y, p.x + width/2, p.y + height + textOffset];
} else {
rect = [iconX, iconY, iconX + iconSize, iconY + iconSize];
}
if (tryInsert(rect, entity.id)) return p;
}
function tryInsert(rect, id) {
// Check that label is visible
if (rect[0] < 0 || rect[1] < 0 || rect[2] > dimensions[0] ||
rect[3] > dimensions[1]) return false;
var v = rtree.search(rect).length === 0;
if (v) {
rect.id = id;
rtree.insert(rect);
rectangles[id] = rect;
}
return v;
}
var label = surface.selectAll('.layer-label'),
halo = surface.selectAll('.layer-halo');
// points
drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point);
drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo', positions.point);
// lines
drawLinePaths(halo, labelled.line, filter, '', positions.line);
drawLineLabels(label, labelled.line, filter, 'linelabel', positions.line);
drawLineLabels(halo, labelled.line, filter, 'linelabel-halo', positions.line);
// areas
drawAreaLabels(label, labelled.area, filter, 'arealabel', positions.area);
drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo', positions.area);
drawAreaIcons(label, labelled.area, filter, 'arealabel-icon', positions.area);
// debug
var showDebug = context.getDebug('collision');
var debug = label.selectAll('.layer-label-debug')
.data(showDebug ? [true] : []);
debug.enter()
.append('g')
.attr('class', 'layer-label-debug');
debug.exit()
.remove();
if (showDebug) {
var gj = rtree.all().map(function(d) {
return { type: 'Polygon', coordinates: [[
[d[0], d[1]],
[d[2], d[1]],
[d[2], d[3]],
[d[0], d[3]],
[d[0], d[1]]
]]};
});
var debugboxes = debug.selectAll('.debug').data(gj);
debugboxes.enter()
.append('path')
.attr('class', 'debug yellow');
debugboxes.exit()
.remove();
debugboxes
.attr('d', d3.geo.path().projection(null));
}
}
drawLabels.supersurface = function(supersurface) {
supersurface
.on('mousemove.hidelabels', hideOnMouseover)
.on('mousedown.hidelabels', function () {
supersurface.on('mousemove.hidelabels', null);
})
.on('mouseup.hidelabels', function () {
supersurface.on('mousemove.hidelabels', hideOnMouseover);
});
};
return drawLabels;
}
function Layers(projection, context) {
var dispatch = d3.dispatch('change'),
svg = d3.select(null),
layers = [
{ id: 'osm', layer: iD.svg.Osm(projection, context, dispatch) },
{ id: 'gpx', layer: iD.svg.Gpx(projection, context, dispatch) },
{ id: 'mapillary-images', layer: iD.svg.MapillaryImages(projection, context, dispatch) },
{ id: 'mapillary-signs', layer: iD.svg.MapillarySigns(projection, context, dispatch) },
{ id: 'debug', layer: iD.svg.Debug(projection, context, dispatch) }
];
function drawLayers(selection) {
svg = selection.selectAll('.surface')
.data([0]);
svg.enter()
.append('svg')
.attr('class', 'surface')
.append('defs');
var groups = svg.selectAll('.data-layer')
.data(layers);
groups.enter()
.append('g')
.attr('class', function(d) { return 'data-layer data-layer-' + d.id; });
groups
.each(function(d) { d3.select(this).call(d.layer); });
groups.exit()
.remove();
}
drawLayers.all = function() {
return layers;
};
drawLayers.layer = function(id) {
var obj = _.find(layers, function(o) {return o.id === id;});
return obj && obj.layer;
};
drawLayers.only = function(what) {
var arr = [].concat(what);
drawLayers.remove(_.difference(_.map(layers, 'id'), arr));
return this;
};
drawLayers.remove = function(what) {
var arr = [].concat(what);
arr.forEach(function(id) {
layers = _.reject(layers, function(o) {return o.id === id;});
});
dispatch.change();
return this;
};
drawLayers.add = function(what) {
var arr = [].concat(what);
arr.forEach(function(obj) {
if ('id' in obj && 'layer' in obj) {
layers.push(obj);
}
});
dispatch.change();
return this;
};
drawLayers.dimensions = function(_) {
if (!arguments.length) return svg.dimensions();
svg.dimensions(_);
layers.forEach(function(obj) {
if (obj.layer.dimensions) {
obj.layer.dimensions(_);
}
});
return this;
};
return d3.rebind(drawLayers, dispatch, 'on');
}
function Lines(projection) {
var highway_stack = {
motorway: 0,
motorway_link: 1,
trunk: 2,
trunk_link: 3,
primary: 4,
primary_link: 5,
secondary: 6,
tertiary: 7,
unclassified: 8,
residential: 9,
service: 10,
footway: 11
};
function waystack(a, b) {
var as = 0, bs = 0;
if (a.tags.highway) { as -= highway_stack[a.tags.highway]; }
if (b.tags.highway) { bs -= highway_stack[b.tags.highway]; }
return as - bs;
}
return function drawLines(surface, graph, entities, filter) {
var ways = [], pathdata = {}, onewaydata = {},
getPath = iD.svg.Path(projection, graph);
for (var i = 0; i < entities.length; i++) {
var entity = entities[i],
outer = iD.geo.simpleMultipolygonOuterMember(entity, graph);
if (outer) {
ways.push(entity.mergeTags(outer.tags));
} else if (entity.geometry(graph) === 'line') {
ways.push(entity);
}
}
ways = ways.filter(getPath);
pathdata = _.groupBy(ways, function(way) { return way.layer(); });
_.forOwn(pathdata, function(v, k) {
onewaydata[k] = _(v)
.filter(function(d) { return d.isOneWay(); })
.map(iD.svg.OneWaySegments(projection, graph, 35))
.flatten()
.valueOf();
});
var layergroup = surface
.selectAll('.layer-lines')
.selectAll('g.layergroup')
.data(d3.range(-10, 11));
layergroup.enter()
.append('g')
.attr('class', function(d) { return 'layer layergroup layer' + String(d); });
var linegroup = layergroup
.selectAll('g.linegroup')
.data(['shadow', 'casing', 'stroke']);
linegroup.enter()
.append('g')
.attr('class', function(d) { return 'layer linegroup line-' + d; });
var lines = linegroup
.selectAll('path')
.filter(filter)
.data(
function() { return pathdata[this.parentNode.parentNode.__data__] || []; },
iD.Entity.key
);
// Optimization: call simple TagClasses only on enter selection. This
// works because iD.Entity.key is defined to include the entity v attribute.
lines.enter()
.append('path')
.attr('class', function(d) { return 'way line ' + this.parentNode.__data__ + ' ' + d.id; })
.call(iD.svg.TagClasses());
lines
.sort(waystack)
.attr('d', getPath)
.call(iD.svg.TagClasses().tags(iD.svg.RelationMemberTags(graph)));
lines.exit()
.remove();
var onewaygroup = layergroup
.selectAll('g.onewaygroup')
.data(['oneway']);
onewaygroup.enter()
.append('g')
.attr('class', 'layer onewaygroup');
var oneways = onewaygroup
.selectAll('path')
.filter(filter)
.data(
function() { return onewaydata[this.parentNode.parentNode.__data__] || []; },
function(d) { return [d.id, d.index]; }
);
oneways.enter()
.append('path')
.attr('class', 'oneway')
.attr('marker-mid', 'url(#oneway-marker)');
oneways
.attr('d', function(d) { return d.d; });
if (iD.detect().ie) {
oneways.each(function() { this.parentNode.insertBefore(this, this); });
}
oneways.exit()
.remove();
};
}
function Midpoints(projection, context) {
return function drawMidpoints(surface, graph, entities, filter, extent) {
var poly = extent.polygon(),
midpoints = {};
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
if (entity.type !== 'way')
continue;
if (!filter(entity))
continue;
if (context.selectedIDs().indexOf(entity.id) < 0)
continue;
var nodes = graph.childNodes(entity);
for (var j = 0; j < nodes.length - 1; j++) {
var a = nodes[j],
b = nodes[j + 1],
id = [a.id, b.id].sort().join('-');
if (midpoints[id]) {
midpoints[id].parents.push(entity);
} else {
if (iD.geo.euclideanDistance(projection(a.loc), projection(b.loc)) > 40) {
var point = iD.geo.interp(a.loc, b.loc, 0.5),
loc = null;
if (extent.intersects(point)) {
loc = point;
} else {
for (var k = 0; k < 4; k++) {
point = iD.geo.lineIntersection([a.loc, b.loc], [poly[k], poly[k+1]]);
if (point &&
iD.geo.euclideanDistance(projection(a.loc), projection(point)) > 20 &&
iD.geo.euclideanDistance(projection(b.loc), projection(point)) > 20)
{
loc = point;
break;
}
}
}
if (loc) {
midpoints[id] = {
type: 'midpoint',
id: id,
loc: loc,
edge: [a.id, b.id],
parents: [entity]
};
}
}
}
}
}
function midpointFilter(d) {
if (midpoints[d.id])
return true;
for (var i = 0; i < d.parents.length; i++)
if (filter(d.parents[i]))
return true;
return false;
}
var groups = surface.selectAll('.layer-hit').selectAll('g.midpoint')
.filter(midpointFilter)
.data(_.values(midpoints), function(d) { return d.id; });
var enter = groups.enter()
.insert('g', ':first-child')
.attr('class', 'midpoint');
enter.append('polygon')
.attr('points', '-6,8 10,0 -6,-8')
.attr('class', 'shadow');
enter.append('polygon')
.attr('points', '-3,4 5,0 -3,-4')
.attr('class', 'fill');
groups
.attr('transform', function(d) {
var translate = iD.svg.PointTransform(projection),
a = context.entity(d.edge[0]),
b = context.entity(d.edge[1]),
angle = Math.round(iD.geo.angle(a, b, projection) * (180 / Math.PI));
return translate(d) + ' rotate(' + angle + ')';
})
.call(iD.svg.TagClasses().tags(
function(d) { return d.parents[0].tags; }
));
// Propagate data bindings.
groups.select('polygon.shadow');
groups.select('polygon.fill');
groups.exit()
.remove();
};
}
function Points(projection, context) {
function markerPath(selection, klass) {
selection
.attr('class', klass)
.attr('transform', 'translate(-8, -23)')
.attr('d', 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z');
}
function sortY(a, b) {
return b.loc[1] - a.loc[1];
}
return function drawPoints(surface, graph, entities, filter) {
var wireframe = surface.classed('fill-wireframe'),
points = wireframe ? [] : _.filter(entities, function(e) {
return e.geometry(graph) === 'point';
});
points.sort(sortY);
var groups = surface.selectAll('.layer-hit').selectAll('g.point')
.filter(filter)
.data(points, iD.Entity.key);
var group = groups.enter()
.append('g')
.attr('class', function(d) { return 'node point ' + d.id; })
.order();
group.append('path')
.call(markerPath, 'shadow');
group.append('path')
.call(markerPath, 'stroke');
group.append('use')
.attr('transform', 'translate(-6, -20)')
.attr('class', 'icon')
.attr('width', '12px')
.attr('height', '12px');
groups.attr('transform', iD.svg.PointTransform(projection))
.call(iD.svg.TagClasses());
// Selecting the following implicitly
// sets the data (point entity) on the element
groups.select('.shadow');
groups.select('.stroke');
groups.select('.icon')
.attr('xlink:href', function(entity) {
var preset = context.presets().match(entity, graph);
return preset.icon ? '#' + preset.icon + '-12' : '';
});
groups.exit()
.remove();
};
}
function Vertices(projection, context) {
var radiuses = {
// z16-, z17, z18+, tagged
shadow: [6, 7.5, 7.5, 11.5],
stroke: [2.5, 3.5, 3.5, 7],
fill: [1, 1.5, 1.5, 1.5]
};
var hover;
function siblingAndChildVertices(ids, graph, extent) {
var vertices = {};
function addChildVertices(entity) {
if (!context.features().isHiddenFeature(entity, graph, entity.geometry(graph))) {
var i;
if (entity.type === 'way') {
for (i = 0; i < entity.nodes.length; i++) {
addChildVertices(graph.entity(entity.nodes[i]));
}
} else if (entity.type === 'relation') {
for (i = 0; i < entity.members.length; i++) {
var member = context.hasEntity(entity.members[i].id);
if (member) {
addChildVertices(member);
}
}
} else if (entity.intersects(extent, graph)) {
vertices[entity.id] = entity;
}
}
}
ids.forEach(function(id) {
var entity = context.hasEntity(id);
if (entity && entity.type === 'node') {
vertices[entity.id] = entity;
context.graph().parentWays(entity).forEach(function(entity) {
addChildVertices(entity);
});
} else if (entity) {
addChildVertices(entity);
}
});
return vertices;
}
function draw(selection, vertices, klass, graph, zoom) {
var icons = {},
z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2);
var groups = selection
.data(vertices, iD.Entity.key);
function icon(entity) {
if (entity.id in icons) return icons[entity.id];
icons[entity.id] =
entity.hasInterestingTags() &&
context.presets().match(entity, graph).icon;
return icons[entity.id];
}
function setClass(klass) {
return function(entity) {
this.setAttribute('class', 'node vertex ' + klass + ' ' + entity.id);
};
}
function setAttributes(selection) {
['shadow','stroke','fill'].forEach(function(klass) {
var rads = radiuses[klass];
selection.selectAll('.' + klass)
.each(function(entity) {
var i = z && icon(entity),
c = i ? 0.5 : 0,
r = rads[i ? 3 : z];
this.setAttribute('cx', c);
this.setAttribute('cy', -c);
this.setAttribute('r', r);
if (i && klass === 'fill') {
this.setAttribute('visibility', 'hidden');
} else {
this.removeAttribute('visibility');
}
});
});
selection.selectAll('use')
.each(function() {
if (z) {
this.removeAttribute('visibility');
} else {
this.setAttribute('visibility', 'hidden');
}
});
}
var enter = groups.enter()
.append('g')
.attr('class', function(d) { return 'node vertex ' + klass + ' ' + d.id; });
enter.append('circle')
.each(setClass('shadow'));
enter.append('circle')
.each(setClass('stroke'));
// Vertices with icons get a `use`.
enter.filter(function(d) { return icon(d); })
.append('use')
.attr('transform', 'translate(-6, -6)')
.attr('xlink:href', function(d) { return '#' + icon(d) + '-12'; })
.attr('width', '12px')
.attr('height', '12px')
.each(setClass('icon'));
// Vertices with tags get a fill.
enter.filter(function(d) { return d.hasInterestingTags(); })
.append('circle')
.each(setClass('fill'));
groups
.attr('transform', iD.svg.PointTransform(projection))
.classed('shared', function(entity) { return graph.isShared(entity); })
.call(setAttributes);
groups.exit()
.remove();
}
function drawVertices(surface, graph, entities, filter, extent, zoom) {
var selected = siblingAndChildVertices(context.selectedIDs(), graph, extent),
wireframe = surface.classed('fill-wireframe'),
vertices = [];
for (var i = 0; i < entities.length; i++) {
var entity = entities[i],
geometry = entity.geometry(graph);
if (wireframe && geometry === 'point') {
vertices.push(entity);
continue;
}
if (geometry !== 'vertex')
continue;
if (entity.id in selected ||
entity.hasInterestingTags() ||
entity.isIntersection(graph)) {
vertices.push(entity);
}
}
surface.selectAll('.layer-hit').selectAll('g.vertex.vertex-persistent')
.filter(filter)
.call(draw, vertices, 'vertex-persistent', graph, zoom);
drawHover(surface, graph, extent, zoom);
}
function drawHover(surface, graph, extent, zoom) {
var hovered = hover ? siblingAndChildVertices([hover.id], graph, extent) : {};
surface.selectAll('.layer-hit').selectAll('g.vertex.vertex-hover')
.call(draw, d3.values(hovered), 'vertex-hover', graph, zoom);
}
drawVertices.drawHover = function(surface, graph, target, extent, zoom) {
if (target === hover) return;
hover = target;
drawHover(surface, graph, extent, zoom);
};
return drawVertices;
}
function Map(context) {
var dimensions = [1, 1],
dispatch = d3.dispatch('move', 'drawn'),
projection = context.projection,
zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale() * 2 * Math.PI)
.scaleExtent([1024, 256 * Math.pow(2, 24)])
.on('zoom', zoomPan),
dblclickEnabled = true,
redrawEnabled = true,
transformStart,
transformed = false,
easing = false,
minzoom = 0,
drawLayers = Layers(projection, context),
drawPoints = Points(projection, context),
drawVertices = Vertices(projection, context),
drawLines = Lines(projection),
drawAreas = Areas(projection),
drawMidpoints = Midpoints(projection, context),
drawLabels = Labels(projection, context),
supersurface,
wrapper,
surface,
mouse,
mousemove;
function map(selection) {
context
.on('change.map', redraw);
context.history()
.on('change.map', redraw);
context.background()
.on('change.map', redraw);
context.features()
.on('redraw.map', redraw);
drawLayers
.on('change.map', function() {
context.background().updateImagery();
redraw();
});
selection
.on('dblclick.map', dblClick)
.call(zoom);
supersurface = selection.append('div')
.attr('id', 'supersurface')
.call(setTransform, 0, 0);
// Need a wrapper div because Opera can't cope with an absolutely positioned
// SVG element: http://bl.ocks.org/jfirebaugh/6fbfbd922552bf776c16
wrapper = supersurface
.append('div')
.attr('class', 'layer layer-data');
map.surface = surface = wrapper
.call(drawLayers)
.selectAll('.surface')
.attr('id', 'surface');
surface
.on('mousedown.zoom', function() {
if (d3.event.button === 2) {
d3.event.stopPropagation();
}
}, true)
.on('mouseup.zoom', function() {
if (resetTransform()) redraw();
})
.on('mousemove.map', function() {
mousemove = d3.event;
})
.on('mouseover.vertices', function() {
if (map.editable() && !transformed) {
var hover = d3.event.target.__data__;
surface.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
dispatch.drawn({full: false});
}
})
.on('mouseout.vertices', function() {
if (map.editable() && !transformed) {
var hover = d3.event.relatedTarget && d3.event.relatedTarget.__data__;
surface.call(drawVertices.drawHover, context.graph(), hover, map.extent(), map.zoom());
dispatch.drawn({full: false});
}
});
supersurface
.call(context.background());
context.on('enter.map', function() {
if (map.editable() && !transformed) {
var all = context.intersects(map.extent()),
filter = d3.functor(true),
graph = context.graph();
all = context.features().filter(all, graph);
surface
.call(drawVertices, graph, all, filter, map.extent(), map.zoom())
.call(drawMidpoints, graph, all, filter, map.trimmedExtent());
dispatch.drawn({full: false});
}
});
map.dimensions(selection.dimensions());
drawLabels.supersurface(supersurface);
}
function pxCenter() {
return [dimensions[0] / 2, dimensions[1] / 2];
}
function drawVector(difference, extent) {
var graph = context.graph(),
features = context.features(),
all = context.intersects(map.extent()),
data, filter;
if (difference) {
var complete = difference.complete(map.extent());
data = _.compact(_.values(complete));
filter = function(d) { return d.id in complete; };
features.clear(data);
} else {
// force a full redraw if gatherStats detects that a feature
// should be auto-hidden (e.g. points or buildings)..
if (features.gatherStats(all, graph, dimensions)) {
extent = undefined;
}
if (extent) {
data = context.intersects(map.extent().intersection(extent));
var set = d3.set(_.map(data, 'id'));
filter = function(d) { return set.has(d.id); };
} else {
data = all;
filter = d3.functor(true);
}
}
data = features.filter(data, graph);
surface
.call(drawVertices, graph, data, filter, map.extent(), map.zoom())
.call(drawLines, graph, data, filter)
.call(drawAreas, graph, data, filter)
.call(drawMidpoints, graph, data, filter, map.trimmedExtent())
.call(drawLabels, graph, data, filter, dimensions, !difference && !extent)
.call(drawPoints, graph, data, filter);
dispatch.drawn({full: true});
}
function editOff() {
context.features().resetStats();
surface.selectAll('.layer-osm *').remove();
dispatch.drawn({full: true});
}
function dblClick() {
if (!dblclickEnabled) {
d3.event.preventDefault();
d3.event.stopImmediatePropagation();
}
}
function zoomPan() {
if (Math.log(d3.event.scale) / Math.LN2 - 8 < minzoom) {
surface.interrupt();
flash(context.container())
.select('.content')
.text(t('cannot_zoom'));
setZoom(context.minEditableZoom(), true);
queueRedraw();
dispatch.move(map);
return;
}
projection
.translate(d3.event.translate)
.scale(d3.event.scale / (2 * Math.PI));
var scale = d3.event.scale / transformStart[0],
tX = (d3.event.translate[0] / scale - transformStart[1][0]) * scale,
tY = (d3.event.translate[1] / scale - transformStart[1][1]) * scale;
transformed = true;
setTransform(supersurface, tX, tY, scale);
queueRedraw();
dispatch.move(map);
}
function resetTransform() {
if (!transformed) return false;
surface.selectAll('.radial-menu').interrupt().remove();
setTransform(supersurface, 0, 0);
transformed = false;
return true;
}
function redraw(difference, extent) {
if (!surface || !redrawEnabled) return;
clearTimeout(timeoutId);
// If we are in the middle of a zoom/pan, we can't do differenced redraws.
// It would result in artifacts where differenced entities are redrawn with
// one transform and unchanged entities with another.
if (resetTransform()) {
difference = extent = undefined;
}
var zoom = String(~~map.zoom());
if (surface.attr('data-zoom') !== zoom) {
surface.attr('data-zoom', zoom)
.classed('low-zoom', zoom <= 16);
}
if (!difference) {
supersurface.call(context.background());
}
// OSM
if (map.editable()) {
context.loadTiles(projection, dimensions);
drawVector(difference, extent);
} else {
editOff();
}
wrapper
.call(drawLayers);
transformStart = [
projection.scale() * 2 * Math.PI,
projection.translate().slice()];
return map;
}
var timeoutId;
function queueRedraw() {
timeoutId = setTimeout(function() { redraw(); }, 750);
}
function pointLocation(p) {
var translate = projection.translate(),
scale = projection.scale() * 2 * Math.PI;
return [(p[0] - translate[0]) / scale, (p[1] - translate[1]) / scale];
}
function locationPoint(l) {
var translate = projection.translate(),
scale = projection.scale() * 2 * Math.PI;
return [l[0] * scale + translate[0], l[1] * scale + translate[1]];
}
map.mouse = function() {
var e = mousemove || d3.event, s;
while ((s = e.sourceEvent)) e = s;
return mouse(e);
};
map.mouseCoordinates = function() {
return projection.invert(map.mouse());
};
map.dblclickEnable = function(_) {
if (!arguments.length) return dblclickEnabled;
dblclickEnabled = _;
return map;
};
map.redrawEnable = function(_) {
if (!arguments.length) return redrawEnabled;
redrawEnabled = _;
return map;
};
function interpolateZoom(_) {
var k = projection.scale(),
t = projection.translate();
surface.node().__chart__ = {
x: t[0],
y: t[1],
k: k * 2 * Math.PI
};
setZoom(_);
projection.scale(k).translate(t); // undo setZoom projection changes
zoom.event(surface.transition());
}
function setZoom(_, force) {
if (_ === map.zoom() && !force)
return false;
var scale = 256 * Math.pow(2, _),
center = pxCenter(),
l = pointLocation(center);
scale = Math.max(1024, Math.min(256 * Math.pow(2, 24), scale));
projection.scale(scale / (2 * Math.PI));
zoom.scale(scale);
var t = projection.translate();
l = locationPoint(l);
t[0] += center[0] - l[0];
t[1] += center[1] - l[1];
projection.translate(t);
zoom.translate(projection.translate());
return true;
}
function setCenter(_) {
var c = map.center();
if (_[0] === c[0] && _[1] === c[1])
return false;
var t = projection.translate(),
pxC = pxCenter(),
ll = projection(_);
projection.translate([
t[0] - ll[0] + pxC[0],
t[1] - ll[1] + pxC[1]]);
zoom.translate(projection.translate());
return true;
}
map.pan = function(d) {
var t = projection.translate();
t[0] += d[0];
t[1] += d[1];
projection.translate(t);
zoom.translate(projection.translate());
dispatch.move(map);
return redraw();
};
map.dimensions = function(_) {
if (!arguments.length) return dimensions;
var center = map.center();
dimensions = _;
drawLayers.dimensions(dimensions);
context.background().dimensions(dimensions);
projection.clipExtent([[0, 0], dimensions]);
mouse = fastMouse(supersurface.node());
setCenter(center);
return redraw();
};
function zoomIn(integer) {
interpolateZoom(~~map.zoom() + integer);
}
function zoomOut(integer) {
interpolateZoom(~~map.zoom() - integer);
}
map.zoomIn = function() { zoomIn(1); };
map.zoomInFurther = function() { zoomIn(4); };
map.zoomOut = function() { zoomOut(1); };
map.zoomOutFurther = function() { zoomOut(4); };
map.center = function(loc) {
if (!arguments.length) {
return projection.invert(pxCenter());
}
if (setCenter(loc)) {
dispatch.move(map);
}
return redraw();
};
map.zoom = function(z) {
if (!arguments.length) {
return Math.max(Math.log(projection.scale() * 2 * Math.PI) / Math.LN2 - 8, 0);
}
if (z < minzoom) {
surface.interrupt();
flash(context.container())
.select('.content')
.text(t('cannot_zoom'));
z = context.minEditableZoom();
}
if (setZoom(z)) {
dispatch.move(map);
}
return redraw();
};
map.zoomTo = function(entity, zoomLimits) {
var extent = entity.extent(context.graph());
if (!isFinite(extent.area())) return;
var zoom = map.trimmedExtentZoom(extent);
zoomLimits = zoomLimits || [context.minEditableZoom(), 20];
map.centerZoom(extent.center(), Math.min(Math.max(zoom, zoomLimits[0]), zoomLimits[1]));
};
map.centerZoom = function(loc, z) {
var centered = setCenter(loc),
zoomed = setZoom(z);
if (centered || zoomed) {
dispatch.move(map);
}
return redraw();
};
map.centerEase = function(loc2, duration) {
duration = duration || 250;
surface.one('mousedown.ease', function() {
map.cancelEase();
});
if (easing) {
map.cancelEase();
}
var t1 = Date.now(),
t2 = t1 + duration,
loc1 = map.center(),
ease = d3.ease('cubic-in-out');
easing = true;
d3.timer(function() {
if (!easing) return true; // cancelled ease
var tNow = Date.now();
if (tNow > t2) {
tNow = t2;
easing = false;
}
var locNow = interp(loc1, loc2, ease((tNow - t1) / duration));
setCenter(locNow);
d3.event = {
scale: zoom.scale(),
translate: zoom.translate()
};
zoomPan();
return !easing;
});
return map;
};
map.cancelEase = function() {
easing = false;
d3.timer.flush();
return map;
};
map.extent = function(_) {
if (!arguments.length) {
return new Extent(projection.invert([0, dimensions[1]]),
projection.invert([dimensions[0], 0]));
} else {
var extent = Extent(_);
map.centerZoom(extent.center(), map.extentZoom(extent));
}
};
map.trimmedExtent = function(_) {
if (!arguments.length) {
var headerY = 60, footerY = 30, pad = 10;
return new Extent(projection.invert([pad, dimensions[1] - footerY - pad]),
projection.invert([dimensions[0] - pad, headerY + pad]));
} else {
var extent = Extent(_);
map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));
}
};
function calcZoom(extent, dim) {
var tl = projection([extent[0][0], extent[1][1]]),
br = projection([extent[1][0], extent[0][1]]);
// Calculate maximum zoom that fits extent
var hFactor = (br[0] - tl[0]) / dim[0],
vFactor = (br[1] - tl[1]) / dim[1],
hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2,
vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2,
newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff);
return newZoom;
}
map.extentZoom = function(_) {
return calcZoom(Extent(_), dimensions);
};
map.trimmedExtentZoom = function(_) {
var trimY = 120, trimX = 40,
trimmed = [dimensions[0] - trimX, dimensions[1] - trimY];
return calcZoom(Extent(_), trimmed);
};
map.editable = function() {
return map.zoom() >= context.minEditableZoom();
};
map.minzoom = function(_) {
if (!arguments.length) return minzoom;
minzoom = _;
return map;
};
map.layers = drawLayers;
return d3.rebind(map, dispatch, 'on');
}
exports.actions = actions;
exports.geo = geo;
exports.behavior = behavior;
exports.modes = modes;
exports.operations = Operations;
exports.presets = presets;
exports.util = util;
exports.validations = validations;
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;
exports.BackgroundSource = BackgroundSource;
exports.Background = Background$1;
exports.Features = Features;
exports.Map = Map;
exports.TileLayer = TileLayer;
Object.defineProperty(exports, '__esModule', { value: true });
}));