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..1f7988984e --- /dev/null +++ b/examples/cache-read.js @@ -0,0 +1,36 @@ +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: { + tileloaded: updateHits + } + }) + ], + center: [0, 0], + zoom: 1 + }); + cacheRead = new OpenLayers.Control.CacheRead(); + map.addControl(cacheRead); + + + + // User interface + var status = document.getElementById("status"), + 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..ca59f68d80 --- /dev/null +++ b/examples/cache-write.html @@ -0,0 +1,37 @@ + + + + + + + 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..8f4ec9e081 --- /dev/null +++ b/examples/cache-write.js @@ -0,0 +1,48 @@ +// Use proxy to get same origin URLs for tiles that don't support CORS. +OpenLayers.ProxyHost = "proxy.cgi?url="; + +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: { + 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.html b/examples/offline-storage.html new file mode 100644 index 0000000000..1c510ca537 --- /dev/null +++ b/examples/offline-storage.html @@ -0,0 +1,44 @@ + + + + + + + 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..e0b5929791 --- /dev/null +++ b/examples/offline-storage.js @@ -0,0 +1,199 @@ +// 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() { + map = new OpenLayers.Map({ + div: "map", + projection: "EPSG:900913", + 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: { + tileloaded: updateStatus + } + }) + ], + center: [0, 0], + zoom: 1 + }); + // 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(); + } + } + }); + 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([cacheRead1, cacheRead2, cacheWrite, layerSwitcher]); + layerSwitcher.maximizeControl(); + + + + // add UI and behavior + var status = document.getElementById("status"), + hits = document.getElementById("hits"), + cacheHits = 0, + seeding = false; + 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 = startSeeding; + + // 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."; + } + } 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 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 && 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(); + updateStatus(); + } + + // activate the cacheRead control that matches the desired fetch strategy + function setType() { + if (tileloadstart.checked) { + cacheRead1.activate(); + } else { + cacheRead2.activate(); + } + } + + // 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 + cacheWrite.activate(); + // turn off cache reading + cacheRead1.deactivate(); + cacheRead2.deactivate(); + + layer.events.register("loadend", null, seed); + + // start seeding + 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 (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 diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js index 5953534fc0..749a14f027 100644 --- a/lib/OpenLayers.js +++ b/lib/OpenLayers.js @@ -211,6 +211,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..2589fdf6ca --- /dev/null +++ b/lib/OpenLayers/Control/CacheRead.js @@ -0,0 +1,156 @@ +/* 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 || map.layers; + for (i=layers.length-1; i>=0; --i) { + this.addLayer({layer: layers[i]}); + } + if (!this.layers) { + 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 && + evt.tile instanceof OpenLayers.Tile.Image) { + var tile = evt.tile, + 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") { + tile.setImgSrc(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..85ece0b1dd --- /dev/null +++ b/lib/OpenLayers/Control/CacheWrite.js @@ -0,0 +1,243 @@ +/* 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 + * @requires OpenLayers/Request.js + * @requires OpenLayers/Console.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. + * + * 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: + * - + */ +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 || map.layers; + for (i=layers.length-1; i>=0; --i) { + this.addLayer({layer: layers[i]}); + } + if (!this.layers) { + 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.on({ + tileloadstart: this.makeSameOrigin, + tileloaded: this.cache, + scope: this + }); + }, + + /** + * 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.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; + } + } + }, + + /** + * 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 instanceof OpenLayers.Tile.Image && + tile.url.substr(0, 5) !== 'data:') { + try { + var canvasContext = tile.getCanvasContext(); + if (canvasContext) { + window.localStorage.setItem( + "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 + var reason = e.name || e.message; + if (reason && this.quotaRegEx.test(reason)) { + this.events.triggerEvent("cachefull", {tile: tile}); + } else { + OpenLayers.Console.error(e.toString()); + } + } + } + } + }, + + /** + * 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); + } + } +}; + +/** + * 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 ); 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 cef96a4dd7..973576590f 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