mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-10 03:47:34 +02:00
cb0e8ab66c
It will also be much faster to fetch the remote entities in batches rather than one at a time through LoadEntity. One bonus/hazard with Multi Fetch GET is that it will get deleted entities with `visible=false`, rather than returning a HTTP Status Code 410 (Gone). This will be the only way that we can really do proper undeletion (Incrementing the current version by 1 is not guaranteed to work. And if a way is moved, fetching way/full will tell us whether the childnodes are part of the way, but not necessarily whether they exist or not.) We must be careful never to merge deleted entities into the real graph. e.g, a deleted node will not have a 'loc' attribute, so code that assumes every node must have a `loc` will be broken. So because deleted entities are very special, the output from `loadMultiple` should only be used for conflict resolution for now.
437 lines
13 KiB
JavaScript
437 lines
13 KiB
JavaScript
iD.Connection = function() {
|
|
var event = d3.dispatch('authenticating', 'authenticated', 'auth', 'loading', 'loaded'),
|
|
url = 'http://www.openstreetmap.org',
|
|
connection = {},
|
|
inflight = {},
|
|
loadedTiles = {},
|
|
tileZoom = 16,
|
|
oauth = osmAuth({
|
|
url: 'http://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',
|
|
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 = iD.Entity.id.type(id),
|
|
osmID = iD.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.loadMultiple = function(ids, callback) {
|
|
// TODO: upgrade lodash and just use _.chunk
|
|
function chunk(arr, chunkSize) {
|
|
var result = [];
|
|
for (var i = 0; i < arr.length; i += chunkSize) {
|
|
result.push(arr.slice(i, i + chunkSize));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
_.each(_.groupBy(ids, iD.Entity.id.type), function(v, k) {
|
|
var type = k + 's',
|
|
osmIDs = _.map(v, iD.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 iD.Node({
|
|
id: 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 iD.Way({
|
|
id: 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 iD.Relation({
|
|
id: 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.3,
|
|
'@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.3,
|
|
'@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 tags = {
|
|
imagery_used: imageryUsed.join(';').substr(0, 255),
|
|
created_by: 'iD ' + iD.version
|
|
};
|
|
|
|
if (comment) {
|
|
tags.comment = comment;
|
|
}
|
|
|
|
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);
|
|
oauth.xhr({
|
|
method: 'PUT',
|
|
path: '/api/0.6/changeset/' + changeset_id + '/close'
|
|
}, function(err) {
|
|
callback(err, changeset_id);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
var userDetails;
|
|
|
|
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.status = function(callback) {
|
|
function done(capabilities) {
|
|
var apiStatus = capabilities.getElementsByTagName('status');
|
|
callback(undefined, apiStatus[0].getAttribute('api'));
|
|
}
|
|
d3.xml(url + '/api/capabilities').get()
|
|
.on('load', done)
|
|
.on('error', callback);
|
|
};
|
|
|
|
function abortRequest(i) { i.abort(); }
|
|
|
|
connection.tileZoom = function(_) {
|
|
if (!arguments.length) return tileZoom;
|
|
tileZoom = _;
|
|
return connection;
|
|
};
|
|
|
|
connection.loadTiles = function(projection, dimensions, callback) {
|
|
|
|
if (off) return;
|
|
|
|
var s = projection.scale() * 2 * Math.PI,
|
|
z = Math.max(Math.log(s) / Math.log(2) - 8, 0),
|
|
ts = 256 * Math.pow(2, z - tileZoom),
|
|
origin = [
|
|
s / 2 - projection.translate()[0],
|
|
s / 2 - projection.translate()[1]];
|
|
|
|
var tiles = d3.geo.tile()
|
|
.scaleExtent([tileZoom, tileZoom])
|
|
.scale(s)
|
|
.size(dimensions)
|
|
.translate(projection.translate())()
|
|
.map(function(tile) {
|
|
var x = tile[0] * ts - origin[0],
|
|
y = tile[1] * ts - origin[1];
|
|
|
|
return {
|
|
id: tile.toString(),
|
|
extent: iD.geo.Extent(
|
|
projection.invert([x, y + ts]),
|
|
projection.invert([x + ts, y]))
|
|
};
|
|
});
|
|
|
|
function bboxUrl(tile) {
|
|
return url + '/api/0.6/map?bbox=' + tile.extent.toParam();
|
|
}
|
|
|
|
_.filter(inflight, function(v, i) {
|
|
var wanted = _.find(tiles, function(tile) {
|
|
return i === tile.id;
|
|
});
|
|
if (!wanted) delete inflight[i];
|
|
return !wanted;
|
|
}).map(abortRequest);
|
|
|
|
tiles.forEach(function(tile) {
|
|
var id = tile.id;
|
|
|
|
if (loadedTiles[id] || inflight[id]) return;
|
|
|
|
if (_.isEmpty(inflight)) {
|
|
event.loading();
|
|
}
|
|
|
|
inflight[id] = connection.loadFromURL(bboxUrl(tile), function(err, parsed) {
|
|
loadedTiles[id] = true;
|
|
delete inflight[id];
|
|
|
|
if (callback) callback(err, _.extend({data: parsed}, tile));
|
|
|
|
if (_.isEmpty(inflight)) {
|
|
event.loaded();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
connection.switch = function(options) {
|
|
url = options.url;
|
|
oauth.options(_.extend({
|
|
loading: authenticating,
|
|
done: authenticated
|
|
}, options));
|
|
event.auth();
|
|
connection.flush();
|
|
return connection;
|
|
};
|
|
|
|
connection.toggle = function(_) {
|
|
off = !_;
|
|
return connection;
|
|
};
|
|
|
|
connection.flush = function() {
|
|
_.forEach(inflight, abortRequest);
|
|
loadedTiles = {};
|
|
inflight = {};
|
|
return connection;
|
|
};
|
|
|
|
connection.loadedTiles = function(_) {
|
|
if (!arguments.length) return loadedTiles;
|
|
loadedTiles = _;
|
|
return connection;
|
|
};
|
|
|
|
connection.logout = function() {
|
|
oauth.logout();
|
|
event.auth();
|
|
return connection;
|
|
};
|
|
|
|
connection.authenticate = function(callback) {
|
|
function done(err, res) {
|
|
event.auth();
|
|
if (callback) callback(err, res);
|
|
}
|
|
return oauth.authenticate(done);
|
|
};
|
|
|
|
return d3.rebind(connection, event, 'on');
|
|
};
|