UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

322 lines (321 loc) 12.4 kB
import GirafeSingleton from '../../base/GirafeSingleton'; import MapManager from '../state/mapManager'; import StateManager from '../state/statemanager'; import { Feature } from 'ol'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import { fromExtent } from 'ol/geom/Polygon'; import Style from 'ol/style/Style'; import { Stroke } from 'ol/style'; import ConfigManager from '../configuration/configmanager'; class OfflineManager extends GirafeSingleton { get state() { return this.stateManager.state; } constructor(type) { super(type); Object.defineProperty(this, "serviceWorker", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "database", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "map", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "stateManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "configManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "totalLength", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "counter", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "progressCallback", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "storeVersion", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "dbCacheName", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "tilesStoreName", { enumerable: true, configurable: true, writable: true, value: 'tiles' }); Object.defineProperty(this, "bboxStoreName", { enumerable: true, configurable: true, writable: true, value: 'bbox' }); Object.defineProperty(this, "vectorLayer", { enumerable: true, configurable: true, writable: true, value: new VectorLayer({ style: new Style({ stroke: new Stroke({ color: 'red', width: 5 }) }) }) }); this.stateManager = StateManager.getInstance(); this.configManager = ConfigManager.getInstance(); this.map = MapManager.getInstance().getMap(); this.map.addLayer(this.vectorLayer); } initializeOfflineState(isOffline) { this.registerEvents(); this.stateManager.state.isOffline = isOffline; } registerEvents() { window.addEventListener('offline', () => { this.state.isOffline = true; }); window.addEventListener('online', () => { this.state.isOffline = false; }); this.stateManager.subscribe('isOffline', () => { this.switchOffline(); }); } /** Exports all the WMTS tiles for the layers in parameter and store them to local cache */ async exportWMTSTiles(bbox, wmtsLayers, progressCallback) { this.progressCallback = progressCallback; await this.saveBoundingBox(bbox); const tileUrls = this.getAllTileUrls(bbox, wmtsLayers); await this.fetchAndSaveTiles(tileUrls); } async openIndexedDB() { return new Promise((resolve, reject) => { console.debug('Opening IndexedDB'); const request = indexedDB.open(this.dbCacheName, this.storeVersion); let timedOut = false; const timeoutId = setTimeout(() => { timedOut = true; console.debug('Timeout while opening IndexedDB'); reject(new Error('Timeout while opening IndexedDB')); }, 3000); request.onerror = (event) => { if (!timedOut) { clearTimeout(timeoutId); const message = event.target.error?.message; console.debug(`IndexedDB could not be opened : ${message}`); reject(new Error(message)); } }; request.onsuccess = (event) => { if (!timedOut) { clearTimeout(timeoutId); console.debug('IndexedDB is open'); resolve(event.target.result); } }; request.onupgradeneeded = (event) => { // Version migration is necessary. // open the indexedDB and create the new structure console.debug('Upgrading IndexedDB'); const database = event.target.result; // First : a store for tiles const tilesStore = database.createObjectStore('tiles', { autoIncrement: true }); tilesStore.createIndex('url', 'url', { unique: true }); // Second : A store for offline available bbox database.createObjectStore('bbox', { autoIncrement: true }); console.debug('IndexedDB upgraded.'); resolve(database); }; }); } /** The offline manager works with a ServiceWorker in charge of intercepting * the fetch requests and read the data from the local cache if the application * is offline. This method defines the servicework object to use. * Without this, the offline mode won't work. */ async setServiceWorker(sw, storeVersion, dbCacheName) { if (!sw) { console.warn("ServiceWorker cannot be initialized. Offline functionalities won't be available."); return; } this.serviceWorker = sw; this.storeVersion = storeVersion; this.dbCacheName = dbCacheName; this.database = await this.openIndexedDB(); await this.configManager.loadConfig(); this.serviceWorker.postMessage({ storeVersion: this.storeVersion, dbCacheName: this.dbCacheName, tilesStoreName: this.tilesStoreName }); } switchOffline() { if (this.stateManager.state.isOffline) { this.displayBoundBoxes(); } else if (this.vectorLayer) { this.vectorLayer.setSource(null); } } getAllTileUrls(bbox, wmtsLayers) { const tileUrls = []; for (const wmtsLayer of wmtsLayers) { const layerSource = wmtsLayer._olayer?.getSource(); if (layerSource) { const tileGrid = layerSource.getTileGrid(); if (tileGrid) { tileUrls.push(...this.getTileUrlsForWmtsLayer(tileGrid, bbox, layerSource)); } } } return tileUrls; } getTileUrlsForWmtsLayer(tileGrid, bbox, layerSource) { const minZoom = tileGrid.getMinZoom(); const maxZoom = ConfigManager.getInstance().Config.offline?.downloadEndZoom; if (!maxZoom) { throw new Error('Offline configuration is missing. Cannot download maps for offline usage.'); } const projection = layerSource.getProjection(); const tileUrls = []; for (let z = minZoom; z <= maxZoom; z++) { const tileRange = tileGrid.getTileRangeForExtentAndZ(bbox, z); for (let x = tileRange.minX; x <= tileRange.maxX; ++x) { for (let y = tileRange.minY; y <= tileRange.maxY; ++y) { const tileUrl = layerSource.getTileUrlFunction()([z, x, y], window.devicePixelRatio, projection); if (tileUrl) { tileUrls.push(tileUrl); } } } } return tileUrls; } async fetchAndSaveTiles(tileUrls) { this.totalLength = tileUrls.length; console.debug(`Number of Tile to load: ${this.totalLength}`); const iterator = tileUrls.values(); // Use 4 parallel workers to download tiles this.counter = 0; const workers = Array(4) .fill(iterator) .map((iterator) => this.doWork(iterator)); Promise.allSettled(workers).then(() => { console.debug('Everything downloaded.'); if (this.progressCallback) { // Last info : 100% done this.progressCallback(100); } }); } async doWork(iterator) { for (const url of iterator) { const response = await fetch(url); if (response.ok) { response.blob().then((blob) => { const transaction = this.database.transaction([this.tilesStoreName], 'readwrite'); const store = transaction.objectStore(this.tilesStoreName); const index = store.index('url'); const dbRequest = index.getKey(url); dbRequest.onsuccess = () => { let request; if (dbRequest.result) { const key = dbRequest.result; request = store.put({ url: url, data: blob }, key); } else { request = store.put({ url: url, data: blob }); } request.onsuccess = () => { if (this.progressCallback) { this.progressCallback(Math.round((this.counter * 100) / this.totalLength)); } console.debug(`${this.counter++}/${this.totalLength} Tile ${url} added.`); }; request.onerror = () => { console.error(`Error while saving tile ${url}`); }; }; }); } } } async saveBoundingBox(bbox) { if (this.database === undefined) { this.database = await this.openIndexedDB(); } // Save bbox to the store const transaction = this.database.transaction([this.bboxStoreName], 'readwrite'); const store = transaction.objectStore(this.bboxStoreName); const request = store.put(bbox); request.onsuccess = () => { console.debug('BBOX Added to store'); }; request.onerror = () => { console.error('Error while saving bbox'); }; } async displayBoundBoxes() { if (this.database === undefined) { this.database = await this.openIndexedDB(); } // Read bbox for offline tiles const transaction = this.database.transaction([this.bboxStoreName], 'readonly'); const store = transaction.objectStore(this.bboxStoreName); const request = store.getAll(); request.onsuccess = () => { const vectorSource = new VectorSource(); if (request.result) { for (const bbox of request.result) { const feature = new Feature({ geometry: fromExtent(bbox) }); vectorSource.addFeature(feature); } } this.vectorLayer.setSource(vectorSource); }; request.onerror = () => { console.error('Error while saving bbox'); }; } } export default OfflineManager;