Adding CacheRead and CacheWrite controls.

These controls read from and write to the browser's offline storage. Example with a seeding tool included.
This commit is contained in:
ahocevar
2012-03-08 14:18:44 +01:00
parent 0566b0a5bb
commit f18ac9911b
8 changed files with 761 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>OpenLayers Offline Storage Example</title>
<link rel="stylesheet" href="../theme/default/style.css" type="text/css">
<link rel="stylesheet" href="style.css" type="text/css">
<style type="text/css">
.olControlAttribution {
bottom: 0;
}
</style>
<script src="../lib/OpenLayers.js"></script>
<script src="offline-storage.js"></script>
</head>
<body onload="init()">
<h1 id="title">Offline Storage Example</h1>
<div id="tags">
local storage, persistence, cache, html5
</div>
<div id="shortdesc">Caching viewed tiles</div>
<div id="map" class="smallmap"></div>
<div>Cache status: <span id="hits"></span> <span id="status"></span></div>
<div><input id="read" type="checkbox">Read from cache [<input id="tileloadstart" name="type" type="radio">try cache first] [<input id="tileerror" name="type" type="radio">try online first<sup>1</sup>]</div>
<div><input id="write" type="checkbox">Write to cache</div>
<div><button id="clear">Clear cached tiles</button><button id="seed">Seed current extent</button>
<br>
<p><sup>1</sup> <small>Disconnect your device from the network to test - only works for same origin layers.</small></p>
<br>
<div id="docs">
<p>This example shows how to use the CacheWrite control to cache tiles
that are being viewed in the browser's local storage, and how to use
the CacheRead control to use cached tiles when offline or on a slow
connection. See <a href="offline-storage.js">offline-storage.js</a>
for the source code.</p>
</div>
</body>
</html>

183
examples/offline-storage.js Normal file
View File

@@ -0,0 +1,183 @@
var map, cacheWrite, cacheRead1, cacheRead2;
function init(){
map = new OpenLayers.Map({
div: "map",
projection: "EPSG:900913",
layers: [new OpenLayers.Layer.OSM("OpenStreetMap (CORS)", null, {
eventListeners: {
loadend: updateLayerInfo,
tileloaded: updateTileInfo,
tileerror: updateTileInfo
}
}),
new OpenLayers.Layer.WMS("OSGeo (same origin - proxied)", "http://vmap0.tiles.osgeo.org/wms/vmap0", {
layers: "basic"
}, {
eventListeners: {
tileloadstart: function(evt) {
// send requests through proxy
evt.tile.url = "proxy.cgi?url=" + encodeURIComponent(evt.tile.url);
},
loadend: updateLayerInfo,
tileloaded: updateTileInfo
}
})
],
center: [0,0],
zoom: 1
});
cacheWrite = new OpenLayers.Control.CacheWrite({
imageFormat: "image/jpeg",
eventListeners: {
cachefull: function() {
status.innerHTML = "Cache full.";
cacheFull = true;
}
}
});
// try cache before loading from remote resource
cacheRead1 = new OpenLayers.Control.CacheRead({
eventListeners: {
activate: function() {
cacheRead2.deactivate();
}
}
});
// try loading from remote resource and fall back to cache
cacheRead2 = new OpenLayers.Control.CacheRead({
autoActivate: false,
fetchEvent: "tileerror",
eventListeners: {
activate: function() {
cacheRead1.deactivate();
}
}
});
var layerSwitcher = new OpenLayers.Control.LayerSwitcher();
map.addControls([cacheWrite, cacheRead1, cacheRead2, layerSwitcher]);
layerSwitcher.maximizeControl();
// add UI and behavior
var status = document.getElementById("status"),
hits = document.getElementById("hits"),
previousCount = -1,
cacheHits = 0,
cacheFull = false;
updateLayerInfo();
var read = document.getElementById("read");
read.checked = true;
read.onclick = toggleRead;
var write = document.getElementById("write");
write.checked = false;
write.onclick = toggleWrite;
document.getElementById("clear").onclick = clearCache;
var tileloadstart = document.getElementById("tileloadstart");
tileloadstart.checked = "checked";
tileloadstart.onclick = setType;
document.getElementById("tileerror").onclick = setType;
document.getElementById("seed").onclick = seedCache;
// update the number of cached tiles and detect local storage support
function updateLayerInfo(evt) {
if (window.localStorage) {
if (previousCount !== localStorage.length) {
status.innerHTML = localStorage.length + " entries in cache.";
}
previousCount = localStorage.length;
} else {
status.innerHTML = "Local storage not supported. Try a different browser.";
}
}
// update the number of cache hits and detect missing CORS support
function updateTileInfo(evt) {
if (cacheWrite.active) {
try {
var canvasContext = evt.tile.getCanvasContext();
if (canvasContext) {
// will throw an exception if CORS image requests are not supported
canvasContext.canvas.toDataURL();
} else {
status.innerHTML = "Canvas not supported. Try a different browser.";
}
} catch(e) {
status.innerHTML = "CORS image requests not supported. Try a different layer.";
}
}
if (evt.tile.url.substr(0, 5) === "data:") {
cacheHits++;
}
hits.innerHTML = cacheHits + " cache hits.";
}
// turn the cacheRead controls on and off
function toggleRead() {
if (!this.checked) {
cacheRead1.deactivate();
cacheRead2.deactivate();
} else {
setType();
}
}
// turn the cacheWrite control on and off
function toggleWrite() {
cacheWrite[cacheWrite.active ? "deactivate" : "activate"]();
}
// clear all tiles from the cache
function clearCache() {
OpenLayers.Control.CacheWrite.clearCache();
cacheFull = false;
updateLayerInfo();
}
// activate the cacheRead control that matches the desired fetch strategy
function setType() {
if (tileloadstart.checked) {
cacheRead1.activate();
} else {
cacheRead2.activate();
}
}
// seed the cache
function seedCache() {
var zoom = map.getZoom();
var extent = map.getExtent();
var center = map.getCenter();
var active = cacheWrite.active;
var tileWidth = map.baseLayer.tileSize.w;
var layer = map.baseLayer;
var buffer = layer.buffer;
// make sure the next setCenter triggers a load
map.zoomTo(zoom === layer.numZoomLevels-1 ? zoom - 1 : zoom + 1);
// turn on cache writing
cacheWrite.activate();
// turn off cache reading
cacheRead1.deactivate();
cacheRead2.deactivate();
layer.events.register("loadend", this, function next() {
var nextZoom = map.getZoom() + 1;
var extentWidth = extent.getWidth() / map.getResolutionForZoom(nextZoom);
// adjust the layer's buffer size so we don't have to pan
layer.buffer = Math.ceil((extentWidth / tileWidth - map.getSize().w / tileWidth) / 2);
map.zoomIn();
if (cacheFull || nextZoom === layer.numZoomLevels-1) {
// we're done - restore previous settings
layer.events.unregister("loadend", this, next);
layer.buffer = buffer;
map.setCenter(center, zoom);
if (!active) {
cacheWrite.deactivate();
}
if (read.checked) {
setType();
}
}
});
// start seeding
map.setCenter(center, zoom);
}
}

View File

@@ -209,6 +209,8 @@
"OpenLayers/Control.js", "OpenLayers/Control.js",
"OpenLayers/Control/Attribution.js", "OpenLayers/Control/Attribution.js",
"OpenLayers/Control/Button.js", "OpenLayers/Control/Button.js",
"OpenLayers/Control/CacheRead.js",
"OpenLayers/Control/CacheWrite.js",
"OpenLayers/Control/ZoomBox.js", "OpenLayers/Control/ZoomBox.js",
"OpenLayers/Control/ZoomToMaxExtent.js", "OpenLayers/Control/ZoomToMaxExtent.js",
"OpenLayers/Control/DragPan.js", "OpenLayers/Control/DragPan.js",

View File

@@ -0,0 +1,149 @@
/* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for
* full list of contributors). Published under the Clear BSD license.
* See http://svn.openlayers.org/trunk/openlayers/license.txt for the
* full text of the license. */
/**
* @requires OpenLayers/Control.js
*/
/**
* Class: OpenLayers.Control.CacheRead
* A control for using image tiles cached with <OpenLayers.Control.CacheWrite>
* from the browser's local storage.
*
* Inherits from:
* - <OpenLayers.Control>
*/
OpenLayers.Control.CacheRead = OpenLayers.Class(OpenLayers.Control, {
/**
* APIProperty: fetchEvent
* {String} The layer event to listen to for replacing remote resource tile
* URLs with cached data URIs. Supported values are "tileerror" (try
* remote first, fall back to cached) and "tileloadstart" (try cache
* first, fall back to remote). Default is "tileloadstart".
*
* Note that "tileerror" will not work for CORS enabled images (see
* https://developer.mozilla.org/en/CORS_Enabled_Image), i.e. layers
* configured with a <OpenLayers.Tile.Image.crossOriginKeyword> in
* <OpenLayers.Layer.Grid.tileOptions>.
*/
fetchEvent: "tileloadstart",
/**
* APIProperty: layers
* {Array(<OpenLayers.Layer.HTTPRequest>)}. Optional. If provided, only
* these layers will receive tiles from the cache.
*/
layers: null,
/**
* APIProperty: autoActivate
* {Boolean} Activate the control when it is added to a map. Default is
* true.
*/
autoActivate: true,
/**
* Constructor: OpenLayers.Control.CacheRead
*
* Parameters:
* options - {Object} Object with API properties for this control
*/
/**
* Method: setMap
* Set the map property for the control.
*
* Parameters:
* map - {<OpenLayers.Map>}
*/
setMap: function(map) {
OpenLayers.Control.prototype.setMap.apply(this, arguments);
var i, layers = this.layers || this.map.layers;
for (i=layers.length-1; i>=0; --i) {
this.addLayer({layer: layers[i]});
}
if (!this.layers) {
this.map.events.on({
addlayer: this.addLayer,
removeLayer: this.removeLayer,
scope: this
});
}
},
/**
* Method: addLayer
* Adds a layer to the control. Once added, tiles requested for this layer
* will be cached.
*
* Parameters:
* evt - {Object} Object with a layer property referencing an
* <OpenLayers.Layer> instance
*/
addLayer: function(evt) {
evt.layer.events.register(this.fetchEvent, this, this.fetch);
},
/**
* Method: removeLayer
* Removes a layer from the control. Once removed, tiles requested for this
* layer will no longer be cached.
*
* Parameters:
* evt - {Object} Object with a layer property referencing an
* <OpenLayers.Layer> instance
*/
removeLayer: function(evt) {
evt.layer.events.unregister(this.fetchEvent, this, this.fetch);
},
/**
* Method: fetch
* Listener to the <fetchEvent> event. Replaces a tile's url with a data
* URI from the cache.
*
* Parameters:
* evt - {Object} Event object with a tile property.
*/
fetch: function(evt) {
if (this.active && window.localStorage) {
var tile = evt.tile,
dataURI = window.localStorage.getItem("olCache_" + tile.url);
if (dataURI) {
if (evt.type === "tileerror") {
tile.setImgSrc(dataURI);
} else {
tile.url = dataURI;
}
}
}
},
/**
* Method: destroy
* The destroy method is used to perform any clean up before the control
* is dereferenced. Typically this is where event listeners are removed
* to prevent memory leaks.
*/
destroy: function() {
if (this.layers || this.map) {
var i, layers = this.layers || this.map.layers;
for (i=layers.length-1; i>=0; --i) {
this.removeLayer({layer: layers[i]});
}
}
if (this.map) {
this.map.events.un({
addlayer: this.addLayer,
removeLayer: this.removeLayer,
scope: this
});
}
OpenLayers.Control.prototype.destroy.apply(this, arguments);
},
CLASS_NAME: "OpenLayers.Control.CacheRead"
});

View File

@@ -0,0 +1,197 @@
/* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for
* full list of contributors). Published under the Clear BSD license.
* See http://svn.openlayers.org/trunk/openlayers/license.txt for the
* full text of the license. */
/**
* @requires OpenLayers/Control.js
*/
/**
* Class: OpenLayers.Control.CacheWrite
* A control for caching image tiles in the browser's local storage. The
* <OpenLayers.Control.CacheRead> control is used to fetch and use the cached
* tile images.
*
* Inherits from:
* - <OpenLayers.Control>
*/
OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, {
/**
* APIProperty: events
* {<OpenLayers.Events>} Events instance for listeners and triggering
* control specific events.
*
* To register events in the constructor, configure <eventListeners>.
*
* Register a listener for a particular event with the following syntax:
* (code)
* control.events.register(type, obj, listener);
* (end)
*
* Supported event types (in addition to those from <OpenLayers.Control.events>):
* cachefull - Triggered when the cache is full. Listeners receive an
* object with a tile property as first argument. The tile references
* the tile that couldn't be cached.
*/
/**
* APIProperty: eventListeners
* {Object} Object with event listeners, keyed by event name. An optional
* scope property defines the scope that listeners will be executed in.
*/
/**
* APIProperty: layers
* {Array(<OpenLayers.Layer.Grid>)}. Optional. If provided, caching
* will be enabled for these layers only, otherwise for all cacheable
* layers.
*/
layers: null,
/**
* APIProperty: imageFormat
* {String} The image format used for caching. The default is "image/png".
* Supported formats depend on the user agent. If an unsupported
* <imageFormat> is provided, "image/png" will be used. For aerial
* imagery, "image/jpeg" is recommended.
*/
imageFormat: "image/png",
/**
* Property: quotaRegEx
* {RegExp}
*/
quotaRegEx: (/quota/i),
/**
* Constructor: OpenLayers.Control.CacheWrite
*
* Parameters:
* options - {Object} Object with API properties for this control.
*/
/**
* Method: setMap
* Set the map property for the control.
*
* Parameters:
* map - {<OpenLayers.Map>}
*/
setMap: function(map) {
OpenLayers.Control.prototype.setMap.apply(this, arguments);
var i, layers = this.layers || this.map.layers;
for (i=layers.length-1; i>=0; --i) {
this.addLayer({layer: layers[i]});
}
if (!this.layers) {
this.map.events.on({
addlayer: this.addLayer,
removeLayer: this.removeLayer,
scope: this
});
}
},
/**
* Method: addLayer
* Adds a layer to the control. Once added, tiles requested for this layer
* will be cached.
*
* Parameters:
* evt - {Object} Object with a layer property referencing an
* <OpenLayers.Layer> instance
*/
addLayer: function(evt) {
evt.layer.events.register("tileloaded", this, this.cache);
},
/**
* Method: removeLayer
* Removes a layer from the control. Once removed, tiles requested for this
* layer will no longer be cached.
*
* Parameters:
* evt - {Object} Object with a layer property referencing an
* <OpenLayers.Layer> instance
*/
removeLayer: function(evt) {
evt.layer.events.unregister("tileloaded", this, this.cache);
},
/**
* Method: cache
* Adds a tile to the cache. When the cache is full, the "cachefull" event
* is triggered.
*
* Parameters:
* obj - {Object} Object with a tile property, tile being the
* <OpenLayers.Tile.Image> with the data to add to the cache
*/
cache: function(obj) {
if (this.active && window.localStorage) {
var tile = obj.tile;
if (tile.url.substr(0, 5) !== 'data:') {
try {
var canvasContext = tile.getCanvasContext();
if (canvasContext) {
window.localStorage.setItem(
"olCache_" + tile.url,
canvasContext.canvas.toDataURL(this.imageFormat)
);
}
} catch(e) {
// local storage full or CORS violation
var reason = e.name || e.message;
if (reason && this.quotaRegEx.test(reason)) {
this.events.triggerEvent("cachefull", {tile: tile});
} else {
// throw exception in the next cycle
window.setTimeout(function() { throw(e); }, 0);
}
}
}
}
},
/**
* Method: destroy
* The destroy method is used to perform any clean up before the control
* is dereferenced. Typically this is where event listeners are removed
* to prevent memory leaks.
*/
destroy: function() {
if (this.layers || this.map) {
var i, layers = this.layers || this.map.layers;
for (i=layers.length-1; i>=0; --i) {
this.removeLayer({layer: layers[i]});
}
}
if (this.map) {
this.map.events.un({
addlayer: this.addLayer,
removeLayer: this.removeLayer,
scope: this
});
}
OpenLayers.Control.prototype.destroy.apply(this, arguments);
},
CLASS_NAME: "OpenLayers.Control.CacheWrite"
});
/**
* APIFunction: OpenLayers.Control.CacheWrite.clearCache
* Clears all tiles cached with <OpenLayers.Control.CacheWrite> from the cache.
*/
OpenLayers.Control.CacheWrite.clearCache = function() {
if (!window.localStorage) { return; }
var i, key;
for (i=window.localStorage.length-1; i>=0; --i) {
key = window.localStorage.key(i);
if (key.substr(0, 8) === "olCache_") {
window.localStorage.removeItem(key);
}
}
};

View File

@@ -0,0 +1,98 @@
<html>
<head>
<script>
/**
* Because browsers that implement requestAnimationFrame may not execute
* animation functions while a window is not displayed (e.g. in a hidden
* iframe as in these tests), we mask the native implementations here. The
* native requestAnimationFrame functionality is tested in Util.html and
* in PanZoom.html (where a popup is opened before panning). The tests
* here will test the fallback setTimeout implementation for animation.
*/
window.requestAnimationFrame =
window.webkitRequestAnimationFrame =
window.mozRequestAnimationFrame =
window.oRequestAnimationFrame =
window.msRequestAnimationFrame = null;
</script>
<script src="../OLLoader.js"></script>
<script type="text/javascript">
function test_addLayer_removeLayer(t) {
t.plan(6);
var control = new OpenLayers.Control.CacheRead();
var map = new OpenLayers.Map({
div: "map",
controls: [control],
layers: [
new OpenLayers.Layer.WMS("One"),
new OpenLayers.Layer.WMS("Two")
]
});
t.ok(map.layers[0].events.listeners.tileloadstart, "tileloadstart listener registered on layer One");
t.ok(map.layers[1].events.listeners.tileloadstart, "tileloadstart listener registered on layer Two");
control.destroy();
t.ok(!map.layers[1].events.listeners.tileloadstart.length, "tileloadstart listener unregistered");
control = new OpenLayers.Control.CacheRead({
fetchEvent: "tileerror",
layers: [map.layers[0]]
});
map.addControl(control);
t.ok(map.layers[0].events.listeners.tileerror, "tileerror listener registered on layer One");
t.ok(!map.layers[1].events.listeners.tileerror, "tileerror listener not registered on layer Two");
control.destroy();
t.ok(!map.layers[0].events.listeners.tileerror.length, "tileerror listener unregistered");
map.destroy();
}
function test_fetch(t) {
if (!window.localStorage) {
t.plan(1);
var scope = {active: true};
t.eq(OpenLayers.Control.CacheRead.prototype.fetch.call(scope), undefined, "no tiles fetched when localStorage is not supported.");
return;
}
t.plan(4);
var data = "data:image/gif;base64,R0lGODlhAQABAIAAAP7//wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
window.localStorage.setItem("olCache_foo/1/1/1", data);
window.localStorage.setItem("olCache_bar/1/1/1", data);
var layer1 = new OpenLayers.Layer.XYZ("One", "foo/${x}/${y}/${z}");
var layer2 = new OpenLayers.Layer.XYZ("Two", "bar/${x}/${y}/${z}", {isBaseLayer: false});
var control1 = new OpenLayers.Control.CacheRead({
layers: [layer1]
});
var control2 = new OpenLayers.Control.CacheRead({
layers: [layer2],
fetchEvent: "tileerror"
});
var map = new OpenLayers.Map({
div: "map",
projection: "EPSG:900913",
controls: [control1, control2],
layers: [layer1, layer2],
zoom: 1,
center: [0, 0]
});
t.delay_call(1, function() {
t.eq(layer1.grid[1][1].imgDiv.src, data, "[tileloadstart] tile content from cache");
t.ok(layer1.grid[0][0].imgDiv.src !== data, "[tileloadstart] tile content from remote resource");
t.eq(layer2.grid[1][1].imgDiv.src, data, "[tileerror] tile content from cache");
t.ok(layer2.grid[0][0].imgDiv.src !== data, "[tileerror] tile content from remote resource");
window.localStorage.removeItem("olCache_foo/1/1/1");
window.localStorage.removeItem("olCache_bar/1/1/1");
map.destroy();
});
}
</script>
</head>
<body>
<div id="map" style="width: 400px; height: 250px;"/>
</body>
</html>

View File

@@ -0,0 +1,87 @@
<html>
<head>
<script src="../OLLoader.js"></script>
<script type="text/javascript">
function test_addLayer_removeLayer(t) {
t.plan(6);
var control = new OpenLayers.Control.CacheWrite();
var map = new OpenLayers.Map({
div: "map",
controls: [control],
layers: [
new OpenLayers.Layer.WMS("One"),
new OpenLayers.Layer.WMS("Two")
]
});
t.ok(map.layers[0].events.listeners.tileloaded, "tileloaded listener registered on layer One");
t.ok(map.layers[1].events.listeners.tileloaded, "tileloaded listener registered on layer Two");
control.destroy();
t.ok(!map.layers[1].events.listeners.tileloaded.length, "tileloaded listener unregistered");
control = new OpenLayers.Control.CacheWrite({
layers: [map.layers[0]]
});
map.addControl(control);
t.ok(map.layers[0].events.listeners.tileloaded.length, "tileloaded listener registered on layer One");
t.ok(!map.layers[1].events.listeners.tileloaded.length, "tileloaded listener not registered on layer Two");
control.destroy();
t.ok(!map.layers[0].events.listeners.tileloaded.length, "tileloaded listener unregistered");
map.destroy();
}
function test_cache_clearCache(t) {
if (!window.localStorage) {
t.plan(2);
var scope = {active: true};
t.eq(OpenLayers.Control.CacheWrite.prototype.cache.call(scope), undefined, "no tiles cached when localStorage is not supported.");
t.ok(!OpenLayers.Control.CacheWrite.clearCache(), "clearCache does nothing when localStorage is not supported.");
return;
}
t.plan(3);
OpenLayers.Control.CacheWrite.clearCache();
var length = window.localStorage.length;
var tiles = 0;
var layer = new OpenLayers.Layer.XYZ("One", "../../img/blank.gif?${x},${y},${z}", {
eventListeners: {
tileloaded: function() {
tiles++;
}
}
});
var control = new OpenLayers.Control.CacheWrite({autoActivate: true});
var map = new OpenLayers.Map({
div: "map",
projection: "EPSG:900913",
controls: [control],
layers: [layer],
zoom: 1,
center: [0, 0]
});
t.delay_call(1, function() {
var canvasContext = layer.grid[1][1].getCanvasContext();
t.eq(window.localStorage.length, length + (canvasContext ? tiles : 0), "cache filled with tiles");
var url = layer.grid[1][1].url;
// content will be null for browsers that have localStorage but no canvas support
var content = canvasContext ? canvasContext.canvas.toDataURL("image/png") : null;
t.eq(window.localStorage.getItem("olCache_"+url), content, "localStorage contains correct image data");
var key = Math.random();
window.localStorage.setItem(key, "bar");
OpenLayers.Control.CacheWrite.clearCache();
t.eq(window.localStorage.length, length + 1, "cache cleared, but foreign entries left in localStorage");
window.localStorage.removeItem(key);
map.destroy();
});
}
</script>
</head>
<body>
<div id="map" style="width: 400px; height: 250px;"/>
</body>
</html>

View File

@@ -13,6 +13,8 @@
<li>Control/Attribution.html</li> <li>Control/Attribution.html</li>
<li>Control/ArgParser.html</li> <li>Control/ArgParser.html</li>
<li>Control/Button.html</li> <li>Control/Button.html</li>
<li>Control/CacheRead.html</li>
<li>Control/CacheWrite.html</li>
<li>Control/DragFeature.html</li> <li>Control/DragFeature.html</li>
<li>Control/DragPan.html</li> <li>Control/DragPan.html</li>
<li>Control/DrawFeature.html</li> <li>Control/DrawFeature.html</li>