diff --git a/examples/offline-storage.html b/examples/offline-storage.html new file mode 100644 index 0000000000..3cf235fdd4 --- /dev/null +++ b/examples/offline-storage.html @@ -0,0 +1,43 @@ + + + + + + + OpenLayers Offline Storage Example + + + + + + + +

Offline Storage Example

+ +
+ local storage, persistence, cache, html5 +
+ +
Caching viewed tiles
+ +
+
Cache status:
+
Read from cache [try cache first] [try online first1]
+
Write to cache
+
+
+

1 Disconnect your device from the network to test - only works for same origin layers.

+
+
+

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 offline-storage.js + for the source code.

+
+ + diff --git a/examples/offline-storage.js b/examples/offline-storage.js new file mode 100644 index 0000000000..adf27d0aeb --- /dev/null +++ b/examples/offline-storage.js @@ -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); + } +} \ No newline at end of file diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js index 0dbf929f16..9c23c2b7d0 100644 --- a/lib/OpenLayers.js +++ b/lib/OpenLayers.js @@ -209,6 +209,8 @@ "OpenLayers/Control.js", "OpenLayers/Control/Attribution.js", "OpenLayers/Control/Button.js", + "OpenLayers/Control/CacheRead.js", + "OpenLayers/Control/CacheWrite.js", "OpenLayers/Control/ZoomBox.js", "OpenLayers/Control/ZoomToMaxExtent.js", "OpenLayers/Control/DragPan.js", diff --git a/lib/OpenLayers/Control/CacheRead.js b/lib/OpenLayers/Control/CacheRead.js new file mode 100644 index 0000000000..554a6aee3c --- /dev/null +++ b/lib/OpenLayers/Control/CacheRead.js @@ -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 + * from the browser's local storage. + * + * Inherits from: + * - + */ +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 in + * . + */ + fetchEvent: "tileloadstart", + + /** + * APIProperty: layers + * {Array()}. 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 - {} + */ + 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 + * 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 + * instance + */ + removeLayer: function(evt) { + evt.layer.events.unregister(this.fetchEvent, this, this.fetch); + }, + + /** + * Method: fetch + * Listener to the 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" +}); diff --git a/lib/OpenLayers/Control/CacheWrite.js b/lib/OpenLayers/Control/CacheWrite.js new file mode 100644 index 0000000000..e6ca6f62b6 --- /dev/null +++ b/lib/OpenLayers/Control/CacheWrite.js @@ -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 + * control is used to fetch and use the cached + * tile images. + * + * Inherits from: + * - + */ +OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { + + /** + * APIProperty: events + * {} Events instance for listeners and triggering + * control specific events. + * + * To register events in the constructor, configure . + * + * 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 ): + * 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()}. 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 + * 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 - {} + */ + 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 + * 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 + * 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 + * 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 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); + } + } +}; diff --git a/tests/Control/CacheRead.html b/tests/Control/CacheRead.html new file mode 100644 index 0000000000..4755508c26 --- /dev/null +++ b/tests/Control/CacheRead.html @@ -0,0 +1,98 @@ + + + + + + + +
+ + diff --git a/tests/Control/CacheWrite.html b/tests/Control/CacheWrite.html new file mode 100644 index 0000000000..fdb6cabeaf --- /dev/null +++ b/tests/Control/CacheWrite.html @@ -0,0 +1,87 @@ + + + + + + +
+ + diff --git a/tests/list-tests.html b/tests/list-tests.html index 7c4cb1d402..8dcba00385 100644 --- a/tests/list-tests.html +++ b/tests/list-tests.html @@ -13,6 +13,8 @@
  • Control/Attribution.html
  • Control/ArgParser.html
  • Control/Button.html
  • +
  • Control/CacheRead.html
  • +
  • Control/CacheWrite.html
  • Control/DragFeature.html
  • Control/DragPan.html
  • Control/DrawFeature.html