mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-01 16:08:00 +02:00
930e865b42
(closes #5200) This removes a users changes stored in localStorage if they try to close iD while an upload is in progress. It prevents the possiblity that the changeset is eventually accepted and closed, but the user can be prompted to restore those changes the next time they start iD.
1119 lines
33 KiB
JavaScript
1119 lines
33 KiB
JavaScript
import _chunk from 'lodash-es/chunk';
|
|
import _cloneDeep from 'lodash-es/cloneDeep';
|
|
import _extend from 'lodash-es/extend';
|
|
import _forEach from 'lodash-es/forEach';
|
|
import _find from 'lodash-es/find';
|
|
import _groupBy from 'lodash-es/groupBy';
|
|
import _isEmpty from 'lodash-es/isEmpty';
|
|
import _map from 'lodash-es/map';
|
|
import _throttle from 'lodash-es/throttle';
|
|
import _uniq from 'lodash-es/uniq';
|
|
|
|
import rbush from 'rbush';
|
|
|
|
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
|
import { xml as d3_xml } from 'd3-request';
|
|
|
|
import osmAuth from 'osm-auth';
|
|
import { JXON } from '../util/jxon';
|
|
import { geoExtent, geoVecAdd } from '../geo';
|
|
|
|
import {
|
|
osmEntity,
|
|
osmNode,
|
|
osmNote,
|
|
osmRelation,
|
|
osmWay
|
|
} from '../osm';
|
|
|
|
import {
|
|
utilRebind,
|
|
utilIdleWorker,
|
|
utilTiler,
|
|
utilQsString
|
|
} from '../util';
|
|
|
|
|
|
var tiler = utilTiler();
|
|
var dispatch = d3_dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded', 'loadedNotes');
|
|
var urlroot = 'https://www.openstreetmap.org';
|
|
var oauth = osmAuth({
|
|
url: urlroot,
|
|
oauth_consumer_key: '5A043yRSEugj4DJ5TljuapfnrflWDte8jTOcWLlT',
|
|
oauth_secret: 'aB3jKq1TRsCOUrfOIZ6oQMEDmv2ptV76PA54NGLL',
|
|
loading: authLoading,
|
|
done: authDone
|
|
});
|
|
|
|
var _blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*'];
|
|
var _tileCache = { loaded: {}, inflight: {}, seen: {} };
|
|
var _noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, rtree: rbush() };
|
|
var _userCache = { toLoad: {}, user: {} };
|
|
var _changeset = {};
|
|
|
|
var _connectionID = 1;
|
|
var _tileZoom = 16;
|
|
var _noteZoom = 12;
|
|
var _rateLimitError;
|
|
var _userChangesets;
|
|
var _userDetails;
|
|
var _off;
|
|
|
|
|
|
function authLoading() {
|
|
dispatch.call('authLoading');
|
|
}
|
|
|
|
|
|
function authDone() {
|
|
dispatch.call('authDone');
|
|
}
|
|
|
|
|
|
function abortRequest(i) {
|
|
if (i) {
|
|
i.abort();
|
|
}
|
|
}
|
|
|
|
|
|
function abortUnwantedRequests(cache, tiles) {
|
|
_forEach(cache.inflight, function(v, k) {
|
|
var wanted = _find(tiles, function(tile) { return k === tile.id; });
|
|
if (!wanted) {
|
|
abortRequest(v);
|
|
delete cache.inflight[k];
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function getLoc(attrs) {
|
|
var lon = attrs.lon && attrs.lon.value;
|
|
var lat = attrs.lat && attrs.lat.value;
|
|
return [parseFloat(lon), parseFloat(lat)];
|
|
}
|
|
|
|
|
|
function getNodes(obj) {
|
|
var elems = obj.getElementsByTagName('nd');
|
|
var 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('tag');
|
|
var 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('member');
|
|
var 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');
|
|
}
|
|
|
|
|
|
function parseComments(comments) {
|
|
var parsedComments = [];
|
|
|
|
// for each comment
|
|
for (var i = 0; i < comments.length; i++) {
|
|
var comment = comments[i];
|
|
if (comment.nodeName === 'comment') {
|
|
var childNodes = comment.childNodes;
|
|
var parsedComment = {};
|
|
|
|
for (var j = 0; j < childNodes.length; j++) {
|
|
var node = childNodes[j];
|
|
var nodeName = node.nodeName;
|
|
if (nodeName === '#text') continue;
|
|
parsedComment[nodeName] = node.textContent;
|
|
|
|
if (nodeName === 'uid') {
|
|
var uid = node.textContent;
|
|
if (uid && !_userCache.user[uid]) {
|
|
_userCache.toLoad[uid] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (parsedComment) {
|
|
parsedComments.push(parsedComment);
|
|
}
|
|
}
|
|
}
|
|
return parsedComments;
|
|
}
|
|
|
|
|
|
function encodeNoteRtree(note) {
|
|
return {
|
|
minX: note.loc[0],
|
|
minY: note.loc[1],
|
|
maxX: note.loc[0],
|
|
maxY: note.loc[1],
|
|
data: note
|
|
};
|
|
}
|
|
|
|
|
|
var parsers = {
|
|
node: function nodeData(obj, uid) {
|
|
var attrs = obj.attributes;
|
|
return new osmNode({
|
|
id: uid,
|
|
visible: getVisible(attrs),
|
|
version: attrs.version.value,
|
|
changeset: attrs.changeset && attrs.changeset.value,
|
|
timestamp: attrs.timestamp && attrs.timestamp.value,
|
|
user: attrs.user && attrs.user.value,
|
|
uid: attrs.uid && attrs.uid.value,
|
|
loc: getLoc(attrs),
|
|
tags: getTags(obj)
|
|
});
|
|
},
|
|
|
|
way: function wayData(obj, uid) {
|
|
var attrs = obj.attributes;
|
|
return new osmWay({
|
|
id: uid,
|
|
visible: getVisible(attrs),
|
|
version: attrs.version.value,
|
|
changeset: attrs.changeset && attrs.changeset.value,
|
|
timestamp: attrs.timestamp && attrs.timestamp.value,
|
|
user: attrs.user && attrs.user.value,
|
|
uid: attrs.uid && attrs.uid.value,
|
|
tags: getTags(obj),
|
|
nodes: getNodes(obj),
|
|
});
|
|
},
|
|
|
|
relation: function relationData(obj, uid) {
|
|
var attrs = obj.attributes;
|
|
return new osmRelation({
|
|
id: uid,
|
|
visible: getVisible(attrs),
|
|
version: attrs.version.value,
|
|
changeset: attrs.changeset && attrs.changeset.value,
|
|
timestamp: attrs.timestamp && attrs.timestamp.value,
|
|
user: attrs.user && attrs.user.value,
|
|
uid: attrs.uid && attrs.uid.value,
|
|
tags: getTags(obj),
|
|
members: getMembers(obj)
|
|
});
|
|
},
|
|
|
|
note: function parseNote(obj, uid) {
|
|
var attrs = obj.attributes;
|
|
var childNodes = obj.childNodes;
|
|
var props = {};
|
|
|
|
props.id = uid;
|
|
props.loc = getLoc(attrs);
|
|
|
|
// if notes are coincident, move them apart slightly
|
|
var coincident = false;
|
|
var epsilon = 0.00001;
|
|
do {
|
|
if (coincident) {
|
|
props.loc = geoVecAdd(props.loc, [epsilon, epsilon]);
|
|
}
|
|
var bbox = geoExtent(props.loc).bbox();
|
|
coincident = _noteCache.rtree.search(bbox).length;
|
|
} while (coincident);
|
|
|
|
// parse note contents
|
|
for (var i = 0; i < childNodes.length; i++) {
|
|
var node = childNodes[i];
|
|
var nodeName = node.nodeName;
|
|
if (nodeName === '#text') continue;
|
|
|
|
// if the element is comments, parse the comments
|
|
if (nodeName === 'comments') {
|
|
props[nodeName] = parseComments(node.childNodes);
|
|
} else {
|
|
props[nodeName] = node.textContent;
|
|
}
|
|
}
|
|
|
|
var note = new osmNote(props);
|
|
var item = encodeNoteRtree(note);
|
|
_noteCache.note[note.id] = note;
|
|
_noteCache.rtree.insert(item);
|
|
|
|
return note;
|
|
},
|
|
|
|
user: function parseUser(obj, uid) {
|
|
var attrs = obj.attributes;
|
|
var user = {
|
|
id: uid,
|
|
display_name: attrs.display_name && attrs.display_name.value,
|
|
account_created: attrs.account_created && attrs.account_created.value,
|
|
changesets_count: 0
|
|
};
|
|
|
|
var img = obj.getElementsByTagName('img');
|
|
if (img && img[0] && img[0].getAttribute('href')) {
|
|
user.image_url = img[0].getAttribute('href');
|
|
}
|
|
|
|
var changesets = obj.getElementsByTagName('changesets');
|
|
if (changesets && changesets[0] && changesets[0].getAttribute('count')) {
|
|
user.changesets_count = changesets[0].getAttribute('count');
|
|
}
|
|
|
|
_userCache.user[uid] = user;
|
|
delete _userCache.toLoad[uid];
|
|
return user;
|
|
}
|
|
};
|
|
|
|
|
|
function parseXML(xml, callback, options) {
|
|
options = _extend({ skipSeen: true }, options);
|
|
if (!xml || !xml.childNodes) {
|
|
return callback({ message: 'No XML', status: -1 });
|
|
}
|
|
|
|
var root = xml.childNodes[0];
|
|
var children = root.childNodes;
|
|
utilIdleWorker(children, parseChild, done);
|
|
|
|
|
|
function done(results) {
|
|
callback(null, results);
|
|
}
|
|
|
|
function parseChild(child) {
|
|
var parser = parsers[child.nodeName];
|
|
if (!parser) return null;
|
|
|
|
var uid;
|
|
if (child.nodeName === 'user') {
|
|
uid = child.attributes.id.value;
|
|
if (options.skipSeen && _userCache.user[uid]) {
|
|
delete _userCache.toLoad[uid];
|
|
return null;
|
|
}
|
|
|
|
} else if (child.nodeName === 'note') {
|
|
uid = child.getElementsByTagName('id')[0].textContent;
|
|
|
|
} else {
|
|
uid = osmEntity.id.fromOSM(child.nodeName, child.attributes.id.value);
|
|
if (options.skipSeen) {
|
|
if (_tileCache.seen[uid]) return null; // avoid reparsing a "seen" entity
|
|
_tileCache.seen[uid] = true;
|
|
}
|
|
}
|
|
|
|
return parser(child, uid);
|
|
}
|
|
}
|
|
|
|
|
|
// replace or remove note from rtree
|
|
function updateRtree(item, replace) {
|
|
_noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; });
|
|
|
|
if (replace) {
|
|
_noteCache.rtree.insert(item);
|
|
}
|
|
}
|
|
|
|
|
|
function wrapcb(thisArg, callback, cid) {
|
|
return function(err, result) {
|
|
if (err) {
|
|
// 400 Bad Request, 401 Unauthorized, 403 Forbidden..
|
|
if (err.status === 400 || err.status === 401 || err.status === 403) {
|
|
thisArg.logout();
|
|
}
|
|
return callback.call(thisArg, err);
|
|
|
|
} else if (thisArg.getConnectionId() !== cid) {
|
|
return callback.call(thisArg, { message: 'Connection Switched', status: -1 });
|
|
|
|
} else {
|
|
return callback.call(thisArg, err, result);
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
export default {
|
|
|
|
init: function() {
|
|
utilRebind(this, dispatch, 'on');
|
|
},
|
|
|
|
|
|
reset: function() {
|
|
_connectionID++;
|
|
_userChangesets = undefined;
|
|
_userDetails = undefined;
|
|
_rateLimitError = undefined;
|
|
|
|
_forEach(_tileCache.inflight, abortRequest);
|
|
_forEach(_noteCache.inflight, abortRequest);
|
|
_forEach(_noteCache.inflightPost, abortRequest);
|
|
if (_changeset.inflight) abortRequest(_changeset.inflight);
|
|
|
|
_tileCache = { loaded: {}, inflight: {}, seen: {} };
|
|
_noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, rtree: rbush() };
|
|
_userCache = { toLoad: {}, user: {} };
|
|
_changeset = {};
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
getConnectionId: function() {
|
|
return _connectionID;
|
|
},
|
|
|
|
|
|
changesetURL: function(changesetID) {
|
|
return urlroot + '/changeset/' + changesetID;
|
|
},
|
|
|
|
|
|
changesetsURL: function(center, zoom) {
|
|
var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
|
|
return urlroot + '/history#map=' +
|
|
Math.floor(zoom) + '/' +
|
|
center[1].toFixed(precision) + '/' +
|
|
center[0].toFixed(precision);
|
|
},
|
|
|
|
|
|
entityURL: function(entity) {
|
|
return urlroot + '/' + entity.type + '/' + entity.osmId();
|
|
},
|
|
|
|
|
|
historyURL: function(entity) {
|
|
return urlroot + '/' + entity.type + '/' + entity.osmId() + '/history';
|
|
},
|
|
|
|
|
|
userURL: function(username) {
|
|
return urlroot + '/user/' + username;
|
|
},
|
|
|
|
|
|
noteURL: function(note) {
|
|
return urlroot + '/note/' + note.id;
|
|
},
|
|
|
|
|
|
// Generic method to load data from the OSM API
|
|
// Can handle either auth or unauth calls.
|
|
loadFromAPI: function(path, callback, options) {
|
|
options = _extend({ skipSeen: true }, options);
|
|
var that = this;
|
|
var cid = _connectionID;
|
|
|
|
function done(err, xml) {
|
|
if (that.getConnectionId() !== cid) {
|
|
if (callback) callback({ message: 'Connection Switched', status: -1 });
|
|
return;
|
|
}
|
|
|
|
var isAuthenticated = that.authenticated();
|
|
|
|
// 400 Bad Request, 401 Unauthorized, 403 Forbidden
|
|
// Logout and retry the request..
|
|
if (isAuthenticated && err && (err.status === 400 || err.status === 401 || err.status === 403)) {
|
|
that.logout();
|
|
that.loadFromAPI(path, callback, options);
|
|
|
|
// else, no retry..
|
|
} else {
|
|
// 509 Bandwidth Limit Exceeded, 429 Too Many Requests
|
|
// Set the rateLimitError flag and trigger a warning..
|
|
if (!isAuthenticated && !_rateLimitError && err &&
|
|
(err.status === 509 || err.status === 429)) {
|
|
_rateLimitError = err;
|
|
dispatch.call('change');
|
|
}
|
|
|
|
if (callback) {
|
|
if (err) {
|
|
return callback(err);
|
|
} else {
|
|
return parseXML(xml, callback, options);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.authenticated()) {
|
|
return oauth.xhr({ method: 'GET', path: path }, done);
|
|
} else {
|
|
var url = urlroot + path;
|
|
return d3_xml(url).get(done);
|
|
}
|
|
},
|
|
|
|
|
|
// Load a single entity by id (ways and relations use the `/full` call)
|
|
// GET /api/0.6/node/#id
|
|
// GET /api/0.6/[way|relation]/#id/full
|
|
loadEntity: function(id, callback) {
|
|
var type = osmEntity.id.type(id);
|
|
var osmID = osmEntity.id.toOSM(id);
|
|
var options = { skipSeen: false };
|
|
|
|
this.loadFromAPI(
|
|
'/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : ''),
|
|
function(err, entities) {
|
|
if (callback) callback(err, { data: entities });
|
|
},
|
|
options
|
|
);
|
|
},
|
|
|
|
|
|
// Load a single entity with a specific version
|
|
// GET /api/0.6/[node|way|relation]/#id/#version
|
|
loadEntityVersion: function(id, version, callback) {
|
|
var type = osmEntity.id.type(id);
|
|
var osmID = osmEntity.id.toOSM(id);
|
|
var options = { skipSeen: false };
|
|
|
|
this.loadFromAPI(
|
|
'/api/0.6/' + type + '/' + osmID + '/' + version,
|
|
function(err, entities) {
|
|
if (callback) callback(err, { data: entities });
|
|
},
|
|
options
|
|
);
|
|
},
|
|
|
|
|
|
// Load multiple entities in chunks
|
|
// (note: callback may be called multiple times)
|
|
// GET /api/0.6/[nodes|ways|relations]?#parameters
|
|
loadMultiple: function(ids, callback) {
|
|
var that = this;
|
|
|
|
_forEach(_groupBy(_uniq(ids), osmEntity.id.type), function(v, k) {
|
|
var type = k + 's';
|
|
var osmIDs = _map(v, osmEntity.id.toOSM);
|
|
var options = { skipSeen: false };
|
|
|
|
_forEach(_chunk(osmIDs, 150), function(arr) {
|
|
that.loadFromAPI(
|
|
'/api/0.6/' + type + '?' + type + '=' + arr.join(),
|
|
function(err, entities) {
|
|
if (callback) callback(err, { data: entities });
|
|
},
|
|
options
|
|
);
|
|
});
|
|
});
|
|
},
|
|
|
|
|
|
// Create, upload, and close a changeset
|
|
// PUT /api/0.6/changeset/create
|
|
// POST /api/0.6/changeset/#id/upload
|
|
// PUT /api/0.6/changeset/#id/close
|
|
putChangeset: function(changeset, changes, callback) {
|
|
var cid = _connectionID;
|
|
|
|
if (_changeset.inflight) {
|
|
return callback({ message: 'Changeset already inflight', status: -2 }, changeset);
|
|
|
|
} else if (_changeset.open) { // reuse existing open changeset..
|
|
return createdChangeset(null, _changeset.open);
|
|
|
|
} else { // Open a new changeset..
|
|
var options = {
|
|
method: 'PUT',
|
|
path: '/api/0.6/changeset/create',
|
|
options: { header: { 'Content-Type': 'text/xml' } },
|
|
content: JXON.stringify(changeset.asJXON())
|
|
};
|
|
_changeset.inflight = oauth.xhr(
|
|
options,
|
|
wrapcb(this, createdChangeset, cid)
|
|
);
|
|
}
|
|
|
|
|
|
function createdChangeset(err, changesetID) {
|
|
_changeset.inflight = null;
|
|
if (err) { return callback(err, changeset); }
|
|
|
|
_changeset.open = changesetID;
|
|
changeset = changeset.update({ id: changesetID });
|
|
|
|
// Upload the changeset..
|
|
var options = {
|
|
method: 'POST',
|
|
path: '/api/0.6/changeset/' + changesetID + '/upload',
|
|
options: { header: { 'Content-Type': 'text/xml' } },
|
|
content: JXON.stringify(changeset.osmChangeJXON(changes))
|
|
};
|
|
_changeset.inflight = oauth.xhr(
|
|
options,
|
|
wrapcb(this, uploadedChangeset, cid)
|
|
);
|
|
}
|
|
|
|
|
|
function uploadedChangeset(err) {
|
|
_changeset.inflight = null;
|
|
if (err) return callback(err, changeset);
|
|
|
|
// Upload was successful, safe to call the callback.
|
|
// Add delay to allow for postgres replication #1646 #2678
|
|
window.setTimeout(function() { callback(null, changeset); }, 2500);
|
|
_changeset.open = null;
|
|
|
|
// At this point, we don't really care if the connection was switched..
|
|
// Only try to close the changeset if we're still talking to the same server.
|
|
if (this.getConnectionId() === cid) {
|
|
// Still attempt to close changeset, but ignore response because #2667
|
|
oauth.xhr({
|
|
method: 'PUT',
|
|
path: '/api/0.6/changeset/' + changeset.id + '/close',
|
|
options: { header: { 'Content-Type': 'text/xml' } }
|
|
}, function() { return true; });
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
// Load multiple users in chunks
|
|
// (note: callback may be called multiple times)
|
|
// GET /api/0.6/users?users=#id1,#id2,...,#idn
|
|
loadUsers: function(uids, callback) {
|
|
var toLoad = [];
|
|
var cached = [];
|
|
|
|
_uniq(uids).forEach(function(uid) {
|
|
if (_userCache.user[uid]) {
|
|
delete _userCache.toLoad[uid];
|
|
cached.push(_userCache.user[uid]);
|
|
} else {
|
|
toLoad.push(uid);
|
|
}
|
|
});
|
|
|
|
if (cached.length || !this.authenticated()) {
|
|
callback(undefined, cached);
|
|
if (!this.authenticated()) return; // require auth
|
|
}
|
|
|
|
_chunk(toLoad, 150).forEach(function(arr) {
|
|
oauth.xhr(
|
|
{ method: 'GET', path: '/api/0.6/users?users=' + arr.join() },
|
|
wrapcb(this, done, _connectionID)
|
|
);
|
|
}.bind(this));
|
|
|
|
function done(err, xml) {
|
|
if (err) { return callback(err); }
|
|
|
|
var options = { skipSeen: true };
|
|
return parseXML(xml, function(err, results) {
|
|
if (err) {
|
|
return callback(err);
|
|
} else {
|
|
return callback(undefined, results);
|
|
}
|
|
}, options);
|
|
}
|
|
},
|
|
|
|
|
|
// Load a given user by id
|
|
// GET /api/0.6/user/#id
|
|
loadUser: function(uid, callback) {
|
|
if (_userCache.user[uid] || !this.authenticated()) { // require auth
|
|
delete _userCache.toLoad[uid];
|
|
return callback(undefined, _userCache.user[uid]);
|
|
}
|
|
|
|
oauth.xhr(
|
|
{ method: 'GET', path: '/api/0.6/user/' + uid },
|
|
wrapcb(this, done, _connectionID)
|
|
);
|
|
|
|
function done(err, xml) {
|
|
if (err) { return callback(err); }
|
|
|
|
var options = { skipSeen: true };
|
|
return parseXML(xml, function(err, results) {
|
|
if (err) {
|
|
return callback(err);
|
|
} else {
|
|
return callback(undefined, results[0]);
|
|
}
|
|
}, options);
|
|
}
|
|
},
|
|
|
|
|
|
// Load the details of the logged-in user
|
|
// GET /api/0.6/user/details
|
|
userDetails: function(callback) {
|
|
if (_userDetails) { // retrieve cached
|
|
return callback(undefined, _userDetails);
|
|
}
|
|
|
|
oauth.xhr(
|
|
{ method: 'GET', path: '/api/0.6/user/details' },
|
|
wrapcb(this, done, _connectionID)
|
|
);
|
|
|
|
function done(err, xml) {
|
|
if (err) { return callback(err); }
|
|
|
|
var options = { skipSeen: false };
|
|
return parseXML(xml, function(err, results) {
|
|
if (err) {
|
|
return callback(err);
|
|
} else {
|
|
_userDetails = results[0];
|
|
return callback(undefined, _userDetails);
|
|
}
|
|
}, options);
|
|
}
|
|
},
|
|
|
|
|
|
// Load previous changesets for the logged in user
|
|
// GET /api/0.6/changesets?user=#id
|
|
userChangesets: function(callback) {
|
|
if (_userChangesets) { // retrieve cached
|
|
return callback(undefined, _userChangesets);
|
|
}
|
|
|
|
this.userDetails(
|
|
wrapcb(this, gotDetails, _connectionID)
|
|
);
|
|
|
|
|
|
function gotDetails(err, user) {
|
|
if (err) { return callback(err); }
|
|
|
|
oauth.xhr(
|
|
{ method: 'GET', path: '/api/0.6/changesets?user=' + user.id },
|
|
wrapcb(this, done, _connectionID)
|
|
);
|
|
}
|
|
|
|
function done(err, xml) {
|
|
if (err) { return callback(err); }
|
|
|
|
_userChangesets = Array.prototype.map.call(
|
|
xml.getElementsByTagName('changeset'),
|
|
function (changeset) { return { tags: getTags(changeset) }; }
|
|
).filter(function (changeset) {
|
|
var comment = changeset.tags.comment;
|
|
return comment && comment !== '';
|
|
});
|
|
|
|
return callback(undefined, _userChangesets);
|
|
}
|
|
},
|
|
|
|
|
|
// Fetch the status of the OSM API
|
|
// GET /api/capabilities
|
|
status: function(callback) {
|
|
d3_xml(urlroot + '/api/capabilities').get(
|
|
wrapcb(this, done, _connectionID)
|
|
);
|
|
|
|
function done(err, xml) {
|
|
if (err) { return callback(err); }
|
|
|
|
// update blacklists
|
|
var elements = xml.getElementsByTagName('blacklist');
|
|
var regexes = [];
|
|
for (var i = 0; i < elements.length; i++) {
|
|
var regex = elements[i].getAttribute('regex'); // needs unencode?
|
|
if (regex) {
|
|
regexes.push(regex);
|
|
}
|
|
}
|
|
if (regexes.length) {
|
|
_blacklists = regexes;
|
|
}
|
|
|
|
if (_rateLimitError) {
|
|
return callback(_rateLimitError, 'rateLimited');
|
|
} else {
|
|
var apiStatus = xml.getElementsByTagName('status');
|
|
var val = apiStatus[0].getAttribute('api');
|
|
return callback(undefined, val);
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
// Load data (entities) from the API in tiles
|
|
// GET /api/0.6/map?bbox=
|
|
loadTiles: function(projection, callback) {
|
|
if (_off) return;
|
|
|
|
var that = this;
|
|
var path = '/api/0.6/map?bbox=';
|
|
|
|
// determine the needed tiles to cover the view
|
|
var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);
|
|
|
|
// abort inflight requests that are no longer needed
|
|
var hadRequests = !_isEmpty(_tileCache.inflight);
|
|
abortUnwantedRequests(_tileCache, tiles);
|
|
if (hadRequests && _isEmpty(_tileCache.inflight)) {
|
|
dispatch.call('loaded'); // stop the spinner
|
|
}
|
|
|
|
// issue new requests..
|
|
tiles.forEach(function(tile) {
|
|
if (_tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;
|
|
if (_isEmpty(_tileCache.inflight)) {
|
|
dispatch.call('loading'); // start the spinner
|
|
}
|
|
|
|
var options = { skipSeen: true };
|
|
_tileCache.inflight[tile.id] = that.loadFromAPI(
|
|
path + tile.extent.toParam(),
|
|
function(err, parsed) {
|
|
delete _tileCache.inflight[tile.id];
|
|
if (!err) {
|
|
_tileCache.loaded[tile.id] = true;
|
|
}
|
|
if (callback) {
|
|
callback(err, _extend({ data: parsed }, tile));
|
|
}
|
|
if (_isEmpty(_tileCache.inflight)) {
|
|
dispatch.call('loaded'); // stop the spinner
|
|
}
|
|
},
|
|
options
|
|
);
|
|
});
|
|
},
|
|
|
|
|
|
// Load notes from the API in tiles
|
|
// GET /api/0.6/notes?bbox=
|
|
loadNotes: function(projection, noteOptions) {
|
|
noteOptions = _extend({ limit: 10000, closed: 7 }, noteOptions);
|
|
if (_off) return;
|
|
|
|
var that = this;
|
|
var path = '/api/0.6/notes?limit=' + noteOptions.limit + '&closed=' + noteOptions.closed + '&bbox=';
|
|
var throttleLoadUsers = _throttle(function() {
|
|
var uids = Object.keys(_userCache.toLoad);
|
|
if (!uids.length) return;
|
|
that.loadUsers(uids, function() {}); // eagerly load user details
|
|
}, 750);
|
|
|
|
// determine the needed tiles to cover the view
|
|
var tiles = tiler.zoomExtent([_noteZoom, _noteZoom]).getTiles(projection);
|
|
|
|
// abort inflight requests that are no longer needed
|
|
abortUnwantedRequests(_noteCache, tiles);
|
|
|
|
// issue new requests..
|
|
tiles.forEach(function(tile) {
|
|
if (_noteCache.loaded[tile.id] || _noteCache.inflight[tile.id]) return;
|
|
|
|
var options = { skipSeen: false };
|
|
_noteCache.inflight[tile.id] = that.loadFromAPI(
|
|
path + tile.extent.toParam(),
|
|
function(err) {
|
|
delete _noteCache.inflight[tile.id];
|
|
if (!err) {
|
|
_noteCache.loaded[tile.id] = true;
|
|
}
|
|
throttleLoadUsers();
|
|
dispatch.call('loadedNotes');
|
|
},
|
|
options
|
|
);
|
|
});
|
|
},
|
|
|
|
|
|
// Create a note
|
|
// POST /api/0.6/notes?params
|
|
postNoteCreate: function(note, callback) {
|
|
if (!this.authenticated()) {
|
|
return callback({ message: 'Not Authenticated', status: -3 }, note);
|
|
}
|
|
if (_noteCache.inflightPost[note.id]) {
|
|
return callback({ message: 'Note update already inflight', status: -2 }, note);
|
|
}
|
|
|
|
if (!note.loc[0] || !note.loc[1] || !note.newComment) return; // location & description required
|
|
|
|
var comment = note.newComment;
|
|
if (note.newCategory && note.newCategory !== 'None') { comment += ' #' + note.newCategory; }
|
|
|
|
var path = '/api/0.6/notes?' + utilQsString({ lon: note.loc[0], lat: note.loc[1], text: comment });
|
|
|
|
_noteCache.inflightPost[note.id] = oauth.xhr(
|
|
{ method: 'POST', path: path },
|
|
wrapcb(this, done, _connectionID)
|
|
);
|
|
|
|
|
|
function done(err, xml) {
|
|
delete _noteCache.inflightPost[note.id];
|
|
if (err) { return callback(err); }
|
|
|
|
// we get the updated note back, remove from caches and reparse..
|
|
this.removeNote(note);
|
|
|
|
var options = { skipSeen: false };
|
|
return parseXML(xml, function(err, results) {
|
|
if (err) {
|
|
return callback(err);
|
|
} else {
|
|
return callback(undefined, results[0]);
|
|
}
|
|
}, options);
|
|
}
|
|
},
|
|
|
|
|
|
// Update a note
|
|
// POST /api/0.6/notes/#id/comment?text=comment
|
|
// POST /api/0.6/notes/#id/close?text=comment
|
|
// POST /api/0.6/notes/#id/reopen?text=comment
|
|
postNoteUpdate: function(note, newStatus, callback) {
|
|
if (!this.authenticated()) {
|
|
return callback({ message: 'Not Authenticated', status: -3 }, note);
|
|
}
|
|
if (_noteCache.inflightPost[note.id]) {
|
|
return callback({ message: 'Note update already inflight', status: -2 }, note);
|
|
}
|
|
|
|
var action;
|
|
if (note.status !== 'closed' && newStatus === 'closed') {
|
|
action = 'close';
|
|
} else if (note.status !== 'open' && newStatus === 'open') {
|
|
action = 'reopen';
|
|
} else {
|
|
action = 'comment';
|
|
if (!note.newComment) return; // when commenting, comment required
|
|
}
|
|
|
|
var path = '/api/0.6/notes/' + note.id + '/' + action;
|
|
if (note.newComment) {
|
|
path += '?' + utilQsString({ text: note.newComment });
|
|
}
|
|
|
|
_noteCache.inflightPost[note.id] = oauth.xhr(
|
|
{ method: 'POST', path: path },
|
|
wrapcb(this, done, _connectionID)
|
|
);
|
|
|
|
|
|
function done(err, xml) {
|
|
delete _noteCache.inflightPost[note.id];
|
|
if (err) { return callback(err); }
|
|
|
|
// we get the updated note back, remove from caches and reparse..
|
|
this.removeNote(note);
|
|
|
|
var options = { skipSeen: false };
|
|
return parseXML(xml, function(err, results) {
|
|
if (err) {
|
|
return callback(err);
|
|
} else {
|
|
return callback(undefined, results[0]);
|
|
}
|
|
}, options);
|
|
}
|
|
},
|
|
|
|
|
|
switch: function(options) {
|
|
urlroot = options.urlroot;
|
|
|
|
oauth.options(_extend({
|
|
url: urlroot,
|
|
loading: authLoading,
|
|
done: authDone
|
|
}, options));
|
|
|
|
this.reset();
|
|
this.userChangesets(function() {}); // eagerly load user details/changesets
|
|
dispatch.call('change');
|
|
return this;
|
|
},
|
|
|
|
|
|
toggle: function(_) {
|
|
_off = !_;
|
|
return this;
|
|
},
|
|
|
|
|
|
isChangesetInflight: function() {
|
|
return !!_changeset.inflight;
|
|
},
|
|
|
|
|
|
// get/set cached data
|
|
// This is used to save/restore the state when entering/exiting the walkthrough
|
|
// Also used for testing purposes.
|
|
caches: function(obj) {
|
|
if (!arguments.length) {
|
|
return {
|
|
tile: _cloneDeep(_tileCache),
|
|
note: _cloneDeep(_noteCache),
|
|
user: _cloneDeep(_userCache)
|
|
};
|
|
}
|
|
|
|
// access caches directly for testing (e.g., loading notes rtree)
|
|
if (obj === 'get') {
|
|
return {
|
|
tile: _tileCache,
|
|
note: _noteCache,
|
|
user: _userCache
|
|
};
|
|
}
|
|
|
|
if (obj.tile) {
|
|
_tileCache = obj.tile;
|
|
_tileCache.inflight = {};
|
|
}
|
|
if (obj.note) {
|
|
_noteCache = obj.note;
|
|
_noteCache.inflight = {};
|
|
_noteCache.inflightPost = {};
|
|
}
|
|
if (obj.user) {
|
|
_userCache = obj.user;
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
logout: function() {
|
|
_userChangesets = undefined;
|
|
_userDetails = undefined;
|
|
oauth.logout();
|
|
dispatch.call('change');
|
|
return this;
|
|
},
|
|
|
|
|
|
authenticated: function() {
|
|
return oauth.authenticated();
|
|
},
|
|
|
|
|
|
authenticate: function(callback) {
|
|
var that = this;
|
|
var cid = _connectionID;
|
|
_userChangesets = undefined;
|
|
_userDetails = undefined;
|
|
|
|
function done(err, res) {
|
|
if (err) {
|
|
if (callback) callback(err);
|
|
return;
|
|
}
|
|
if (that.getConnectionId() !== cid) {
|
|
if (callback) callback({ message: 'Connection Switched', status: -1 });
|
|
return;
|
|
}
|
|
_rateLimitError = undefined;
|
|
dispatch.call('change');
|
|
if (callback) callback(err, res);
|
|
that.userChangesets(function() {}); // eagerly load user details/changesets
|
|
}
|
|
|
|
return oauth.authenticate(done);
|
|
},
|
|
|
|
|
|
imageryBlacklists: function() {
|
|
return _blacklists;
|
|
},
|
|
|
|
|
|
tileZoom: function(_) {
|
|
if (!arguments.length) return _tileZoom;
|
|
_tileZoom = _;
|
|
return this;
|
|
},
|
|
|
|
|
|
// get all cached notes covering the viewport
|
|
notes: function(projection) {
|
|
var viewport = projection.clipExtent();
|
|
var min = [viewport[0][0], viewport[1][1]];
|
|
var max = [viewport[1][0], viewport[0][1]];
|
|
var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
|
|
|
|
return _noteCache.rtree.search(bbox)
|
|
.map(function(d) { return d.data; });
|
|
},
|
|
|
|
|
|
// get a single note from the cache
|
|
getNote: function(id) {
|
|
return _noteCache.note[id];
|
|
},
|
|
|
|
|
|
// remove a single note from the cache
|
|
removeNote: function(note) {
|
|
if (!(note instanceof osmNote) || !note.id) return;
|
|
|
|
delete _noteCache.note[note.id];
|
|
updateRtree(encodeNoteRtree(note), false); // false = remove
|
|
},
|
|
|
|
|
|
// replace a single note in the cache
|
|
replaceNote: function(note) {
|
|
if (!(note instanceof osmNote) || !note.id) return;
|
|
|
|
_noteCache.note[note.id] = note;
|
|
updateRtree(encodeNoteRtree(note), true); // true = replace
|
|
return note;
|
|
}
|
|
|
|
};
|