Using service workers it is possible to cache vector tiles to eliminate repetitive requests to the tile server while also ensuring usability of maps in an environment with little or no connectivity.
Prerequisites
Services
Web APIs
- Service Worker - Working Draft / Browser Support
- Fetch - Living Standard / Browser Support
- Geolocation - Recommendation / Browser Support
Libraries
Service Worker
Registration
navigator.serviceWorker.register('sw-tile.js');
sw-tile.js
var tileDomain = 'vector.mapzen.com';
function unableToResolve() {
return new Response('', {status: 503, statusText: 'service unavailable'});
}
self.addEventListener('fetch', function(event) {
var request = event.request;
// if the cached response does not exist perform a fetch for that resource
// and return the response, otherwise return response from cache
var queriedCache = function(cached) {
var response = cached || fetch(request)
.then(fetchedFromNetwork, unableToResolve)
.catch(unableToResolve);
return response;
};
var fetchedFromNetwork = function(response) {
// cache response if request was successful
if (response.status === 200) {
caches.open(tileDomain).then(function(cache) {
// store response in cache keyed by original request
cache.put(event.request, response);
});
}
// cache.put consumes response body
return response.clone();
};
// only cache requests from tile server
if (request.url.indexOf(tileDomain) === -1) {
event.respondWith(fetch(request));
} else {
event.respondWith(caches.match(request).then(queriedCache));
}
});
Cache Vector Tiles Based on Geolocation
When a position is determined from the Geolocation API then more requests will be made for additional vector tiles from the tile service in order to build out the cache for deeper zoom levels near the current position. Precaching these tiles should allow for more fluid zoom behavior in environments with limited connectivity.
Sending a new XMLHttpRequest to the tile server will trigger the service worker to cache new vector tiles.
var tileSrc = 'https://vector.mapzen.com/osm/all/{z}/{x}/{y}.topojson?api_key={api_key}';
var zoomRangeCache = [10, 11, 12, 13, 14, 15];
var defaultZoom = 12;
var defaultCenter = [-98.5795, 39.828175];
var cacheRadius = 10000;
var tilesCached = false;
var map = new TileMap({
center: defaultCenter,
layers: ['water', 'landuse', 'roads', 'buildings'],
selector: '.map',
url: tileSrc,
zoom: defaultZoom
});
// make requests for tiles given a set of coordinates and zoom ranges
// these requests will trigger the service worker to cache responses
function requestTiles(src, imageCoordinates, zoomRange) {
zoomRange.forEach(function(zoom) {
imageCoordinates.forEach(function(coordinate) {
var request = new XMLHttpRequest();
var tileSrc = src;
tileSrc = tileSrc.replace('{z}', zoom);
tileSrc = tileSrc.replace('{x}', coordinate[0]);
tileSrc = tileSrc.replace('{y}', coordinate[1]);
request.open('GET', tileSrc);
request.send();
});
});
}
navigator.geolocation.watchPosition(function(position) {
// cache nearby vector tiles if accuracy is within 10km
if (!tilesCached && position.coords.accuracy <= cacheRadius) {
requestTiles(tileSrc, map.image.data(), zoomRangeCache);
tilesCached = true;
}
});
Test Results
Requests without a service worker registered.
Requests with service worker responding with cached responses.
Further Improvement
The cache control strategy utilized in this demo could be improved to request updated tiles from the server when cached responses exceed a maximum age. Currently the service worker will not update the cached response after the initial request.
The initial reference implementation renders the map as an svg
element with tens of thousands of path
elements which causes janky rendering, panning/zooming on mobile. A better solution would make use of canvas
to render paths from D3. One drawback to this approach is losing the ability to directly style the map using CSS.
Further Reading
- MDN - Using Service Workers
- ponyfoo.com - ServiceWorker: Revolution of the Web Platform
Source: https://github.com/csbrandt/cache-map
Demo: https://csbrandt.github.io/cache-map/