localStorage , so I decided to switch to the bright side of power, where the code is open, simple and understandable. emr.fire('storageLoaded', storage); , which will be called after the storage is initialized and ready to use, so that the card does not fall when accessing the storage. var getIndexedDBStorage = function () { var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; var IndexedDBImpl = function () { var self = this; var db = null; var request = indexedDB.open('TileStorage'); request.onsuccess = function() { db = this.result; emr.fire('storageLoaded', self); }; request.onerror = function (error) { console.log(error); }; request.onupgradeneeded = function () { var store = this.result.createObjectStore('tile', { keyPath: 'key'}); store.createIndex('key', 'key', { unique: true }); }; this.add = function (key, value) { var transaction = db.transaction(['tile'], 'readwrite'); var objectStore = transaction.objectStore('tile'); objectStore.put({key: key, value: value}); }; this.delete = function (key) { var transaction = db.transaction(['tile'], 'readwrite'); var objectStore = transaction.objectStore('tile'); objectStore.delete(key); }; this.get = function (key, successCallback, errorCallback) { var transaction = db.transaction(['tile'], 'readonly'); var objectStore = transaction.objectStore('tile'); var result = objectStore.get(key); result.onsuccess = function () { successCallback(this.result ? this.result.value : undefined); }; result.onerror = errorCallback; }; }; return indexedDB ? new IndexedDBImpl() : null; };  var getWebSqlStorage = function () { var openDatabase = window.openDatabase; var WebSqlImpl = function () { var self = this; var db = openDatabase('TileStorage', '1.0', 'Tile Storage', 5 * 1024 * 1024); db.transaction(function (tx) { tx.executeSql('CREATE TABLE IF NOT EXISTS tile (key TEXT PRIMARY KEY, value TEXT)', [], function () { emr.fire('storageLoaded', self); }); }); this.add = function (key, value) { db.transaction(function (tx) { tx.executeSql('INSERT INTO tile (key, value) VALUES (?, ?)', [key, value]); }); }; this.delete = function (key) { db.transaction(function (tx) { tx.executeSql('DELETE FROM tile WHERE key = ?', [key]); }); }; this.get = function (key, successCallback, errorCallback) { db.transaction(function (tx) { tx.executeSql('SELECT value FROM tile WHERE key = ?', [key], function (tx, result) { successCallback(result.rows.length ? result.rows.item(0).value : undefined); }, errorCallback); }); }; }; return openDatabase ? new WebSqlImpl() : null; };  var storage = getIndexedDBStorage() || getWebSqlStorage() || null; if (!storage) { emr.fire('storageLoaded', null); } onupgradeneeded event may not be implemented in them.toDataURL or getImageData : _imageToDataUri: function (image) { var canvas = window.document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; var context = canvas.getContext('2d'); context.drawImage(image, 0, 0); return canvas.toDataURL('image/png'); } Access-Control-Allow-Origing for Your domain will not be saved. Fortunately, mapnik tiles or tile.openstreetmap.org have an Access-Control-Allow-Origing: * , but for everything to work, you need to set the flag of the img.crossOrigin element to Anonymous .WebWorkers + AJAX instead of canvas .src for tiles: _loadTile: function (tile, tilePoint) { tile._layer = this; tile.onload = this._tileOnLoad; tile.onerror = this._tileOnError; tile.src = this.getTileUrl(tilePoint); } src from storage, then we will do what we need. We also implement adding data to the repository, if they were downloaded from the network and we get full caching. var StorageTileLayer = L.TileLayer.extend({ _imageToDataUri: function (image) { var canvas = window.document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; var context = canvas.getContext('2d'); context.drawImage(image, 0, 0); return canvas.toDataURL('image/png'); }, _tileOnLoadWithCache: function () { var storage = this._layer.options.storage; if (storage) { storage.add(this._storageKey, this._layer._imageToDataUri(this)); } L.TileLayer.prototype._tileOnLoad.apply(this, arguments); }, _setUpTile: function (tile, key, value, cache) { tile._layer = this; if (cache) { tile._storageKey = key; tile.onload = this._tileOnLoadWithCache; tile.crossOrigin = 'Anonymous'; } else { tile.onload = this._tileOnLoad; } tile.onerror = this._tileOnError; tile.src = value; }, _loadTile: function (tile, tilePoint) { this._adjustTilePoint(tilePoint); var key = tilePoint.z + ',' + tilePoint.y + ',' + tilePoint.x; var self = this; if (this.options.storage) { this.options.storage.get(key, function (value) { if (value) { self._setUpTile(tile, key, value, false); } else { self._setUpTile(tile, key, self.getTileUrl(tilePoint), true); } }, function () { self._setUpTile(tile, key, self.getTileUrl(tilePoint), true); }); } else { self._setUpTile(tile, key, self.getTileUrl(tilePoint), false); } } });  var map = L.map('map').setView([53.902254, 27.561850], 13); new StorageTileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {storage: storage}).addTo(map);  CACHE MANIFEST NETWORK: * CACHE: index.html style.css event.js storage.js map.js run.js leaflet.css leaflet.js images/layers.png images/marker-icon.png images/marker-icon@2x.png images/marker-shadow.png TemplatedLayer , which is a regular map layer with a template provider, the code that we need will be in the layer class: MM.TemplatedLayer = function(template, subdomains, name) { return new MM.Layer(new MM.Template(template, subdomains), null, name); }; requestManager URL being sent to the requestManager : if (!this.requestManager.hasRequest(tile_key)) { var tileToRequest = this.provider.getTile(tile_coord); if (typeof tileToRequest == 'string') { this.addTileImage(tile_key, tile_coord, tileToRequest); } else if (tileToRequest) { this.addTileElement(tile_key, tile_coord, tileToRequest); } }  addTileImage: function(key, coord, url) { this.requestManager.requestTile(key, coord, url); }  addTileElement: function(key, coordinate, element) { element.id = key; element.coord = coordinate.copy(); this.positionTile(element); } requestManager itself is initialized in the map layer's constructor. Creating an img element img and setting its src occurs in the processQueue method, which also jerks from the map layer: processQueue: function(sortFunc) { if (sortFunc && this.requestQueue.length > 8) { this.requestQueue.sort(sortFunc); } while (this.openRequestCount < this.maxOpenRequests && this.requestQueue.length > 0) { var request = this.requestQueue.pop(); if (request) { this.openRequestCount++; var img = document.createElement('img'); img.id = request.id; img.style.position = 'absolute'; img.coord = request.coord; this.loadingBay.appendChild(img); img.onload = img.onerror = this.getLoadComplete(); img.src = request.url; request = request.id = request.coord = request.url = null; } } }  var StorageRequestManager = function (storage) { MM.RequestManager.apply(this, []); this._storage = storage; }; StorageRequestManager.prototype._imageToDataUri = function (image) { var canvas = window.document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; var context = canvas.getContext('2d'); context.drawImage(image, 0, 0); return canvas.toDataURL('image/png'); }; StorageRequestManager.prototype._createTileImage = function (id, coord, value, cache) { var img = window.document.createElement('img'); img.id = id; img.style.position = 'absolute'; img.coord = coord; this.loadingBay.appendChild(img); if (cache) { img.onload = this.getLoadCompleteWithCache(); img.crossOrigin = 'Anonymous'; } else { img.onload = this.getLoadComplete(); } img.onerror = this.getLoadComplete(); img.src = value; }; StorageRequestManager.prototype._loadTile = function (id, coord, url) { var self = this; if (this._storage) { this._storage.get(id, function (value) { if (value) { self._createTileImage(id, coord, value, false); } else { self._createTileImage(id, coord, url, true); } }, function () { self._createTileImage(id, coord, url, true); }); } else { self._createTileImage(id, coord, url, false); } }; StorageRequestManager.prototype.processQueue = function (sortFunc) { if (sortFunc && this.requestQueue.length > 8) { this.requestQueue.sort(sortFunc); } while (this.openRequestCount < this.maxOpenRequests && this.requestQueue.length > 0) { var request = this.requestQueue.pop(); if (request) { this.openRequestCount++; this._loadTile(request.id, request.coord, request.url); request = request.id = request.coord = request.url = null; } } }; StorageRequestManager.prototype.getLoadCompleteWithCache = function () { if (!this._loadComplete) { var theManager = this; this._loadComplete = function(e) { e = e || window.event; var img = e.srcElement || e.target; img.onload = img.onerror = null; if (theManager._storage) { theManager._storage.add(this.id, theManager._imageToDataUri(this)); } theManager.loadingBay.removeChild(img); theManager.openRequestCount--; delete theManager.requestsById[img.id]; if (e.type === 'load' && (img.complete || (img.readyState && img.readyState === 'complete'))) { theManager.dispatchCallback('requestcomplete', img); } else { theManager.dispatchCallback('requesterror', { element: img, url: ('' + img.src) }); img.src = null; } setTimeout(theManager.getProcessQueue(), 0); }; } return this._loadComplete; }; MM.extend(StorageRequestManager, MM.RequestManager); var StorageLayer = function(provider, parent, name, storage) { this.parent = parent || document.createElement('div'); this.parent.style.cssText = 'position: absolute; top: 0px; left: 0px;' + 'width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0'; this.name = name; this.levels = {}; this.requestManager = new StorageRequestManager(storage); this.requestManager.addCallback('requestcomplete', this.getTileComplete()); this.requestManager.addCallback('requesterror', this.getTileError()); if (provider) { this.setProvider(provider); } }; MM.extend(StorageLayer, MM.Layer); var StorageTemplatedLayer = function(template, subdomains, name, storage) { return new StorageLayer(new MM.Template(template, subdomains), null, name, storage); };  var map = mapbox.map('map'); map.addLayer(new StorageTemplatedLayer('http://{S}.tile.osm.org/{Z}/{X}/{Y}.png', ['a', 'b', 'c'], undefined, storage)); map.ui.zoomer.add(); map.ui.zoombox.add(); map.centerzoom({lat: 53.902254, lon: 27.561850}, 13);  CACHE MANIFEST NETWORK: * CACHE: index.html style.css event.js storage.js map.js run.js mapbox.css mapbox.js map-controls.png  [first] [last] [include] OpenLayers/Map.js OpenLayers/Layer/OSM.js OpenLayers/Control/Zoom.js OpenLayers/Control/Navigation.js OpenLayers/Control/TouchNavigation.js [exclude] OpenLayers.Layer.OSM , so I’ll start the search from it: url: [ 'http://a.tile.openstreetmap.org/${z}/${x}/${y}.png', 'http://b.tile.openstreetmap.org/${z}/${x}/${y}.png', 'http://c.tile.openstreetmap.org/${z}/${x}/${y}.png' ] OpenLayers.Layer.OSM inherited from OpenLayers.Layer.XYZ with redefined URLs. The getURL method is interesting here: getURL: function (bounds) { var xyz = this.getXYZ(bounds); var url = this.url; if (OpenLayers.Util.isArray(url)) { var s = '' + xyz.x + xyz.y + xyz.z; url = this.selectUrl(s, url); } return OpenLayers.String.format(url, xyz); } getXYZ method, which can be used to create a key: getXYZ: function(bounds) { var res = this.getServerResolution(); var x = Math.round((bounds.left - this.maxExtent.left) / (res * this.tileSize.w)); var y = Math.round((this.maxExtent.top - bounds.top) / (res * this.tileSize.h)); var z = this.getServerZoom(); if (this.wrapDateLine) { var limit = Math.pow(2, z); x = ((x % limit) + limit) % limit; } return {'x': x, 'y': y, 'z': z}; } OpenLayers.Layer.XYZ itself is inherited from OpenLayers.Layer.Grid , which has the addTile method and which internally creates tiles using tileClass , which is OpenLayers.Tile.Image : addTile: function(bounds, position) { var tile = new this.tileClass( this, position, bounds, null, this.tileSize, this.tileOptions ); this.events.triggerEvent("addtile", {tile: tile}); return tile; } OpenLayers.Tile.Image src is set in the setImgSrc method: setImgSrc: function(url) { var img = this.imgDiv; if (url) { img.style.visibility = 'hidden'; img.style.opacity = 0; if (this.crossOriginKeyword) { if (url.substr(0, 5) !== 'data:') { img.setAttribute("crossorigin", this.crossOriginKeyword); } else { img.removeAttribute("crossorigin"); } } img.src = url; } else { this.stopLoading(); this.imgDiv = null; if (img.parentNode) { img.parentNode.removeChild(img); } } } onload and onerror . The method itself is twitching from initImage , where these handlers are hung: initImage: function() { this.events.triggerEvent('beforeload'); this.layer.div.appendChild(this.getTile()); this.events.triggerEvent(this._loadEvent); var img = this.getImage(); if (this.url && img.getAttribute("src") == this.url) { this._loadTimeout = window.setTimeout( OpenLayers.Function.bind(this.onImageLoad, this), 0 ); } else { this.stopLoading(); if (this.crossOriginKeyword) { img.removeAttribute("crossorigin"); } OpenLayers.Event.observe(img, "load", OpenLayers.Function.bind(this.onImageLoad, this) ); OpenLayers.Event.observe(img, "error", OpenLayers.Function.bind(this.onImageError, this) ); this.imageReloadAttempts = 0; this.setImgSrc(this.url); } } getURL class method, as well as initImage , is jerked from the renderTile : renderTile: function() { if (this.layer.async) { var id = this.asyncRequestId = (this.asyncRequestId || 0) + 1; this.layer.getURLasync(this.bounds, function(url) { if (id == this.asyncRequestId) { this.url = url; this.initImage(); } }, this); } else { this.url = this.layer.getURL(this.bounds); this.initImage(); } }  var StorageImageTile = OpenLayers.Class(OpenLayers.Tile.Image, { _imageToDataUri: function (image) { var canvas = window.document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; var context = canvas.getContext('2d'); context.drawImage(image, 0, 0); return canvas.toDataURL('image/png'); }, onImageLoadWithCache: function() { if (this.storage) { this.storage.add(this._storageKey, this._imageToDataUri(this.imgDiv)); } this.onImageLoad.apply(this, arguments); }, renderTile: function() { var self = this; var xyz = this.layer.getXYZ(this.bounds); var key = xyz.z + ',' + xyz.y + ',' + xyz.x; var url = this.layer.getURL(this.bounds); if (this.storage) { this.storage.get(key, function (value) { if (value) { self.initImage(key, value, false); } else { self.initImage(key, url, true); } }, function () { self.initImage(key, url, true); }); } else { self.initImage(key, url, false); } }, initImage: function(key, url, cache) { this.events.triggerEvent('beforeload'); this.layer.div.appendChild(this.getTile()); this.events.triggerEvent(this._loadEvent); var img = this.getImage(); this.stopLoading(); if (cache) { OpenLayers.Event.observe(img, 'load', OpenLayers.Function.bind(this.onImageLoadWithCache, this) ); this._storageKey = key; } else { OpenLayers.Event.observe(img, 'load', OpenLayers.Function.bind(this.onImageLoad, this) ); } OpenLayers.Event.observe(img, 'error', OpenLayers.Function.bind(this.onImageError, this) ); this.imageReloadAttempts = 0; this.setImgSrc(url); } }); var StorageOSMLayer = OpenLayers.Class(OpenLayers.Layer.OSM, { async: true, tileClass: StorageImageTile, initialize: function(name, url, options) { OpenLayers.Layer.OSM.prototype.initialize.apply(this, arguments); this.tileOptions = OpenLayers.Util.extend({ storage: options.storage }, this.options && this.options.tileOptions); }, clone: function (obj) { if (obj == null) { obj = new StorageOSMLayer(this.name, this.url, this.getOptions()); } obj = OpenLayers.Layer.Grid.prototype.clone.apply(this, [obj]); return obj; } });  var map = new OpenLayers.Map('map'); map.addLayer(new StorageOSMLayer(undefined, undefined, {storage: storage})); var fromProjection = new OpenLayers.Projection('EPSG:4326'); var toProjection = new OpenLayers.Projection('EPSG:900913'); var center = new OpenLayers.LonLat(27.561850, 53.902254).transform(fromProjection, toProjection); map.setCenter(center, 13);  CACHE MANIFEST NETWORK: * CACHE: index.html style.css event.js storage.js map.js run.js theme/default/style.css OpenLayers.js OpenLayers.Control.CacheWrite cache, but only using localStorage , which is not very interesting.IndexedDB or WebSQL is enough to cache a city or more, which makes the application approach more interesting than in the version with localStorage .Source: https://habr.com/ru/post/170129/
All Articles