diff --git a/modules/core/context.js b/modules/core/context.js index 1acc4f96b..0c4032e98 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -13,6 +13,7 @@ import { services } from '../services/index'; import { uiInit } from '../ui/init'; import { utilDetect } from '../util/detect'; import { utilRebind } from '../util/rebind'; +import { utilCallWhenIdle } from '../util/index'; export var areaKeys = {}; @@ -83,9 +84,9 @@ export function coreContext() { /* Connection */ - function entitiesLoaded(err, result) { + var entitiesLoaded = utilCallWhenIdle(function entitiesLoaded(err, result) { if (!err) history.merge(result.data, result.extent); - } + }); context.preauth = function(options) { if (connection) { @@ -94,7 +95,7 @@ export function coreContext() { return context; }; - context.loadTiles = function(projection, dimensions, callback) { + context.loadTiles = utilCallWhenIdle(function(projection, dimensions, callback) { function done(err, result) { entitiesLoaded(err, result); if (callback) callback(err, result); @@ -102,7 +103,7 @@ export function coreContext() { if (connection) { connection.loadTiles(projection, dimensions, done); } - }; + }); context.loadEntity = function(id, callback) { function done(err, result) { diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 8e82e12c8..8fbad68e3 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -64,8 +64,27 @@ export function rendererMap(context) { .on('zoom', zoomPan); var _selection = d3.select(null); + var isRedrawScheduled = false; + var pendingRedrawCall; + function scheduleRedraw() { + // Only schedule the redraw if one has not already been set. + if (isRedrawScheduled) return; + isRedrawScheduled = true; + var that = this; + var args = arguments; + pendingRedrawCall = requestIdleCallback(function () { + // Reset the boolean so future redraws can be set. + isRedrawScheduled = false; + redraw.apply(that, args); + }, { timeout: 1400 }); + } + function cancelPendingRedraw() { + isRedrawScheduled = false; + window.cancelIdleCallback(pendingRedrawCall); + } + function map(selection) { _selection = selection; @@ -323,7 +342,7 @@ export function rendererMap(context) { surface.interrupt(); uiFlash().text(t('cannot_zoom')); setZoom(context.minEditableZoom(), true); - queueRedraw(); + scheduleRedraw(); dispatch.call('move', this, map); return; } @@ -346,7 +365,7 @@ export function rendererMap(context) { transformed = true; transformLast = eventTransform; utilSetTransform(supersurface, tX, tY, scale); - queueRedraw(); + scheduleRedraw(); dispatch.call('move', this, map); } @@ -403,11 +422,9 @@ export function rendererMap(context) { } - var queueRedraw = _.throttle(redraw, 750); - var immediateRedraw = function(difference, extent) { - if (!difference && !extent) queueRedraw.cancel(); + if (!difference && !extent) cancelPendingRedraw(); redraw(difference, extent); }; @@ -576,7 +593,7 @@ export function rendererMap(context) { mouse = utilFastMouse(supersurface.node()); setCenter(center); - queueRedraw(); + scheduleRedraw(); return map; }; @@ -605,7 +622,7 @@ export function rendererMap(context) { dispatch.call('move', this, map); } - queueRedraw(); + scheduleRedraw(); return map; }; @@ -625,7 +642,7 @@ export function rendererMap(context) { dispatch.call('move', this, map); } - queueRedraw(); + scheduleRedraw(); return map; }; @@ -648,7 +665,7 @@ export function rendererMap(context) { dispatch.call('move', this, map); } - queueRedraw(); + scheduleRedraw(); return map; }; diff --git a/modules/services/osm.js b/modules/services/osm.js index a395f85cd..498e5523f 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -11,7 +11,7 @@ import { osmWay } from '../osm'; -import { utilRebind } from '../util'; +import { utilRebind, utilIdleWorker } from '../util'; var dispatch = d3.dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded'), @@ -19,6 +19,7 @@ var dispatch = d3.dispatch('authLoading', 'authDone', 'change', 'loading', 'load blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*'], inflight = {}, loadedTiles = {}, + entityCache = {}, tileZoom = 16, oauth = osmAuth({ url: urlroot, @@ -100,10 +101,10 @@ function getVisible(attrs) { var parsers = { - node: function nodeData(obj) { + node: function nodeData(obj, uid) { var attrs = obj.attributes; return new osmNode({ - id: osmEntity.id.fromOSM('node', attrs.id.value), + id:uid, visible: getVisible(attrs), version: attrs.version.value, changeset: attrs.changeset && attrs.changeset.value, @@ -115,10 +116,10 @@ var parsers = { }); }, - way: function wayData(obj) { + way: function wayData(obj, uid) { var attrs = obj.attributes; return new osmWay({ - id: osmEntity.id.fromOSM('way', attrs.id.value), + id: uid, visible: getVisible(attrs), version: attrs.version.value, changeset: attrs.changeset && attrs.changeset.value, @@ -130,10 +131,10 @@ var parsers = { }); }, - relation: function relationData(obj) { + relation: function relationData(obj, uid) { var attrs = obj.attributes; return new osmRelation({ - id: osmEntity.id.fromOSM('relation', attrs.id.value), + id: uid, visible: getVisible(attrs), version: attrs.version.value, changeset: attrs.changeset && attrs.changeset.value, @@ -147,22 +148,25 @@ var parsers = { }; -function parse(xml) { +function parse(xml, callback, options) { + options = _.extend({ cache: true }, options); if (!xml || !xml.childNodes) return; var root = xml.childNodes[0], - children = root.childNodes, - entities = []; + children = root.childNodes; - for (var i = 0, l = children.length; i < l; i++) { - var child = children[i], - parser = parsers[child.nodeName]; + function parseChild(child) { + var parser = parsers[child.nodeName]; if (parser) { - entities.push(parser(child)); + var uid = osmEntity.id.fromOSM(child.nodeName, child.attributes.id.value); + if (options.cache && entityCache[uid]) { + return null; + } + return parser(child, uid); } } - return entities; + utilIdleWorker(children, parseChild, callback); } @@ -178,6 +182,7 @@ export default { userDetails = undefined; rateLimitError = undefined; _.forEach(inflight, abortRequest); + entityCache = {}; loadedTiles = {}; inflight = {}; return this; @@ -213,7 +218,8 @@ export default { }, - loadFromAPI: function(path, callback) { + loadFromAPI: function(path, callback, options) { + options = _.extend({ cache: true }, options); var that = this; function done(err, xml) { @@ -237,7 +243,15 @@ export default { } if (callback) { - callback(err, parse(xml)); + if (err) return callback(err, null); + parse(xml, function (entities) { + if (options.cache) { + for (var i in entities) { + entityCache[entities[i].id] = true; + } + } + callback(null, entities); + }, options); } } } @@ -253,42 +267,49 @@ export default { loadEntity: function(id, callback) { var type = osmEntity.id.type(id), - osmID = osmEntity.id.toOSM(id); + osmID = osmEntity.id.toOSM(id), + options = { cache: false }; this.loadFromAPI( '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : ''), function(err, entities) { if (callback) callback(err, { data: entities }); - } + }, + options ); }, loadEntityVersion: function(id, version, callback) { var type = osmEntity.id.type(id), - osmID = osmEntity.id.toOSM(id); + osmID = osmEntity.id.toOSM(id), + options = { cache: false }; this.loadFromAPI( '/api/0.6/' + type + '/' + osmID + '/' + version, function(err, entities) { if (callback) callback(err, { data: entities }); - } + }, + options ); }, loadMultiple: function(ids, callback) { var that = this; + _.each(_.groupBy(_.uniq(ids), osmEntity.id.type), function(v, k) { var type = k + 's', - osmIDs = _.map(v, osmEntity.id.toOSM); + osmIDs = _.map(v, osmEntity.id.toOSM), + options = { cache: false }; _.each(_.chunk(osmIDs, 150), function(arr) { that.loadFromAPI( '/api/0.6/' + type + '?' + type + '=' + arr.join(), function(err, entities) { if (callback) callback(err, { data: entities }); - } + }, + options ); }); }); diff --git a/modules/svg/labels.js b/modules/svg/labels.js index 90a3ea7f2..7b5fdb8e0 100644 --- a/modules/svg/labels.js +++ b/modules/svg/labels.js @@ -18,7 +18,8 @@ import { utilDetect } from '../util/detect'; import { utilDisplayName, utilDisplayNameForPath, - utilEntitySelector + utilEntitySelector, + utilCallWhenIdle } from '../util/index'; @@ -652,7 +653,7 @@ export function svgLabels(projection, context) { } - var throttleFilterLabels = _.throttle(filterLabels, 100); + var throttleFilterLabels = _.throttle(utilCallWhenIdle(filterLabels), 100); drawLabels.observe = function(selection) { diff --git a/modules/ui/background.js b/modules/ui/background.js index a9686fc16..419931fe0 100644 --- a/modules/ui/background.js +++ b/modules/ui/background.js @@ -4,7 +4,7 @@ import { d3keybinding } from '../lib/d3.keybinding.js'; import { t, textDirection } from '../util/locale'; import { geoMetersToOffset, geoOffsetToMeters } from '../geo/index'; import { utilDetect } from '../util/detect'; -import { utilSetTransform } from '../util/index'; +import { utilSetTransform, utilCallWhenIdle } from '../util/index'; import { svgIcon } from '../svg/index'; import { uiMapInMap } from './map_in_map'; import { uiCmd } from './cmd'; @@ -540,7 +540,7 @@ export function uiBackground(context) { ); context.map() - .on('move.background-update', _.debounce(update, 1000)); + .on('move.background-update', _.debounce(utilCallWhenIdle(update), 1000)); context.background() .on('change.background-update', update); diff --git a/modules/util/call_when_idle.js b/modules/util/call_when_idle.js new file mode 100644 index 000000000..1b8ae482a --- /dev/null +++ b/modules/util/call_when_idle.js @@ -0,0 +1,11 @@ +// note the function should be of low priority +// and should not be returning a value. +export function utilCallWhenIdle(func, timeout) { + return function() { + var args = arguments; + var that = this; + window.requestIdleCallback(function() { + func.apply(that, args); + }, {timeout: timeout}); + }; +} diff --git a/modules/util/idle_worker.js b/modules/util/idle_worker.js new file mode 100644 index 000000000..2a5cc5cfb --- /dev/null +++ b/modules/util/idle_worker.js @@ -0,0 +1,46 @@ +export function utilIdleWorker(tasks, processor, callback) { + var processed = []; + var currentPos = 0; + var totalTasks = tasks.length; + + function worker(deadline) { + while (deadline.timeRemaining() > 0 && currentPos < totalTasks) { + var result = processor(tasks[currentPos]); + + // if falsy dont add to the processed list + if (result) processed.push(result); + currentPos++; + } + + // more tasks are left, we might need more idleCallbacks + if (currentPos < totalTasks) { + return window.requestIdleCallback(function(deadline) {worker(deadline);}); + } + + // tasks are completed + return callback(processed); + } + + window.requestIdleCallback(function(deadline) {worker(deadline);}); +} + +// shim +window.requestIdleCallback = + window.requestIdleCallback || + function(cb) { + var start = Date.now(); + return setTimeout(function() { + cb({ + didTimeout: false, + timeRemaining: function() { + return Math.max(0, 50 - (Date.now() - start)); + } + }); + }, 1); + }; + +window.cancelIdleCallback = + window.cancelIdleCallback || + function(id) { + clearTimeout(id); + }; diff --git a/modules/util/index.js b/modules/util/index.js index fc0c71673..f3bf74842 100644 --- a/modules/util/index.js +++ b/modules/util/index.js @@ -22,3 +22,5 @@ export { utilSuggestNames } from './suggest_names'; export { utilTagText } from './util'; export { utilTriggerEvent } from './trigger_event'; export { utilWrap } from './util'; +export { utilIdleWorker} from './idle_worker'; +export { utilCallWhenIdle } from './call_when_idle'; diff --git a/test/spec/services/osm.js b/test/spec/services/osm.js index f23c1d635..ec7bd3878 100644 --- a/test/spec/services/osm.js +++ b/test/spec/services/osm.js @@ -320,6 +320,24 @@ describe('iD.serviceOsm', function () { [200, { 'Content-Type': 'text/xml' }, wayXML]); server.respond(); }); + + it('does not ignore repeat requests', function(done) { + var id = 'n1'; + connection.loadEntity(id, function(err1, result1) { + var entity1 = _.find(result1.data, function(e1) { return e1.id === id; }); + expect(entity1).to.be.an.instanceOf(iD.Node); + connection.loadEntity(id, function(err2, result2) { + var entity2 = _.find(result2.data, function(e2) { return e2.id === id; }); + expect(entity2).to.be.an.instanceOf(iD.Node); + done(); + }); + server.respond(); + }); + + server.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/node/1', + [200, { 'Content-Type': 'text/xml' }, nodeXML]); + server.respond(); + }); }); describe('#loadEntityVersion', function () { @@ -363,6 +381,24 @@ describe('iD.serviceOsm', function () { [200, { 'Content-Type': 'text/xml' }, wayXML]); server.respond(); }); + + it('does not ignore repeat requests', function(done) { + var id = 'n1'; + connection.loadEntityVersion(id, 1, function(err1, result1) { + var entity1 = _.find(result1.data, function(e1) { return e1.id === id; }); + expect(entity1).to.be.an.instanceOf(iD.Node); + connection.loadEntityVersion(id, 1, function(err2, result2) { + var entity2 = _.find(result2.data, function(e2) { return e2.id === id; }); + expect(entity2).to.be.an.instanceOf(iD.Node); + done(); + }); + server.respond(); + }); + + server.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/node/1/1', + [200, { 'Content-Type': 'text/xml' }, nodeXML]); + server.respond(); + }); }); describe('#loadMultiple', function () { @@ -376,6 +412,7 @@ describe('iD.serviceOsm', function () { it('loads nodes'); it('loads ways'); + it('does not ignore repeat requests'); });