From f18ac9911b7a714b9669f772da3eae3635627686 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 8 Mar 2012 14:18:44 +0100 Subject: [PATCH 1/6] Adding CacheRead and CacheWrite controls. These controls read from and write to the browser's offline storage. Example with a seeding tool included. --- examples/offline-storage.html | 43 ++++++ examples/offline-storage.js | 183 +++++++++++++++++++++++++ lib/OpenLayers.js | 2 + lib/OpenLayers/Control/CacheRead.js | 149 ++++++++++++++++++++ lib/OpenLayers/Control/CacheWrite.js | 197 +++++++++++++++++++++++++++ tests/Control/CacheRead.html | 98 +++++++++++++ tests/Control/CacheWrite.html | 87 ++++++++++++ tests/list-tests.html | 2 + 8 files changed, 761 insertions(+) create mode 100644 examples/offline-storage.html create mode 100644 examples/offline-storage.js create mode 100644 lib/OpenLayers/Control/CacheRead.js create mode 100644 lib/OpenLayers/Control/CacheWrite.js create mode 100644 tests/Control/CacheRead.html create mode 100644 tests/Control/CacheWrite.html 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
  • From e366722375bc38164122d6ae3a4bc793aa998bff Mon Sep 17 00:00:00 2001 From: ahocevar Date: Thu, 8 Mar 2012 18:36:18 +0100 Subject: [PATCH 2/6] Note about terms of service. --- lib/OpenLayers/Control/CacheWrite.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/OpenLayers/Control/CacheWrite.js b/lib/OpenLayers/Control/CacheWrite.js index e6ca6f62b6..0311fde801 100644 --- a/lib/OpenLayers/Control/CacheWrite.js +++ b/lib/OpenLayers/Control/CacheWrite.js @@ -13,6 +13,9 @@ * control is used to fetch and use the cached * tile images. * + * Note: Before using this control on any layer that is not your own, make sure + * that the terms of service of the tile provider allow local storage of tiles. + * * Inherits from: * - */ From 51a11e053f32ef5022425bf1de267cb110d9cfb9 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 9 Mar 2012 15:32:04 +0100 Subject: [PATCH 3/6] Additional simple examples for just CacheRead and CacheWrite. * Abort seeding immediately when cache is full. * Abort seeding immediately when CORS image requests are not supported. --- examples/cache-read.html | 36 ++++++++++++++++ examples/cache-read.js | 40 +++++++++++++++++ examples/cache-write.html | 36 ++++++++++++++++ examples/cache-write.js | 48 +++++++++++++++++++++ examples/offline-storage.js | 86 ++++++++++++++++++++++++------------- 5 files changed, 215 insertions(+), 31 deletions(-) create mode 100644 examples/cache-read.html create mode 100644 examples/cache-read.js create mode 100644 examples/cache-write.html create mode 100644 examples/cache-write.js diff --git a/examples/cache-read.html b/examples/cache-read.html new file mode 100644 index 0000000000..895813c689 --- /dev/null +++ b/examples/cache-read.html @@ -0,0 +1,36 @@ + + + + + + + OpenLayers Cache Read Example + + + + + + +

    Cache Read Example

    + +
    + local storage, persistence, cache, html5 +
    + +
    Caching viewed tiles
    + +
    +
    +
    +
    +

    This example shows how to use the CacheRead control to fetch cached + tiles from the browser's Local Storage. As you pan and zoom the map, + you can see how the number of cache hits incrases as you browse regions + that are available in the cache.

    +

    To fill the cache with tiles, switch to the + cache-write.html example.

    +

    See cache-read.js for the source + code.

    +
    + + diff --git a/examples/cache-read.js b/examples/cache-read.js new file mode 100644 index 0000000000..f8c42a9115 --- /dev/null +++ b/examples/cache-read.js @@ -0,0 +1,40 @@ +var map, cacheRead; +function init(){ + map = new OpenLayers.Map({ + div: "map", + projection: "EPSG:900913", + layers: [ + new OpenLayers.Layer.WMS("OSGeo", "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); + }, + tileloaded: updateHits + } + }) + ], + center: [0,0], + zoom: 1 + }); + cacheRead = new OpenLayers.Control.CacheRead(); + map.addControl(cacheRead); + + + + // User interface + var status = document.getElementById("status"); + var hits = 0; + + // update the number of cached tiles and detect local storage support + function updateHits(evt) { + hits += evt.tile.url.substr(0, 5) === "data:"; + if (window.localStorage) { + status.innerHTML = hits + " cache hits."; + } else { + status.innerHTML = "Local storage not supported. Try a different browser."; + } + } +} \ No newline at end of file diff --git a/examples/cache-write.html b/examples/cache-write.html new file mode 100644 index 0000000000..e36f3de9b7 --- /dev/null +++ b/examples/cache-write.html @@ -0,0 +1,36 @@ + + + + + + + OpenLayers Cache Write Example + + + + + + +

    Cache Write Example

    + +
    + local storage, persistence, cache, html5 +
    + +
    Caching viewed tiles
    + +
    +
    Cache status:
    +
    +
    +
    +

    This example shows how to use the CacheWrite control to cache the + tiles. Caching is turned on, and as you pan and zoom the map, every + tile that is loaded is also copied to the browsers Local Storage.

    +

    To use the cached tiles, switch to the + cache-read.html example.

    +

    See cache-write.js for the source + code.

    +
    + + diff --git a/examples/cache-write.js b/examples/cache-write.js new file mode 100644 index 0000000000..b47cb60134 --- /dev/null +++ b/examples/cache-write.js @@ -0,0 +1,48 @@ +var map, cacheWrite; +function init(){ + map = new OpenLayers.Map({ + div: "map", + projection: "EPSG:900913", + layers: [ + new OpenLayers.Layer.WMS("OSGeo", "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); + }, + tileloaded: updateStatus + } + }) + ], + center: [0,0], + zoom: 1 + }); + cacheWrite = new OpenLayers.Control.CacheWrite({ + autoActivate: true, + imageFormat: "image/jpeg", + eventListeners: { + cachefull: function() { status.innerHTML = "Cache full."; } + } + }); + map.addControl(cacheWrite); + + + + // User interface + var status = document.getElementById("status"); + document.getElementById("clear").onclick = function() { + OpenLayers.Control.CacheWrite.clearCache(); + updateStatus(); + }; + + // update the number of cached tiles and detect local storage support + function updateStatus() { + if (window.localStorage) { + status.innerHTML = localStorage.length + " entries in cache."; + } else { + status.innerHTML = "Local storage not supported. Try a different browser."; + } + } +} \ No newline at end of file diff --git a/examples/offline-storage.js b/examples/offline-storage.js index adf27d0aeb..c3c351ee2f 100644 --- a/examples/offline-storage.js +++ b/examples/offline-storage.js @@ -30,6 +30,9 @@ function init(){ imageFormat: "image/jpeg", eventListeners: { cachefull: function() { + if (seeding) { + stopSeeding(); + } status.innerHTML = "Cache full."; cacheFull = true; } @@ -56,13 +59,16 @@ function init(){ 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; + cacheFull = false, + seeding = false; updateLayerInfo(); var read = document.getElementById("read"); read.checked = true; @@ -75,7 +81,7 @@ function init(){ tileloadstart.checked = "checked"; tileloadstart.onclick = setType; document.getElementById("tileerror").onclick = setType; - document.getElementById("seed").onclick = seedCache; + document.getElementById("seed").onclick = startSeeding; // update the number of cached tiles and detect local storage support function updateLayerInfo(evt) { @@ -88,6 +94,7 @@ function init(){ 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) { @@ -100,6 +107,9 @@ function init(){ status.innerHTML = "Canvas not supported. Try a different browser."; } } catch(e) { + if (seeding) { + stopSeeding(); + } status.innerHTML = "CORS image requests not supported. Try a different layer."; } } @@ -140,15 +150,18 @@ function init(){ } } - // 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; + // start seeding the cache + function startSeeding() { + var layer = map.baseLayer, + zoom = map.getZoom(); + seeding = { + zoom: zoom, + extent: map.getExtent(), + center: map.getCenter(), + cacheWriteActive: cacheWrite.active, + buffer: layer.buffer, + layer: layer + }; // make sure the next setCenter triggers a load map.zoomTo(zoom === layer.numZoomLevels-1 ? zoom - 1 : zoom + 1); // turn on cache writing @@ -157,27 +170,38 @@ function init(){ 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(); - } - } - }); + layer.events.register("loadend", null, seed); // start seeding - map.setCenter(center, zoom); + map.setCenter(seeding.center, zoom); + } + + // seed a zoom level based on the extent at the time startSeeding was called + function seed() { + var layer = seeding.layer; + var tileWidth = layer.tileSize.w; + var nextZoom = map.getZoom() + 1; + var extentWidth = seeding.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) { + stopSeeding(); + } + } + + // stop seeding (when done or when cache is full) + function stopSeeding() { + // we're done - restore previous settings + seeding.layer.events.unregister("loadend", null, seed); + seeding.layer.buffer = seeding.buffer; + map.setCenter(seeding.center, seeding.zoom); + if (!seeding.cacheWriteActive) { + cacheWrite.deactivate(); + } + if (read.checked) { + setType(); + } + seeding = false; } } \ No newline at end of file From 5f48e546bcb60bc8f11fd973b043d05024f92ccb Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 9 Mar 2012 19:07:20 +0100 Subject: [PATCH 4/6] Simplified example. --- examples/offline-storage.js | 95 +++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/examples/offline-storage.js b/examples/offline-storage.js index c3c351ee2f..b82470159b 100644 --- a/examples/offline-storage.js +++ b/examples/offline-storage.js @@ -3,25 +3,24 @@ 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 - } - }) + layers: [ + new OpenLayers.Layer.OSM("OpenStreetMap (CORS)", null, { + eventListeners: { + tileloaded: updateStatus, + loadend: detect + } + }), + 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); + }, + tileloaded: updateStatus + } + }) ], center: [0,0], zoom: 1 @@ -34,7 +33,6 @@ function init(){ stopSeeding(); } status.innerHTML = "Cache full."; - cacheFull = true; } } }); @@ -67,9 +65,7 @@ function init(){ hits = document.getElementById("hits"), previousCount = -1, cacheHits = 0, - cacheFull = false, seeding = false; - updateLayerInfo(); var read = document.getElementById("read"); read.checked = true; read.onclick = toggleRead; @@ -82,36 +78,34 @@ function init(){ tileloadstart.onclick = setType; document.getElementById("tileerror").onclick = setType; document.getElementById("seed").onclick = startSeeding; - - // 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."; + + // detect what the browser supports + function detect(evt) { + // detection is only done once, so we remove the listener. + evt.object.events.unregister("loadend", null, detect); + var tile = map.baseLayer.grid[0][0]; + try { + var canvasContext = 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."; } - previousCount = localStorage.length; - } else { - status.innerHTML = "Local storage not supported. Try a different browser."; + } catch(e) { + // we remove the OSM layer if CORS image requests are not supported. + map.setBaseLayer(map.layers[1]); + evt.object.destroy(); + layerSwitcher.destroy(); } } - + // 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) { - if (seeding) { - stopSeeding(); - } - status.innerHTML = "CORS image requests not supported. Try a different layer."; - } + function updateStatus(evt) { + if (window.localStorage) { + status.innerHTML = localStorage.length + " entries in cache."; + } else { + status.innerHTML = "Local storage not supported. Try a different browser."; } if (evt.tile.url.substr(0, 5) === "data:") { cacheHits++; @@ -137,8 +131,7 @@ function init(){ // clear all tiles from the cache function clearCache() { OpenLayers.Control.CacheWrite.clearCache(); - cacheFull = false; - updateLayerInfo(); + updateStatus(); } // activate the cacheRead control that matches the desired fetch strategy @@ -185,7 +178,7 @@ function init(){ // 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) { + if (nextZoom === layer.numZoomLevels-1) { stopSeeding(); } } From 3d8a9168f09da950fd5c889663b18f51025514a5 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Fri, 9 Mar 2012 19:07:54 +0100 Subject: [PATCH 5/6] Always set the tile url. This avoids false positives in the url check in CacheWrite::cache when the CacheRead control is also active. --- lib/OpenLayers/Control/CacheRead.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/OpenLayers/Control/CacheRead.js b/lib/OpenLayers/Control/CacheRead.js index 554a6aee3c..6f44c7bbab 100644 --- a/lib/OpenLayers/Control/CacheRead.js +++ b/lib/OpenLayers/Control/CacheRead.js @@ -113,10 +113,9 @@ OpenLayers.Control.CacheRead = OpenLayers.Class(OpenLayers.Control, { var tile = evt.tile, dataURI = window.localStorage.getItem("olCache_" + tile.url); if (dataURI) { + tile.url = dataURI; if (evt.type === "tileerror") { tile.setImgSrc(dataURI); - } else { - tile.url = dataURI; } } } From bd262fce416bee7fca95bc5d85b0bac72150b50d Mon Sep 17 00:00:00 2001 From: ahocevar Date: Sun, 11 Mar 2012 22:15:34 +0100 Subject: [PATCH 6/6] Addressing @elemoine's review comments. Now the application no longer needs to care about the tile origin, because the CacheWrite control modifies the url if the CORS image loading is disabled and it is from a different origin. This only requires OpenLayers.ProxyHost to be properly configured. Also local storage keys use the original url instead of the proxied url, to make the CacheRead control work without proxy settings. No deferred exceptions are thrown any more. Instead, OpenLayers.Console is used to show an error message for security exceptions. We now check for OpenLayers.Tile.Image, because other tile types (e.g. UTFGrid) are not supported (yet). To make the same origin handling in the CacheWrite control easier, OpenLayers.Request now exposes the same origin logic from request.issue as a separate function, so it can also be used by other components. --- examples/cache-read.js | 12 ++--- examples/cache-write.html | 1 + examples/cache-write.js | 12 ++--- examples/offline-storage.html | 1 + examples/offline-storage.js | 39 ++++++++-------- lib/OpenLayers/Control/CacheRead.js | 20 ++++++--- lib/OpenLayers/Control/CacheWrite.js | 59 +++++++++++++++++++++---- lib/OpenLayers/Request.js | 66 ++++++++++++++++++---------- 8 files changed, 138 insertions(+), 72 deletions(-) diff --git a/examples/cache-read.js b/examples/cache-read.js index f8c42a9115..1f7988984e 100644 --- a/examples/cache-read.js +++ b/examples/cache-read.js @@ -1,5 +1,5 @@ var map, cacheRead; -function init(){ +function init() { map = new OpenLayers.Map({ div: "map", projection: "EPSG:900913", @@ -8,15 +8,11 @@ function init(){ layers: "basic" }, { eventListeners: { - tileloadstart: function(evt) { - // send requests through proxy - evt.tile.url = "proxy.cgi?url=" + encodeURIComponent(evt.tile.url); - }, tileloaded: updateHits } }) ], - center: [0,0], + center: [0, 0], zoom: 1 }); cacheRead = new OpenLayers.Control.CacheRead(); @@ -25,8 +21,8 @@ function init(){ // User interface - var status = document.getElementById("status"); - var hits = 0; + var status = document.getElementById("status"), + hits = 0; // update the number of cached tiles and detect local storage support function updateHits(evt) { diff --git a/examples/cache-write.html b/examples/cache-write.html index e36f3de9b7..ca59f68d80 100644 --- a/examples/cache-write.html +++ b/examples/cache-write.html @@ -8,6 +8,7 @@ + diff --git a/examples/cache-write.js b/examples/cache-write.js index b47cb60134..8f4ec9e081 100644 --- a/examples/cache-write.js +++ b/examples/cache-write.js @@ -1,5 +1,9 @@ +// Use proxy to get same origin URLs for tiles that don't support CORS. +OpenLayers.ProxyHost = "proxy.cgi?url="; + var map, cacheWrite; -function init(){ + +function init() { map = new OpenLayers.Map({ div: "map", projection: "EPSG:900913", @@ -8,15 +12,11 @@ function init(){ layers: "basic" }, { eventListeners: { - tileloadstart: function(evt) { - // send requests through proxy - evt.tile.url = "proxy.cgi?url=" + encodeURIComponent(evt.tile.url); - }, tileloaded: updateStatus } }) ], - center: [0,0], + center: [0, 0], zoom: 1 }); cacheWrite = new OpenLayers.Control.CacheWrite({ diff --git a/examples/offline-storage.html b/examples/offline-storage.html index 3cf235fdd4..1c510ca537 100644 --- a/examples/offline-storage.html +++ b/examples/offline-storage.html @@ -13,6 +13,7 @@ } + diff --git a/examples/offline-storage.js b/examples/offline-storage.js index b82470159b..e0b5929791 100644 --- a/examples/offline-storage.js +++ b/examples/offline-storage.js @@ -1,5 +1,9 @@ +// Use proxy to get same origin URLs for tiles that don't support CORS. +OpenLayers.ProxyHost = "proxy.cgi?url="; + var map, cacheWrite, cacheRead1, cacheRead2; -function init(){ + +function init() { map = new OpenLayers.Map({ div: "map", projection: "EPSG:900913", @@ -14,28 +18,13 @@ function init(){ layers: "basic" }, { eventListeners: { - tileloadstart: function(evt) { - // send requests through proxy - evt.tile.url = "proxy.cgi?url=" + encodeURIComponent(evt.tile.url); - }, tileloaded: updateStatus } }) ], - center: [0,0], + center: [0, 0], zoom: 1 }); - cacheWrite = new OpenLayers.Control.CacheWrite({ - imageFormat: "image/jpeg", - eventListeners: { - cachefull: function() { - if (seeding) { - stopSeeding(); - } - status.innerHTML = "Cache full."; - } - } - }); // try cache before loading from remote resource cacheRead1 = new OpenLayers.Control.CacheRead({ eventListeners: { @@ -54,8 +43,19 @@ function init(){ } } }); + cacheWrite = new OpenLayers.Control.CacheWrite({ + imageFormat: "image/jpeg", + eventListeners: { + cachefull: function() { + if (seeding) { + stopSeeding(); + } + status.innerHTML = "Cache full."; + } + } + }); var layerSwitcher = new OpenLayers.Control.LayerSwitcher(); - map.addControls([cacheWrite, cacheRead1, cacheRead2, layerSwitcher]); + map.addControls([cacheRead1, cacheRead2, cacheWrite, layerSwitcher]); layerSwitcher.maximizeControl(); @@ -63,7 +63,6 @@ function init(){ // add UI and behavior var status = document.getElementById("status"), hits = document.getElementById("hits"), - previousCount = -1, cacheHits = 0, seeding = false; var read = document.getElementById("read"); @@ -107,7 +106,7 @@ function init(){ } else { status.innerHTML = "Local storage not supported. Try a different browser."; } - if (evt.tile.url.substr(0, 5) === "data:") { + if (evt && evt.tile.url.substr(0, 5) === "data:") { cacheHits++; } hits.innerHTML = cacheHits + " cache hits."; diff --git a/lib/OpenLayers/Control/CacheRead.js b/lib/OpenLayers/Control/CacheRead.js index 6f44c7bbab..2589fdf6ca 100644 --- a/lib/OpenLayers/Control/CacheRead.js +++ b/lib/OpenLayers/Control/CacheRead.js @@ -33,8 +33,8 @@ OpenLayers.Control.CacheRead = OpenLayers.Class(OpenLayers.Control, { /** * APIProperty: layers - * {Array()}. Optional. If provided, only - * these layers will receive tiles from the cache. + * {Array()}. Optional. If provided, only these + * layers will receive tiles from the cache. */ layers: null, @@ -61,12 +61,12 @@ OpenLayers.Control.CacheRead = OpenLayers.Class(OpenLayers.Control, { */ setMap: function(map) { OpenLayers.Control.prototype.setMap.apply(this, arguments); - var i, layers = this.layers || this.map.layers; + var i, layers = this.layers || map.layers; for (i=layers.length-1; i>=0; --i) { this.addLayer({layer: layers[i]}); } if (!this.layers) { - this.map.events.on({ + map.events.on({ addlayer: this.addLayer, removeLayer: this.removeLayer, scope: this @@ -109,9 +109,17 @@ OpenLayers.Control.CacheRead = OpenLayers.Class(OpenLayers.Control, { * evt - {Object} Event object with a tile property. */ fetch: function(evt) { - if (this.active && window.localStorage) { + if (this.active && window.localStorage && + evt.tile instanceof OpenLayers.Tile.Image) { var tile = evt.tile, - dataURI = window.localStorage.getItem("olCache_" + tile.url); + url = tile.url; + // deal with modified tile urls when both CacheWrite and CacheRead + // are active + if (!tile.layer.crossOriginKeyword && OpenLayers.ProxyHost && + url.indexOf(OpenLayers.ProxyHost) === 0) { + url = OpenLayers.Control.CacheWrite.urlMap[url]; + } + var dataURI = window.localStorage.getItem("olCache_" + tile.url); if (dataURI) { tile.url = dataURI; if (evt.type === "tileerror") { diff --git a/lib/OpenLayers/Control/CacheWrite.js b/lib/OpenLayers/Control/CacheWrite.js index 0311fde801..85ece0b1dd 100644 --- a/lib/OpenLayers/Control/CacheWrite.js +++ b/lib/OpenLayers/Control/CacheWrite.js @@ -5,6 +5,8 @@ /** * @requires OpenLayers/Control.js + * @requires OpenLayers/Request.js + * @requires OpenLayers/Console.js */ /** @@ -84,12 +86,12 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { */ setMap: function(map) { OpenLayers.Control.prototype.setMap.apply(this, arguments); - var i, layers = this.layers || this.map.layers; + var i, layers = this.layers || map.layers; for (i=layers.length-1; i>=0; --i) { this.addLayer({layer: layers[i]}); } if (!this.layers) { - this.map.events.on({ + map.events.on({ addlayer: this.addLayer, removeLayer: this.removeLayer, scope: this @@ -107,7 +109,11 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { * instance */ addLayer: function(evt) { - evt.layer.events.register("tileloaded", this, this.cache); + evt.layer.events.on({ + tileloadstart: this.makeSameOrigin, + tileloaded: this.cache, + scope: this + }); }, /** @@ -120,7 +126,34 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { * instance */ removeLayer: function(evt) { - evt.layer.events.unregister("tileloaded", this, this.cache); + evt.layer.events.un({ + tileloadstart: this.makeSameOrigin, + tileloaded: this.cache, + scope: this + }); + }, + + /** + * Method: makeSameOrigin + * If the tile does not have CORS image loading enabled and is from a + * different origin, use OpenLayers.ProxyHost to make it a same origin url. + * + * Parameters: + * evt - {} + */ + makeSameOrigin: function(evt) { + if (this.active) { + var tile = evt.tile; + if (tile instanceof OpenLayers.Tile.Image && + !tile.crossOriginKeyword && + tile.url.substr(0, 5) !== "data:") { + var sameOriginUrl = OpenLayers.Request.makeSameOrigin( + tile.url, OpenLayers.ProxyHost + ); + OpenLayers.Control.CacheWrite.urlMap[sameOriginUrl] = tile.url; + tile.url = sameOriginUrl; + } + } }, /** @@ -135,14 +168,16 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { cache: function(obj) { if (this.active && window.localStorage) { var tile = obj.tile; - if (tile.url.substr(0, 5) !== 'data:') { + if (tile instanceof OpenLayers.Tile.Image && + tile.url.substr(0, 5) !== 'data:') { try { var canvasContext = tile.getCanvasContext(); if (canvasContext) { window.localStorage.setItem( - "olCache_" + tile.url, + "olCache_" + OpenLayers.Control.CacheWrite.urlMap[tile.url], canvasContext.canvas.toDataURL(this.imageFormat) ); + delete OpenLayers.Control.CacheWrite.urlMap[tile.url]; } } catch(e) { // local storage full or CORS violation @@ -150,8 +185,7 @@ OpenLayers.Control.CacheWrite = OpenLayers.Class(OpenLayers.Control, { 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); + OpenLayers.Console.error(e.toString()); } } } @@ -198,3 +232,12 @@ OpenLayers.Control.CacheWrite.clearCache = function() { } } }; + +/** + * Property: OpenLayers.Control.CacheWrite.urlMap + * {Object} Mapping of same origin urls to cache url keys. Entries will be + * deleted as soon as a tile was cached. + */ +OpenLayers.Control.CacheWrite.urlMap = {}; + + diff --git a/lib/OpenLayers/Request.js b/lib/OpenLayers/Request.js index a5de46fdee..86e15d8099 100644 --- a/lib/OpenLayers/Request.js +++ b/lib/OpenLayers/Request.js @@ -65,6 +65,47 @@ OpenLayers.Request = { */ events: new OpenLayers.Events(this), + /** + * Method: makeSameOrigin + * Using the specified proxy, returns a same origin url of the provided url. + * + * Parameters: + * url - {String} An arbitrary url + * proxy {String|Function} The proxy to use to make the provided url a + * same origin url. + * + * Returns + * {String} the same origin url. If no proxy is provided, the returned url + * will be the same as the provided url. + */ + makeSameOrigin: function(url, proxy) { + var sameOrigin = !(url.indexOf("http") == 0); + var urlParts = !sameOrigin && url.match(this.URL_SPLIT_REGEX); + if (urlParts) { + var location = window.location; + sameOrigin = + urlParts[1] == location.protocol && + urlParts[3] == location.hostname; + var uPort = urlParts[4], lPort = location.port; + if (uPort != 80 && uPort != "" || lPort != "80" && lPort != "") { + sameOrigin = sameOrigin && uPort == lPort; + } + } + if (!sameOrigin) { + if (proxy) { + if (typeof proxy == "function") { + url = proxy(url); + } else { + url = proxy + encodeURIComponent(url); + } + } else { + OpenLayers.Console.warn( + OpenLayers.i18n("proxyNeeded"), {url: url}); + } + } + return url; + }, + /** * APIMethod: issue * Create a new XMLHttpRequest object, open it, set any headers, bind @@ -153,30 +194,7 @@ OpenLayers.Request = { var request = new OpenLayers.Request.XMLHttpRequest(); var url = OpenLayers.Util.urlAppend(config.url, OpenLayers.Util.getParameterString(config.params || {})); - var sameOrigin = !(url.indexOf("http") == 0); - var urlParts = !sameOrigin && url.match(this.URL_SPLIT_REGEX); - if (urlParts) { - var location = window.location; - sameOrigin = - urlParts[1] == location.protocol && - urlParts[3] == location.hostname; - var uPort = urlParts[4], lPort = location.port; - if (uPort != 80 && uPort != "" || lPort != "80" && lPort != "") { - sameOrigin = sameOrigin && uPort == lPort; - } - } - if (!sameOrigin) { - if (config.proxy) { - if (typeof config.proxy == "function") { - url = config.proxy(url); - } else { - url = config.proxy + encodeURIComponent(url); - } - } else { - OpenLayers.Console.warn( - OpenLayers.i18n("proxyNeeded"), {url: url}); - } - } + url = OpenLayers.Request.makeSameOrigin(url, config.proxy); request.open( config.method, url, config.async, config.user, config.password );