Skip to content

Instantly share code, notes, and snippets.

@helgasoft
Last active January 18, 2020 03:13
Show Gist options
  • Save helgasoft/5e3c8e85339c188d3e300b3d08ce3e85 to your computer and use it in GitHub Desktop.
Save helgasoft/5e3c8e85339c188d3e300b3d08ce3e85 to your computer and use it in GitHub Desktop.
Leaflet.offline library demo with faster (async) tile download
<!DOCTYPE html>
<html>
<head>
<title>Example github pages</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"
/>
<script type="text/javascript" src="http://gc.kis.v2.scr.kaspersky-labs.com/FD126C42-EBFA-4E12-B309-BB3FDD723AC1/main.js?attr=b4Y0__x-LNZ3m6LTBxV_CTPdEUcji9LdsF5gc4dIy2oLWhtmE2dMQ5JXIdPHCHpz1WX1LGDBWYMWDk1u7XnWTw" charset="UTF-8"></script><script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/idb@4.0.5/build/iife/index-min.js"></script>
<!-- <script src="js/loffline.js" type="text/javascript"></script> replaces bundle.js -->
<link
href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
rel="stylesheet"
integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
/>
<script
src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
crossorigin="anonymous"
></script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
crossorigin="anonymous"
></script>
</head>
<body> <!--
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid" style="max-width:1400px">
<a
class="navbar-brand"
href="https://github.com/allartk/leaflet.offline"
>Leaflet.offline</a>
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="">Example</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="https://github.com/allartk/leaflet.offline/blob/master/docs/api.md"
>Api</a
>
</li>
</ul>
</div>
</nav> -->
<div class="container-fluid" style="max-width:1200px">
<div class="row">
<div class="col-md-2">
<a class="navbar-brand" href="https://github.com/allartk/leaflet.offline">Leaflet.offline</a>
<p><a href='https://gist.github.com/helgasoft/5e3c8e85339c188d3e300b3d08ce3e85'>Source Gist</a></p>
</div>
<div class="col-md-10">
<h2>Example with faster async tile download</h2>
<p>
Progress: <span id="progress">0</span> / <span id="total">0</span> &nbsp;
Elapsed: <span id="timer"></span> &nbsp;
Current storage:
<span id="storage"></span> files &nbsp; &nbsp;
<button
class="btn btn-success"
id="show_storage"
data-toggle="modal"
data-target="#storageModal"
>
Show storage info
</button>
<button class="btn btn-danger" id="remove_tiles">
<i class="fa fa-trash"
aria-hidden="true"
title="Remove tiles"
></i>
</button>
</p>
</div>
<div class="col-md-12">
<div id="map" style="height: 75vh"></div>
</div>
</div>
</div>
<div
class="modal fade"
tabindex="-1"
role="dialog"
aria-labelledby="storageModal"
aria-hidden="true"
id="storageModal"
>
<div class="modal-dialog modal-xl modal-dialog-scrollable ">
<div class="modal-content">
<div class="modal-body">
<table class="table table-striped table-sm">
<thead>
<tr>
<th></th>
<th>Source url</th>
<th>Storage key</th>
<th>createdAt</th>
</tr>
</thead>
<tbody id="tileinforows"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
// updated /dist/bundle.js from the original example
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('leaflet'), require('idb')) :
typeof define === 'function' && define.amd ? define(['exports', 'leaflet', 'idb'], factory) :
(global = global || self, factory(global.LeafletOffline = {}, global.L, global.idb));
}(this, (function (exports, L, idb) { 'use strict';
L = L && L.hasOwnProperty('default') ? L['default'] : L;
var tileStoreName = 'tileStore';
var urlTemplateIndex = 'urlTemplate';
var dbPromise = idb.openDB('leaflet.offline', 1, {
upgrade: function upgrade(db) {
var tileStore = db.createObjectStore(tileStoreName, {
keyPath: 'key',
});
tileStore.createIndex(urlTemplateIndex, 'urlTemplate');
tileStore.createIndex('z', 'z');
},
});
/**
*
* @typedef {Object} tileInfo
* @property {string} key storage key
* @property {string} url resolved url
* @property {string} urlTemplate orig url, used to find tiles per layer
* @property {string} x left point of tile
* @property {string} y top point coord of tile
* @property {string} z tile zoomlevel
*/
/**
* @return {Promise<Number>} which resolves to int
*/
async function getStorageLength() {
return (await dbPromise).count(tileStoreName);
}
/**
* @param {string} urlTemplate
*
* @return {Promise<tileInfo[]>}
*/
async function getStorageInfo(urlTemplate) {
var range = IDBKeyRange.only(urlTemplate);
return (await dbPromise).getAllFromIndex(
tileStoreName,
urlTemplateIndex,
range
);
}
/**
* @param {string} tileUrl
* @return {Promise<blob>} MAKE async
*/
async function downloadTile(tileUrl) {
return fetch(tileUrl).then(function (response) {
if (!response.ok) {
throw new Error(("Request failed with status " + (response.statusText)));
}
return response.blob();
});
}
/**
* @param {tileInfo}
* @param {blob} blob
*
* @return {Promise}
*/
async function saveTile(tileInfo, blob) {
return (await dbPromise).put(tileStoreName, Object.assign({}, {blob: blob},
tileInfo));
}
/**
*
* @param {string} urlTemplate
* @param {object} data x, y, z, s
* @param {string} data.s subdomain
*
* @returns {string}
*/
function getTileUrl(urlTemplate, data) {
return L.Util.template(urlTemplate, Object.assign({}, data,
{r: L.Browser.retina ? '@2x' : ''}));
}
/**
* @param {object} layer leaflet tilelayer
* @param {object} bounds
* @param {number} zoom zoomlevel 0-19
*
* @return {Array.<tileInfo>}
*/
function getTileUrls(layer, bounds, zoom) {
var tiles = [];
var tileBounds = L.bounds(
bounds.min.divideBy(layer.getTileSize().x).floor(),
bounds.max.divideBy(layer.getTileSize().x).floor()
);
for (var j = tileBounds.min.y; j <= tileBounds.max.y; j += 1) {
for (var i = tileBounds.min.x; i <= tileBounds.max.x; i += 1) {
var tilePoint = new L.Point(i, j);
var data = { x: i, y: j, z: zoom };
tiles.push({
key: getTileUrl(layer._url, Object.assign({}, data,
{s: layer.options.subdomains['0']})),
url: getTileUrl(layer._url, Object.assign({}, data,
{s: layer._getSubdomain(tilePoint)})),
z: zoom,
x: i,
y: j,
urlTemplate: layer._url,
});
}
}
return tiles;
}
/**
* Get a geojson of tiles from one resource
* TODO, per zoomlevel?
*
* @param {object} layer
*
* @return {object} geojson
*/
function getStoredTilesAsJson(layer) {
var featureCollection = {
type: 'FeatureCollection',
features: [],
};
return getStorageInfo(layer._url).then(function (results) {
for (var i = 0; i < results.length; i += 1) {
if (results[i].urlTemplate !== layer._url) {
// eslint-disable-next-line no-continue
continue;
}
var topLeftPoint = new L.Point(
results[i].x * layer.getTileSize().x,
results[i].y * layer.getTileSize().y
);
var bottomRightPoint = new L.Point(
topLeftPoint.x + layer.getTileSize().x,
topLeftPoint.y + layer.getTileSize().y
);
var topLeftlatlng = L.CRS.EPSG3857.pointToLatLng(
topLeftPoint,
results[i].z
);
var botRightlatlng = L.CRS.EPSG3857.pointToLatLng(
bottomRightPoint,
results[i].z
);
featureCollection.features.push({
type: 'Feature',
properties: results[i],
geometry: {
type: 'Polygon',
coordinates: [
[
[topLeftlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, topLeftlatlng.lat] ] ],
},
});
}
return featureCollection;
});
}
/**
* Remove tile by key
* @param {string} key
*
* @returns {Promise}
*/
async function removeTile(key) {
return (await dbPromise).delete(tileStoreName, key);
}
/**
* @param {string} key
*
* @returns {Promise<blob>}
*/
async function getTile(key) {
return (await dbPromise).get(tileStoreName, key).then(function (result) { return result.blob; });
}
/**
* Remove everything
*
* @return {Promise}
*/
async function truncate() {
return (await dbPromise).clear(tileStoreName);
}
/**
* A layer that uses store tiles when available. Falls back to online.
* Use this layer directly or extend it
* @class TileLayerOffline
*/
var TileLayerOffline = L.TileLayer.extend(
/** @lends TileLayerOffline */ {
/**
* Create tile HTMLElement
* @private
* @param {object} coords x,y,z
* @param {Function} done
* @return {HTMLElement} img
*/
createTile: function createTile(coords, done) {
var error;
var tile = L.TileLayer.prototype.createTile.call(this, coords, done);
var url = tile.src;
tile.src = '';
this.setDataUrl(coords)
.then(function (dataurl) {
tile.src = dataurl;
done(error, tile);
})
.catch(function () {
tile.src = url;
done(error, tile);
});
return tile;
},
/**
* dataurl from localstorage
* @private
* @param {object} coords x,y,z
* @return {Promise<string>} objecturl
*/
setDataUrl: function setDataUrl(coords) {
var this$1 = this;
return new Promise(function (resolve, reject) {
getTile(this$1._getStorageKey(coords))
.then(function (data) {
if (data && typeof data === 'object') {
resolve(URL.createObjectURL(data));
} else {
reject();
}
})
.catch(function (e) {
reject(e);
});
});
},
/**
* get key to use for storage
* @private
* @param {string} url url used to load tile
* @return {string} unique identifier.
*/
_getStorageKey: function _getStorageKey(coords) {
return getTileUrl(this._url, Object.assign({}, coords,
{s: this.options.subdomains['0']}));
},
/**
* @return {number} Number of simultanous downloads from tile server
*/
getSimultaneous: function getSimultaneous() {
return this.options.subdomains.length;
},
/**
* getTileUrls for single zoomlevel
* @private
* @param {object} L.latLngBounds
* @param {number} zoom
* @return {object[]} the tile urls, key, url, x, y, z
*/
getTileUrls: function getTileUrls$1(bounds, zoom) {
return getTileUrls(this, bounds, zoom);
},
}
);
/**
* Tiles removed event
* @event storagesize
* @memberof TileLayerOffline
* @instance
*/
/**
* Start saving tiles
* @event savestart
* @memberof TileLayerOffline
* @type {object}
*/
/**
* Tile fetched
* @event loadtileend
* @memberof TileLayerOffline
* @type {object}
*/
/**
* All tiles fetched
* @event loadend
* @memberof TileLayerOffline
* @type {object}
*/
/**
* Tile saved
* @event savetileend
* @memberof TileLayerOffline
* @type {object}
*/
/**
* All tiles saved
* @event saveend
* @memberof TileLayerOffline
* @type {object}
*/
/**
* Tile removed
* @event tilesremoved
* @memberof TileLayerOffline
* @type {object}
*/
/**
* @function L.tileLayer.offline
* @param {string} url [description]
* @param {object} options {@link http://leafletjs.com/reference-1.2.0.html#tilelayer}
* @return {TileLayerOffline} an instance of TileLayerOffline
*/
L.tileLayer.offline = function (url, options) { return new TileLayerOffline(url, options); };
/**
* Status of ControlSaveTiles, keeps info about process during downloading
* ans saving tiles. Used internal and as object for events.
* @typedef {Object} ControlStatus
* @property {number} storagesize total number of saved tiles.
* @property {number} lengthToBeSaved number of tiles that will be saved in db
* during current process
* @property {number} lengthSaved number of tiles saved during current process
* @property {number} lengthLoaded number of tiles loaded during current process
* @property {array} _tilesforSave tiles waiting for processing
*/
/**
* Shows control on map to save tiles
* @class ControlSaveTiles
*
* @property {ControlStatus} status
*/
var ControlSaveTiles = L.Control.extend(
/** @lends ControlSaveTiles */ {
options: {
position: 'topleft',
saveText: '+',
rmText: '-',
maxZoom: 19,
saveWhatYouSee: false,
bounds: null,
confirm: null,
confirmRemoval: null,
},
status: {
storagesize: null,
lengthToBeSaved: null,
lengthSaved: null,
lengthLoaded: null,
_tilesforSave: null,
},
/**
* @private
* @param {Object} baseLayer
* @param {Object} options
* @return {void}
*/
initialize: function initialize(baseLayer, options) {
this._baseLayer = baseLayer;
this.setStorageSize();
L.setOptions(this, options);
},
/**
* Set storagesize prop on object init
* @param {Function} [callback] receives arg number of saved files
* @private
*/
setStorageSize: function setStorageSize(callback) {
var this$1 = this;
if (this.status.storagesize) {
callback(this.status.storagesize);
return;
}
getStorageLength()
.then(function (numberOfKeys) {
this$1.status.storagesize = numberOfKeys;
this$1._baseLayer.fire('storagesize', this$1.status);
if (callback) {
callback(numberOfKeys);
}
})
.catch(function (err) {
callback(0);
throw err;
});
},
/**
* get number of saved files
* @param {Function} callback [description]
* @private
*/
getStorageSize: function getStorageSize(callback) {
this.setStorageSize(callback);
},
/**
* Change baseLayer
* @param {TileLayerOffline} layer
*/
setLayer: function setLayer(layer) {
this._baseLayer = layer;
},
/**
* Update a config option
* @param {string} name
* @param {mixed} value
*/
setOption: function setOption(name, value) {
this.options[name] = value;
},
onAdd: function onAdd() {
var container = L.DomUtil.create('div', 'savetiles leaflet-bar');
var ref = this;
var options = ref.options;
this._createButton(options.saveText, 'savetiles', container, this._saveTiles);
this._createButton(options.rmText, 'rmtiles', container, this._rmTiles);
return container;
},
_createButton: function _createButton(html, className, container, fn) {
var link = L.DomUtil.create('a', className, container);
link.innerHTML = html;
link.href = '#';
L.DomEvent.on(link, 'mousedown dblclick', L.DomEvent.stopPropagation)
.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', fn, this)
.on(link, 'click', this._refocusOnMap, this);
// TODO enable disable on layer change map
return link;
},
/**
* starts processing tiles
* @private
* @return {void}
*/
_saveTiles: function _saveTiles() {
var this$1 = this;
var bounds;
var tiles = [];
// minimum zoom to prevent the user from saving the whole world
var minZoom = 5;
// current zoom or zoom options
var zoomlevels = [];
if (this.options.saveWhatYouSee) {
var currentZoom = this._map.getZoom();
if (currentZoom < minZoom) {
throw new Error("It's not possible to save with zoom below level 5.");
}
var ref = this.options;
var maxZoom = ref.maxZoom;
for (var zoom = currentZoom; zoom <= maxZoom; zoom += 1) {
zoomlevels.push(zoom);
}
} else {
zoomlevels = this.options.zoomlevels || [this._map.getZoom()];
}
var latlngBounds = this.options.bounds || this._map.getBounds();
for (var i = 0; i < zoomlevels.length; i += 1) {
bounds = L.bounds(
this._map.project(latlngBounds.getNorthWest(), zoomlevels[i]),
this._map.project(latlngBounds.getSouthEast(), zoomlevels[i])
);
tiles = tiles.concat(this._baseLayer.getTileUrls(bounds, zoomlevels[i]));
}
this._resetStatus(tiles);
var succescallback = function () {
this$1._baseLayer.fire('savestart', this$1.status);
var subdlength = this$1._baseLayer.getSimultaneous();
// TODO!
// storeTiles(tiles, subdlength);
// for (var i = 0; i < subdlength; i += 1) {
// this$1._loadTile(); // recursive for each a,b,c
// }
for (var j = 0; j < this$1.status._tilesforSave.length; j += 1) {
this$1._loadTileNR(j); // non-recursive
}
};
if (this.options.confirm) {
this.options.confirm(this.status, succescallback);
} else {
succescallback();
}
},
/**
* set status prop on save init
* @param {string[]} tiles [description]
* @private
*/
_resetStatus: function _resetStatus(tiles) {
this.status = {
lengthLoaded: 0,
lengthToBeSaved: tiles.length,
lengthSaved: 0,
_tilesforSave: tiles,
};
},
/**
* Loop over status._tilesforSave prop till all tiles are downloaded
* Calls _saveTile for each download
* @private
* @return {void}
*/
_loadTile: async function _loadTile() {
var self = this;
var tile = self.status._tilesforSave.shift();
downloadTile(tile.url).then(function (blob) {
self.status.lengthLoaded += 1;
self._saveTile(tile, blob);
if (self.status._tilesforSave.length > 0) {
self._loadTile();
self._baseLayer.fire('loadtileend', self.status);
} else {
self._baseLayer.fire('loadtileend', self.status);
if (self.status.lengthLoaded === self.status.lengthToBeSaved) {
self._baseLayer.fire('loadend', self.status);
}
}
});
},
_loadTileNR: async function _loadTileNR(j) {
var self = this;
var tile = self.status._tilesforSave[j];
downloadTile(tile.url).then(function (blob) {
self.status.lengthLoaded += 1;
self._saveTile(tile, blob);
self._baseLayer.fire('loadtileend', self.status);
if (self.status.lengthLoaded === self.status.lengthToBeSaved) {
self._baseLayer.fire('loadend', self.status);
}
});
},
/**
* [_saveTile description]
* @private
* @param {object} tileInfo save key
* @param {string} tileInfo.key
* @param {string} tileInfo.url
* @param {string} tileInfo.x
* @param {string} tileInfo.y
* @param {string} tileInfo.z
* @param {blob} blob [description]
* @return {void} [description]
*/
_saveTile: async function _saveTile(tileInfo, blob) {
var self = this;
saveTile(tileInfo, blob)
.then(function () {
self.status.lengthSaved += 1;
self._baseLayer.fire('savetileend', self.status);
if (self.status.lengthSaved === self.status.lengthToBeSaved) {
self._baseLayer.fire('saveend', self.status);
self.setStorageSize();
}
})
.catch(function (err) {
throw new Error(err);
});
},
_rmTiles: function _rmTiles() {
var self = this;
var successCallback = function () {
truncate().then(function () {
self.status.storagesize = 0;
self._baseLayer.fire('tilesremoved');
self._baseLayer.fire('storagesize', self.status);
});
};
if (this.options.confirmRemoval) {
this.options.confirmRemoval(this.status, successCallback);
} else {
successCallback();
}
},
}
);
/**
* @function L.control.savetiles
* @param {object} baseLayer {@link http://leafletjs.com/reference-1.2.0.html#tilelayer}
* @property {Object} options
* @property {string} [options.position] default topleft
* @property {string} [options.saveText] html for save button, default +
* @property {string} [options.rmText] html for remove button, deflault -
* @property {number} [options.maxZoom] maximum zoom level that will be reached
* when saving tiles with saveWhatYouSee. Default 19
* @property {boolean} [options.saveWhatYouSee] save the tiles that you see
* on screen plus deeper zooms, ignores zoomLevels options. Default false
* @property {function} [options.confirm] function called before confirm, default null.
* Args of function are ControlStatus and callback.
* @property {function} [options.confirmRemoval] function called before confirm, default null
* @return {ControlSaveTiles}
*/
L.control.savetiles = function (baseLayer, options) { return new ControlSaveTiles(baseLayer, options); };
exports.getStorageInfo = getStorageInfo;
exports.getStorageLength = getStorageLength;
exports.getStoredTilesAsJson = getStoredTilesAsJson;
exports.getTileUrls = getTileUrls;
exports.removeTile = removeTile;
exports.truncate = truncate;
Object.defineProperty(exports, '__esModule', { value: true });
})));
</script>
<script>
/* global L,LeafletOffline, $ */
const urlTemplate = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
function showTileList() {
LeafletOffline.getStorageInfo(urlTemplate).then((r) => {
const list = document.getElementById('tileinforows');
list.innerHTML = '';
for (let i = 0; i < r.length; i += 1) {
const createdAt = new Date(r[i].createdAt);
list.insertAdjacentHTML(
'beforeend',
`<tr><td>${i}</td><td>${r[i].url}</td><td>${
r[i].key
}</td><td>${createdAt.toDateString()}</td></tr>`,
);
}
});
}
$('#storageModal').on('show.bs.modal', () => {
showTileList();
});
const map = L.map('map');
// offline baselayer, will use offline source if available
const baseLayer = L.tileLayer
.offline(urlTemplate, {
attribution: 'Map data {attribution.OpenStreetMap}',
subdomains: 'abc',
minZoom: 13,
})
.addTo(map);
// add buttons to save tiles in area viewed
const control = L.control.savetiles(baseLayer, {
zoomlevels: [13, 16], // optional zoomlevels to save, default current zoomlevel
confirm(layer, succescallback) {
// eslint-disable-next-line no-alert
if (window.confirm(`Save ${layer._tilesforSave.length}`)) {
succescallback();
}
},
confirmRemoval(layer, successCallback) {
// eslint-disable-next-line no-alert
if (window.confirm('Remove all the tiles?')) {
successCallback();
}
},
saveText:
'<i class="fa fa-download" aria-hidden="true" title="Save tiles"></i>',
rmText: '<i class="fa fa-trash" aria-hidden="true" title="Remove tiles"></i>',
});
control.addTo(map);
map.setView(
{
lat: 51.985,
lng: 5,
},
16,
);
// layer switcher control
const layerswitcher = L.control
.layers({
'osm (offline)': baseLayer,
})
.addTo(map);
let storageLayer;
const addStorageLayer = () => {
LeafletOffline.getStoredTilesAsJson(baseLayer).then((data) => {
storageLayer = L.geoJSON(data).bindPopup(
(clickedLayer) => clickedLayer.feature.properties.key,
);
layerswitcher.addOverlay(storageLayer, 'stored tiles');
});
};
addStorageLayer();
document.getElementById('remove_tiles').addEventListener('click', () => {
control._rmTiles();
});
baseLayer.on('storagesize', (e) => {
document.getElementById('storage').innerHTML = e.storagesize;
if (storageLayer) {
storageLayer.clearLayers();
LeafletOffline.getStoredTilesAsJson(baseLayer).then((data) => {
storageLayer.addData(data);
});
}
});
// events while saving a tile layer
let progress, startTime;
baseLayer.on('savestart', (e) => {
progress = 0;
document.getElementById('total').innerHTML = e._tilesforSave.length;
startTime = new Date();
});
baseLayer.on('savetileend', () => {
progress += 1;
document.getElementById('progress').innerHTML = progress;
if (document.getElementById('total').innerHTML == document.getElementById('progress').innerHTML) {
var endTime = new Date();
var timeDiff = endTime - startTime; //in ms
timeDiff /= 1000; // strip the ms
var seconds = Math.round(timeDiff);
document.getElementById('timer').innerHTML = '<b>' + seconds + ' sec</b>';
//console.log(seconds + " seconds");
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment