From 46476429e520c632ea3f08c94ac9ecda26e419f9 Mon Sep 17 00:00:00 2001 From: Guillaume Beraudo Date: Thu, 1 Nov 2018 16:19:10 +0100 Subject: [PATCH 1/8] Port Offline feature from older ngeo version - port the ngeo code to ES6 modules; - port the example. Note that the example is actually broken: OSM layers have never been supported by the offline service (we support WMS and WMTS). --- examples/offline.html | 21 + examples/offline.js | 87 ++++ examples/offline.less | 74 ++++ src/offline/Configuration.js | 316 ++++++++++++++ src/offline/Downloader.js | 157 +++++++ src/offline/Mode.js | 86 ++++ src/offline/NetworkStatus.js | 212 ++++++++++ src/offline/Restorer.js | 66 +++ src/offline/SerializerDeserializer.js | 222 ++++++++++ src/offline/ServiceManager.js | 107 +++++ src/offline/TilesDownloader.js | 196 +++++++++ src/offline/component.html | 129 ++++++ src/offline/component.js | 587 ++++++++++++++++++++++++++ src/offline/module.js | 26 ++ src/offline/utils.js | 34 ++ src/utils.js | 16 +- 16 files changed, 2335 insertions(+), 1 deletion(-) create mode 100644 examples/offline.html create mode 100644 examples/offline.js create mode 100644 examples/offline.less create mode 100644 src/offline/Configuration.js create mode 100644 src/offline/Downloader.js create mode 100644 src/offline/Mode.js create mode 100644 src/offline/NetworkStatus.js create mode 100644 src/offline/Restorer.js create mode 100644 src/offline/SerializerDeserializer.js create mode 100644 src/offline/ServiceManager.js create mode 100644 src/offline/TilesDownloader.js create mode 100644 src/offline/component.html create mode 100644 src/offline/component.js create mode 100644 src/offline/module.js create mode 100644 src/offline/utils.js diff --git a/examples/offline.html b/examples/offline.html new file mode 100644 index 000000000000..b43760221132 --- /dev/null +++ b/examples/offline.html @@ -0,0 +1,21 @@ + + + + Offline example + + + + + + +
You are currently offline.
+
+ + +
+

This example shows how to use the ngeo-offline component.

+ + diff --git a/examples/offline.js b/examples/offline.js new file mode 100644 index 000000000000..6aea4faa1bb8 --- /dev/null +++ b/examples/offline.js @@ -0,0 +1,87 @@ +/** + * @module app.offline + */ +const exports = {}; + +import './offline.less'; +import './common_dependencies.js'; +import olMap from 'ol/Map.js'; + +import olView from 'ol/View.js'; +import olLayerTile from 'ol/layer/Tile.js'; +import olSourceOSM from 'ol/source/OSM.js'; +import ngeoMapModule from 'ngeo/map/module.js'; +import ngeoOfflineModule from 'ngeo/offline/module.js'; +import ngeoOfflineConfiguration from 'ngeo/offline/Configuration.js'; +import NgeoOfflineServiceManager from 'ngeo/offline/ServiceManager.js'; + + +// Useful to work on example - remove me later +import 'bootstrap/js/modal.js'; +import 'jquery-ui/ui/widgets/resizable.js'; +import 'jquery-ui/ui/widgets/draggable.js'; + +/** @type {!angular.Module} **/ +exports.module = angular.module('app', [ + 'gettext', + ngeoMapModule.name, + ngeoOfflineModule.name, + NgeoOfflineServiceManager.module.name, +]); + +exports.module.value('ngeoOfflineTestUrl', '../../src/offline/component.html'); + +// Define the offline download configuration service +ngeoOfflineModule.service('ngeoOfflineConfiguration', ngeoOfflineConfiguration); + +class MainController { + + /** + * @param {ngeoFeatureOverlayMgr} ngeoFeatureOverlayMgr ngeo feature overlay manager service. + * @param {ngeoNetworkStatus} ngeoNetworkStatus ngeo network status service. + * @param {NgeoOfflineServiceManager} ngeoOfflineServiceManager ngeo offline service. + * @ngInject + */ + constructor(ngeoFeatureOverlayMgr, ngeoNetworkStatus, ngeoOfflineServiceManager) { + + /** + * Save a square of 10 km sideways (Map's unit is the meter). + * @type {number} + * @export + */ + this.offlineExtentSize = 10000; + + /** + * @type {ngeoNetworkStatus} + * @export + */ + this.ngeoNetworkStatus = ngeoNetworkStatus; + + /** + * @type {ol.Map} + * @export + */ + this.map = new olMap({ + layers: [ + new olLayerTile({ + source: new olSourceOSM() + }) + ], + view: new olView({ + center: [352379, 5172733], + zoom: 4 + }) + }); + + ngeoFeatureOverlayMgr.init(this.map); + + ngeoOfflineServiceManager.setSaveService('offlineDownloader'); + ngeoOfflineServiceManager.setRestoreService('ngeoOfflineRestorer'); + } +} + + +exports.module.controller('MainController', MainController); + + +export default exports; diff --git a/examples/offline.less b/examples/offline.less new file mode 100644 index 000000000000..39dff172ae64 --- /dev/null +++ b/examples/offline.less @@ -0,0 +1,74 @@ +@import "~bootstrap/dist/css/bootstrap.css"; +@import "~font-awesome/less/font-awesome.less"; + +#map { + width: 600px; + height: 400px; + position: relative; +} + +ngeo-offline { + div { + z-index: 1; + } + .main-button { + position: absolute; + right: 1rem; + bottom: 5rem; + cursor: pointer; + .no-data { + color: black; + } + .with-data { + color: red; + } + .no-data, .with-data { + background-color: white; + text-align: center; + font-size: 2.5rem; + line-height: 2rem; + border-radius: 2rem; + font-family: FontAwesome; + &::after { + content: @fa-var-arrow-circle-o-down; + } + } + } + + .validate-extent { + position: absolute; + bottom: 0.5rem; + width: 10rem; + left: calc(~"50% - 5rem"); + } + + .in-progress { + position: absolute; + left: calc(~"50% - 3.3rem"); + top: calc(~"50% - 3rem"); + color: white; + font-weight: bold; + background-color: #337ab7; + padding: 2rem 2rem; + border-radius: 1rem; + } + + .modal-content { + width: 30rem; + } + + .modal-body { + button { + display: block; + margin: 0.5rem auto; + width: 25rem; + } + } +} + +.offline-msg { + display: none; +} +.offline .offline-msg { + display: block; +} diff --git a/src/offline/Configuration.js b/src/offline/Configuration.js new file mode 100644 index 000000000000..f0d6779ee1d0 --- /dev/null +++ b/src/offline/Configuration.js @@ -0,0 +1,316 @@ +/** + * @module ngeo.offline.Configuration + */ +import olObservable from 'ol/Observable.js'; +import olLayerLayer from 'ol/layer/Layer.js'; +import olLayerVector from 'ol/layer/Vector.js'; +import olLayerTile from 'ol/layer/Tile.js'; +import olLayerImage from 'ol/layer/Image.js'; +import * as olProj from 'ol/proj.js'; +import olSourceImage from 'ol/source/Image.js'; +import olSourceImageWMS from 'ol/source/ImageWMS.js'; +import olSourceTileWMS from 'ol/source/TileWMS.js'; +import {createForProjection as createTileGridForProjection} from 'ol/tilegrid.js'; +import SerializerDeserializer from 'ngeo/offline/SerializerDeserializer.js'; +import ngeoCustomEvent from 'ngeo/CustomEvent.js'; +import utils from 'ngeo/offline/utils.js'; +const defaultImageLoadFunction = olSourceImage.defaultImageLoadFunction; + + +/** + * @implements {ngeox.OfflineOnTileDownload} + */ +const exports = class extends olObservable { + + /** + * @ngInject + * @param {!angular.Scope} $rootScope The rootScope provider. + * @param {ngeo.map.BackgroundLayerMgr} ngeoBackgroundLayerMgr + * @param {number} ngeoOfflineGutter + */ + constructor($rootScope, ngeoBackgroundLayerMgr, ngeoOfflineGutter) { + super(); + localforage.config({ + 'name': 'ngeoOfflineStorage', + 'version': 1.0, + 'storeName': 'offlineStorage' + }); + /** + * @param {number} progress new progress. + */ + this.dispatchProgress_ = (progress) => { + this.dispatchEvent(new ngeoCustomEvent('progress', { + 'progress': progress + })); + }; + + /** + * @private + * @type {!angular.Scope} + */ + this.rootScope_ = $rootScope; + + /** + * @protected + * @type {boolean} + */ + this.hasData = false; + this.initializeHasOfflineData(); + + /** + * @private + * @type {ngeo.map.BackgroundLayerMgr} + */ + this.ngeoBackgroundLayerMgr_ = ngeoBackgroundLayerMgr; + + /** + * @private + * @type {ngeo.offline.SerializerDeserializer} + */ + this.serDes_ = new SerializerDeserializer({gutter: ngeoOfflineGutter}); + + /** + * @private + * @type {number} + */ + this.gutter_ = ngeoOfflineGutter; + } + + /** + * @protected + */ + initializeHasOfflineData() { + this.getItem('offline_content').then(value => this.setHasOfflineData(!!value)); + } + + /** + * @return {boolean} whether some offline data is available in the storage + */ + hasOfflineData() { + return this.hasData; + } + + /** + * @param {boolean} value whether there is offline data available in the storage. + */ + setHasOfflineData(value) { + const needDigest = value ^ this.hasData; + this.hasData = value; + if (needDigest) { + this.rootScope_.$applyAsync(); // force update of the UI + } + } + + /** + * Hook to allow measuring get/set item performance. + * @param {string} msg + * @param {string} key + * @param {Promise} promise + * @return {Promise} + */ + traceGetSetItem(msg, key, promise) { + return promise; + } + + /** + * @param {string} key + * @return {Promise} + */ + getItem(key) { + return this.traceGetSetItem('getItem', key, localforage.getItem(key)); + } + + /** + * @param {string} key + * @param {*} value + * @return {Promise} + */ + setItem(key, value) { + return this.traceGetSetItem('setItem', key, localforage.setItem(key, value)); + } + + /** + * @return {Promise} + */ + clear() { + this.setHasOfflineData(false); + return this.traceGetSetItem('clear', '', localforage.clear()); + } + + /** + * @param {!ol.Map} map + * @return {number} + */ + estimateLoadDataSize(map) { + return 50; + } + + /** + * @param {ngeox.OfflineLayerMetadata} layerItem + * @return {string} A key identifying an offline layer and used during restore. + */ + getLayerKey(layerItem) { + return /** @type {string} */ (layerItem.layer.get('label')); + } + + /** + * @override + * @param {number} progress + * @param {ngeox.OfflineTile} tile + * @return {Promise} + */ + onTileDownloadSuccess(progress, tile) { + this.dispatchProgress_(progress); + + if (tile.response) { + return this.setItem(utils.normalizeURL(tile.url), tile.response); + } + return Promise.resolve(); + } + + /** + * @override + * @param {number} progress + * @return {Promise} + */ + onTileDownloadError(progress) { + this.dispatchProgress_(progress); + return Promise.resolve(); + } + + /** + * @param {ol.Map} map + * @param {ol.layer.Layer} layer + * @param {Array} ancestors + * @param {ol.Extent} userExtent The extent selected by the user. + * @return {Array} + */ + getExtentByZoom(map, layer, ancestors, userExtent) { + const currentZoom = map.getView().getZoom(); + // const viewportExtent = map.calculateExtent(map.getSize()); + + const results = []; + [0, 1, 2, 3, 4].forEach((dz) => { + results.push({ + zoom: currentZoom + dz, + extent: userExtent + }); + }); + return results; + } + + /** + * @protected + * @param {ol.source.Source} source + * @param {ol.proj.Projection} projection + * @return {ol.source.Source} + */ + sourceImageWMSToTileWMS(source, projection) { + if (source instanceof olSourceImageWMS && source.getUrl() && source.getImageLoadFunction() === defaultImageLoadFunction) { + const tileGrid = createTileGridForProjection(source.getProjection() || projection, 42, 256); + source = new olSourceTileWMS({ + gutter: this.gutter_, + url: source.getUrl(), + tileGrid: tileGrid, + attributions: source.getAttributions(), + projection: source.getProjection(), + params: source.getParams() + }); + } + return source; + } + + /** + * @param {ol.Map} map The map to work on. + * @param {ol.Extent} userExtent The extent selected by the user. + * @return {!Array} the downloadable layers and metadata. + */ + createLayerMetadatas(map, userExtent) { + const layersItems = []; + + /** + * @param {ol.layer.Base} layer . + * @param {Array} ancestors . + * @return {boolean} whether to traverse this layer children. + */ + const visitLayer = (layer, ancestors) => { + if (layer instanceof olLayerLayer) { + const extentByZoom = this.getExtentByZoom(map, layer, ancestors, userExtent); + const projection = olProj.get(map.getView().getProjection()); + const source = this.sourceImageWMSToTileWMS(layer.getSource(), projection); + let layerType; + let layerSerialization; + if (layer instanceof olLayerTile || layer instanceof olLayerImage) { + layerType = 'tile'; + layerSerialization = this.serDes_.serializeTileLayer(layer, source); + } else if (layer instanceof olLayerVector) { + layerType = 'vector'; + } + + layersItems.push({ + backgroundLayer: this.ngeoBackgroundLayerMgr_.get(map) === layer, + map, + extentByZoom, + layerType, + layerSerialization, + layer, + source, + ancestors + }); + } + return true; + }; + map.getLayers().forEach((root) => { + utils.traverseLayer(root, [], visitLayer); + }); + return layersItems; + } + + /** + * @private + * @param {ngeox.OfflinePersistentLayer} offlineLayer + * @return {function(ol.ImageTile, string)} + */ + createTileLoadFunction_(offlineLayer) { + const that = this; + /** + * Load the tile from persistent storage. + * @param {ol.ImageTile} imageTile + * @param {string} src + */ + const tileLoadFunction = function(imageTile, src) { + that.getItem(utils.normalizeURL(src)).then((content) => { + if (!content) { + // use a transparent 1x1 image to make the map consistent + content = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; + } + imageTile.getImage().src = content; + }); + }; + return tileLoadFunction; + } + + /** + * @param {ngeox.OfflinePersistentLayer} offlineLayer + * @return {ol.layer.Layer} the layer. + */ + recreateOfflineLayer(offlineLayer) { + if (offlineLayer.layerType === 'tile') { + const serialization = offlineLayer.layerSerialization; + const tileLoadFunction = this.createTileLoadFunction_(offlineLayer); + const layer = this.serDes_.deserializeTileLayer(serialization, tileLoadFunction); + return layer; + } + return null; + } + + /** + * @return {number} + */ + getMaxNumberOfParallelDownloads() { + return 11; + } +}; + + +export default exports; diff --git a/src/offline/Downloader.js b/src/offline/Downloader.js new file mode 100644 index 000000000000..675cbd622eb6 --- /dev/null +++ b/src/offline/Downloader.js @@ -0,0 +1,157 @@ +/** + * @module ngeo.offline.Downloader + */ +import {DEVICE_PIXEL_RATIO} from 'ol/has.js'; +import googAsserts from 'goog/asserts.js'; +import olSourceTileWMS from 'ol/source/TileWMS.js'; +import olSourceWMTS from 'ol/source/WMTS.js'; +import TilesDownloader from 'ngeo/offline/TilesDownloader.js'; + + +/** + * @param {ol.Coordinate} a Some coordinates. + * @param {ol.Coordinate} b Some other coordinates. + * @return {number} The squared magnitude. + */ +function magnitude2(a, b) { + let magnitudeSquared = 0; + for (let i = 0; i < a.length; ++i) { + magnitudeSquared += Math.pow(a[1] - b[1], 2); + } + return magnitudeSquared; +} + + +const Downloader = class { + + /** + * @ngInject + * @param {ngeo.offline.Configuration} ngeoOfflineConfiguration A service for customizing offline behaviour. + */ + constructor(ngeoOfflineConfiguration) { + /** + * @private + * @type {ngeo.offline.Configuration} + */ + this.configuration_ = ngeoOfflineConfiguration; + + /** + * @type {TilesDownloader} + * @private + */ + this.tileDownloader_ = null; + } + + cancel() { + this.tileDownloader_.cancel(); + } + + /** + * @param {ngeox.OfflineLayerMetadata} layerMetadata Layers metadata. + * @param {Array} queue Queue of tiles to download. + */ + queueLayerTiles_(layerMetadata, queue) { + const {map, source, extentByZoom} = layerMetadata; + + if (!source) { + return; + } + googAsserts.assert(source instanceof olSourceTileWMS || source instanceof olSourceWMTS); + const projection = map.getView().getProjection(); + const tileGrid = source.getTileGrid(); + const tileUrlFunction = source.getTileUrlFunction(); + + googAsserts.assert(extentByZoom); + for (const extentZoom of extentByZoom) { + const z = extentZoom.zoom; + const extent = extentZoom.extent; + const queueByZ = []; + let minX, minY, maxX, maxY; + tileGrid.forEachTileCoord(extent, z, (coord) => { + maxX = coord[1]; + maxY = coord[2]; + if (minX === undefined) { + minX = coord[1]; + minY = coord[2]; + } + const url = tileUrlFunction(coord, DEVICE_PIXEL_RATIO, projection); + googAsserts.assert(url); + + /** + * @type {ngeox.OfflineTile} + */ + const tile = {coord, url}; + queueByZ.push(tile); + }); + + const centerTileCoord = [z, (minX + maxX) / 2, (minY + maxY) / 2]; + queueByZ.sort((a, b) => magnitude2(a.coord, centerTileCoord) - magnitude2(b.coord, centerTileCoord)); + queue.push(...queueByZ); + } + } + + /** + * @param {ol.Extent} extent The extent to download. + * @param {ol.Map} map The map to work on. + * @return {Promise} A promise resolving when save is finished. + */ + save(extent, map) { + /** + * @type {!Array} + */ + const layersMetadatas = this.configuration_.createLayerMetadatas(map, extent); + + /** + * @type {!Array} + */ + const persistentLayers = []; + const queue = []; + const zooms = []; + for (const layerItem of layersMetadatas) { + if (layerItem.layerType === 'tile') { + const tiles = []; + this.queueLayerTiles_(layerItem, tiles); + queue.push(...tiles); + } + persistentLayers.push({ + backgroundLayer: layerItem.backgroundLayer, + layerType: layerItem.layerType, + layerSerialization: layerItem.layerSerialization, + key: this.configuration_.getLayerKey(layerItem), + }); + + layerItem.extentByZoom.forEach((obj) => { + const zoom = obj.zoom; + if (zooms.indexOf(zoom) < 0) { + zooms.push(zoom); + } + }); + } + + /** + * @type {ngeox.OfflinePersistentContent} + */ + const persistentObject = { + extent: extent, + layers: persistentLayers, + zooms: zooms.sort((a, b) => (a < b ? -1 : 1)) + }; + const setOfflineContentPromise = this.configuration_.setItem('offline_content', persistentObject); + + this.tileDownloader_ = new TilesDownloader(queue, this.configuration_, this.configuration_.getMaxNumberOfParallelDownloads()); + const tileDownloadPromise = this.tileDownloader_.download(); + + const allPromise = Promise.all([setOfflineContentPromise, tileDownloadPromise]); + const setHasOfflineData = () => this.configuration_.setHasOfflineData(true); + allPromise.then(setHasOfflineData, setHasOfflineData); + return allPromise; + } +}; + +const name = 'offlineDownloader'; +Downloader.module = angular.module(name, []).service(name, Downloader); + +const exports = Downloader; + + +export default exports; diff --git a/src/offline/Mode.js b/src/offline/Mode.js new file mode 100644 index 000000000000..a8c558220650 --- /dev/null +++ b/src/offline/Mode.js @@ -0,0 +1,86 @@ +/** + * @module ngeo.offline.Mode + */ + +const exports = class { + + /** + * @param {ngeo.offline.Configuration} ngeoOfflineConfiguration ngeo offline configuration service. + * @ngInject + * @ngdoc service + * @ngname ngeoOfflineState + */ + constructor(ngeoOfflineConfiguration) { + + /** + * Offline mode is enabled or not. + * @type {boolean} + * @private + */ + this.enabled_ = false; + + /** + * Offline component. + * @type {ngeo.offline.component.Controller|undefined} + * @private + */ + this.component_; + + /** + * @private + * @type {ngeo.offline.Configuration} + */ + this.ngeoOfflineConfiguration_ = ngeoOfflineConfiguration; + } + + /** + * Return if we are in offline mode. + * @return {boolean} + * @export + */ + isEnabled() { + return this.enabled_; + } + + /** + * Enable offline mode. ATM we cannot escape from the offline mode. + * @export + */ + enable() { + this.enabled_ = true; + } + + /** + * + * @param {ngeo.offline.component.Controller} component Offline component. + * @export + */ + registerComponent(component) { + this.component_ = component; + } + + /** + * @export + */ + activateOfflineMode() { + this.component_.activateOfflineMode(); + } + + /** + * @return {boolean} True if data are accessible offline. + * @export + */ + hasData() { + return this.ngeoOfflineConfiguration_.hasOfflineData(); + } + +}; + +/** + * @type {!angular.Module} + */ +exports.module = angular.module('ngeoOfflineMode', []); +exports.module.service('ngeoOfflineMode', exports); + + +export default exports; diff --git a/src/offline/NetworkStatus.js b/src/offline/NetworkStatus.js new file mode 100644 index 000000000000..71ed69c0c677 --- /dev/null +++ b/src/offline/NetworkStatus.js @@ -0,0 +1,212 @@ +/** + * @module ngeo.offline.NetworkStatus + */ +import ngeoMiscDebounce from 'ngeo/misc/debounce.js'; + + +/** + * @ngInject + * @param {angular.$q} $q The Angular $q service. + * @param {ngeox.miscDebounce} ngeoDebounce ngeo debounce service. + * @param {ngeo.offline.NetworkStatus} ngeoNetworkStatus ngeo network status service. + * @return {angular.$http.Interceptor} the interceptor + */ +const httpInterceptor = function($q, ngeoDebounce, ngeoNetworkStatus) { + const debouncedCheck = ngeoDebounce(() => ngeoNetworkStatus.check(undefined), 2000, false); + return { + request(config) { + return config; + }, + requestError(rejection) { + return $q.reject(rejection); + }, + response(response) { + return response; + }, + responseError(rejection) { + debouncedCheck(); + return $q.reject(rejection); + } + }; +}; + +const Service = class { + + /** + * This service watches the status of network connection. + * + * Currently it watches every $http and $.ajax requests errors, if an error + * occurs we wait 2 sec then we make an http request on the checker file. + * If the checker responds that means we are online, otherwise we make a + * 2nd request 2 sec later, if the 2nd requests failed that means we + * are offline. + * + * A timeout of 1 sec is set for the checker file, so if we have a bad + * connection, we consider we are offline. + * + * During offline mode we test every 2 sec if we are back online. + * + * @ngInject + * @param {!jQuery} $document Angular document service. + * @param {angular.$window} $window Angular window service. + * @param {!angular.$timeout} $timeout Angular timeout service. + * @param {angular.Scope} $rootScope The root scope. + * @param {string} ngeoOfflineTestUrl Url of the test page. + */ + constructor($document, $window, $timeout, $rootScope, ngeoOfflineTestUrl) { + + /** + * @private + * @type {!jQuery} + */ + this.$document_ = $document; + + /** + * @private + * @type {!Window} + */ + this.$window_ = $window; + + /** + * @private + * @type {!angular.$timeout} + */ + this.$timeout_ = $timeout; + + /** + * @private + * @type {angular.Scope} + */ + this.$rootScope_ = $rootScope; + + /** + * @private + * @type {string} + */ + this.ngeoOfflineTestUrl_ = ngeoOfflineTestUrl; + + /** + * @private + * @type {!number} + */ + this.count_ = 0; + + /** + * @type {!boolean|undefined} + * @private + */ + this.offline_; + + /** + * @private + * @type {angular.$q.Promise|undefined} + */ + this.promise_; + + this.initialize_(); + + } + + initialize_() { + this.offline_ = !this.$window_.navigator.onLine; + + // airplane mode, works offline(firefox) + this.$window_.addEventListener('offline', () => { + this.triggerChangeStatusEvent_(true); + }); + + // online event doesn't means we have a internet connection, that means we + // have possiby one (connected to a router ...) + this.$window_.addEventListener('online', () => { + this.check(undefined); + }); + + // We catch every $.ajax request errors or (canceled request). + this.$document_.ajaxError((evt, jqxhr, settings, thrownError) => { + // Filter out canceled requests + if (!/^(canceled|abort)$/.test(thrownError)) { + this.check(2000); + } + }); + + } + + /** + * Check fir network status + * + * @param {number=} timeout Delay for timeout. + */ + check(timeout) { + if (this.promise_) { + this.$timeout_.cancel(this.promise_); + this.promise_ = undefined; + } + if (timeout !== undefined) { + this.count_++; + this.promise_ = this.$timeout_(() => this.check(), timeout); + return; + } + $.ajax({ + method: 'GET', + url: this.ngeoOfflineTestUrl_, + timeout: 1000, + success: () => { + this.count_ = 0; + if (this.offline_) { + this.triggerChangeStatusEvent_(false); + } + }, + error: () => { + this.count_++; + // We consider we are offline after 3 requests failed + if (this.count_ > 2 && !this.offline_) { + this.triggerChangeStatusEvent_(true); + } + } + }); + + } + + /** + * @param {boolean} offline whether it's offline or not. + * @private + */ + triggerChangeStatusEvent_(offline) { + this.offline_ = offline; + // this.$rootScope_.$broadcast('ngeoNetworkStatusChange', net.offline); + this.$rootScope_.$digest(); + } + + /** + * @return {boolean} True if we are disconnected. + * @export + */ + isDisconnected() { + return !!this.offline_; + } +}; + +const name = 'ngeoNetworkStatus'; + +Service.module = angular.module(name, [ + ngeoMiscDebounce.name +]); +Service.module.factory('httpInterceptor', httpInterceptor); +Service.module.service(name, Service); + +/** + * @ngInject + * @private + * @param {angular.$HttpProvider} $httpProvider . + */ +Service.module.configFunction_ = function($httpProvider) { + $httpProvider.interceptors.push('httpInterceptor'); +}; +Service.module.config(Service.module.configFunction_); + +Service.module.value('ngeoOfflineTestUrl', ''); + +const exports = Service; + + +export default exports; diff --git a/src/offline/Restorer.js b/src/offline/Restorer.js new file mode 100644 index 000000000000..19691fbd2242 --- /dev/null +++ b/src/offline/Restorer.js @@ -0,0 +1,66 @@ +/** + * @module ngeo.offline.Restorer + */ +import ngeoMapBackgroundLayerMgr from 'ngeo/map/BackgroundLayerMgr.js'; + + +class Restorer { + + /** + * @ngInject + * @param {ngeo.offline.Configuration} ngeoOfflineConfiguration A service for customizing offline behaviour. + * @param {ngeo.map.BackgroundLayerMgr} ngeoBackgroundLayerMgr + */ + constructor(ngeoOfflineConfiguration, ngeoBackgroundLayerMgr) { + /** + * @private + * @type {ngeo.offline.Configuration} + */ + this.configuration_ = ngeoOfflineConfiguration; + + /** + * @private + * @type {ngeo.map.BackgroundLayerMgr} + */ + this.ngeoBackgroundLayerMgr_ = ngeoBackgroundLayerMgr; + } + + /** + * @public + * @param {ol.Map} map + * @return {Promise} + */ + restore(map) { + return this.configuration_.getItem('offline_content').then(offlineContent => this.doRestore(map, offlineContent)); + } + + /** + * @protected + * @param {ol.Map} map + * @param {ngeox.OfflinePersistentContent} offlineContent + * @return {ol.Extent} + */ + doRestore(map, offlineContent) { + map.getLayerGroup().getLayers().clear(); + for (const offlineLayer of offlineContent.layers) { + const layer = this.configuration_.recreateOfflineLayer(offlineLayer); + if (layer) { + map.addLayer(layer); + if (offlineLayer.backgroundLayer) { + this.ngeoBackgroundLayerMgr_.set(map, layer); + } + } + } + return offlineContent.extent; + } +} + +const name = 'ngeoOfflineRestorer'; +Restorer.module = angular.module(name, [ + ngeoMapBackgroundLayerMgr.module.name +]).service(name, Restorer); + +const exports = Restorer; + + +export default exports; diff --git a/src/offline/SerializerDeserializer.js b/src/offline/SerializerDeserializer.js new file mode 100644 index 000000000000..56b2ccd3786c --- /dev/null +++ b/src/offline/SerializerDeserializer.js @@ -0,0 +1,222 @@ +/** + * @module ngeo.offline.SerializerDeserializer + */ +import olTilegridTileGrid from 'ol/tilegrid/TileGrid.js'; +import olTilegridWMTS from 'ol/tilegrid/WMTS.js'; +import * as olProj from 'ol/proj.js'; +import olSourceTileWMS from 'ol/source/TileWMS.js'; +import olSourceWMTS from 'ol/source/WMTS.js'; +import olLayerTile from 'ol/layer/Tile.js'; + + +const SerDes = class { + /** + * @param {Object} options + */ + constructor({gutter}) { + /** + * @private + */ + this.gutter_ = gutter; + } + + /** + * @private + * @param {ol.Object} olObject + * @return {Object} + */ + createBaseObject_(olObject) { + const properties = olObject.getProperties(); + const obj = {}; + for (const key in properties) { + const value = properties[key]; + const typeOf = typeof value; + if (typeOf === 'string' || typeOf === 'number') { + obj[key] = value; + } + } + return obj; + } + + /** + * @param {ol.tilegrid.TileGrid} tilegrid + * @return {string} + */ + serializeTilegrid(tilegrid) { + const obj = {}; + obj['extent'] = tilegrid.getExtent(); + obj['minZoom'] = tilegrid.getMinZoom(); + obj['origin'] = tilegrid.getOrigin(0); // hack + obj['resolutions'] = tilegrid.getResolutions(); + obj['tileSize'] = tilegrid.getTileSize(tilegrid.getMinZoom()); + return JSON.stringify(obj); + } + + /** + * @param {string} serialization + * @return {ol.tilegrid.TileGrid} tilegrid + */ + deserializeTilegrid(serialization) { + const options = /** @type {olx.tilegrid.TileGridOptions} */ (JSON.parse(serialization)); + return new olTilegridTileGrid(options); + } + + /** + * @param {ol.tilegrid.WMTS} tilegrid + * @return {string|undefined} + */ + serializeTilegridWMTS(tilegrid) { + if (!tilegrid) { + return undefined; + } + const obj = {}; + const resolutions = tilegrid.getResolutions(); + obj['extent'] = tilegrid.getExtent(); + obj['minZoom'] = tilegrid.getMinZoom(); + obj['matrixIds'] = tilegrid.getMatrixIds(); + obj['resolutions'] = resolutions; + + obj['origins'] = []; + for (let z = 0; z < resolutions.length; ++z) { + obj['origins'].push(tilegrid.getOrigin(z)); + } + return JSON.stringify(obj); + } + + /** + * @param {string} serialization + * @return {ol.tilegrid.WMTS} tilegrid + */ + deserializeTilegridWMTS(serialization) { + const options = /** @type {olx.tilegrid.WMTSOptions} */ (JSON.parse(serialization)); + return new olTilegridWMTS(options); + } + + + /** + * @param {ol.source.TileWMS} source + * @return {string} + */ + serializeSourceTileWMS(source) { + const obj = this.createBaseObject_(source); + obj['params'] = source.getParams(); + obj['urls'] = source.getUrls(); + obj['tileGrid'] = this.serializeTilegrid(source.getTileGrid()); + const projection = source.getProjection(); + if (projection) { + obj['projection'] = olProj.get(source.getProjection()).getCode(); + } + + return JSON.stringify(obj); + } + + /** + * @param {string} serialization + * @param {function(ol.ImageTile, string)=} tileLoadFunction + * @return {ol.source.TileWMS} source + */ + deserializeSourceTileWMS(serialization, tileLoadFunction) { + const options = /** @type {olx.source.TileWMSOptions} */ (JSON.parse(serialization)); + options['tileLoadFunction'] = tileLoadFunction; + if (options['tileGrid']) { + options['tileGrid'] = this.deserializeTilegrid(options['tileGrid']); + } + options['gutter'] = this.gutter_; + return new olSourceTileWMS(options); + } + + /** + * @param {ol.source.WMTS} source + * @return {string} + */ + serializeSourceWMTS(source) { + const obj = this.createBaseObject_(source); + obj['dimensions'] = source.getDimensions(); + obj['format'] = source.getFormat(); + obj['urls'] = source.getUrls(); + obj['version'] = source.getVersion(); + obj['layer'] = source.getLayer(); + obj['style'] = source.getStyle(); + obj['matrixSet'] = source.getMatrixSet(); + // The OL getTileGrid method is expected to return a WMTS tilegrid so it is OK to cast here. + const tileGridWMTS = /** @type {ol.tilegrid.WMTS} */ (source.getTileGrid()); + obj['tileGrid'] = this.serializeTilegridWMTS(tileGridWMTS); + obj['requestEncoding'] = source.getRequestEncoding(); + const projection = source.getProjection(); + if (projection) { + obj['projection'] = olProj.get(source.getProjection()).getCode(); + } + + return JSON.stringify(obj); + } + + /** + * @param {string} serialization + * @param {function(ol.ImageTile, string)=} tileLoadFunction + * @return {ol.source.WMTS} + */ + deserializeSourceWMTS(serialization, tileLoadFunction) { + const options = /** @type {olx.source.WMTSOptions} */ (JSON.parse(serialization)); + options['tileLoadFunction'] = tileLoadFunction; + if (options['tileGrid']) { + options['tileGrid'] = this.deserializeTilegridWMTS(options['tileGrid']); + } + return new olSourceWMTS(options); + } + + /** + * @private + * @param {number} number + * @return {number} + */ + makeInfinitySerializable_(number) { + if (number === Infinity) { + return 1000; + } + return number; + } + + /** + * @param {ol.layer.Tile|ol.layer.Image} layer + * @param {ol.source.Source=} source + * @return {string} + */ + serializeTileLayer(layer, source) { + const obj = this.createBaseObject_(layer); + obj['opacity'] = layer.getOpacity(); + obj['visible'] = layer.getVisible(); + obj['minResolution'] = layer.getMinResolution(); + obj['maxResolution'] = this.makeInfinitySerializable_(layer.getMaxResolution()); + obj['zIndex'] = layer.getZIndex(); + source = source || layer.getSource(); + if (source instanceof olSourceTileWMS) { + obj['source'] = this.serializeSourceTileWMS(source); + obj['sourceType'] = 'tileWMS'; + } else if (source instanceof olSourceWMTS) { + obj['source'] = this.serializeSourceWMTS(source); + obj['sourceType'] = 'WMTS'; + } + return JSON.stringify(obj); + } + + /** + * @param {string} serialization + * @param {function(ol.ImageTile, string)=} tileLoadFunction + * @return {ol.layer.Tile} + */ + deserializeTileLayer(serialization, tileLoadFunction) { + const options = /** @type {olx.layer.TileOptions} */ (JSON.parse(serialization)); + const sourceType = options['sourceType']; + if (sourceType === 'tileWMS') { + options['source'] = this.deserializeSourceTileWMS(options['source'], tileLoadFunction); + } else if (sourceType === 'WMTS') { + options['source'] = this.deserializeSourceWMTS(options['source'], tileLoadFunction); + } + return new olLayerTile(options); + } +}; + +const exports = SerDes; + + +export default exports; diff --git a/src/offline/ServiceManager.js b/src/offline/ServiceManager.js new file mode 100644 index 000000000000..46820cb4621c --- /dev/null +++ b/src/offline/ServiceManager.js @@ -0,0 +1,107 @@ +/** + * @module ngeo.offline.ServiceManager + */ + +const exports = class { + + /** + * @param {angular.$injector} $injector Main injector. + * @struct + * @ngInject + * @ngdoc service + * @ngname ngeoOfflineServiceManager + */ + constructor($injector) { + + /** + * @type {angular.$injector} + * @private + */ + this.$injector_ = $injector; + + /** + * @type {*} + * @private + */ + this.saveService_ = null; + + /** + * @type {*} + * @private + */ + this.restoreService_ = null; + } + + /** + * Set the service to call on 'save'. + * @param {string|null} saveServiceName A service name that can be injected and that have a 'save' method. + */ + setSaveService(saveServiceName) { + if (saveServiceName && this.$injector_.has(saveServiceName)) { + const saveService = this.$injector_.get(saveServiceName); + if (!saveService.save) { + console.warn('Your offline save service must have a "save" function'); + return; + } + this.saveService_ = saveService; + } + } + + /** + * Set the service to call on 'restore' + * @param {string|null} restoreServiceName A service name that can be injected and that have a 'restore' method. + */ + setRestoreService(restoreServiceName) { + if (restoreServiceName && this.$injector_.has(restoreServiceName)) { + const restoreService = this.$injector_.get(restoreServiceName); + if (!restoreService.restore) { + console.warn('Your offline restore service must have a "restore" function'); + return; + } + this.restoreService_ = restoreService; + } + } + + cancel() { + if (!this.saveService_) { + console.warn('You must register a saveService first'); + return; + } + this.saveService_.cancel(); + } + + /** + * Ask the provided service to save the data to an offline purpose + * @param {ol.Extent} extent The extent to dowload. + * @param {ol.Map} map The map to work on. + */ + save(extent, map) { + if (!this.saveService_) { + console.warn('You must register a saveService first'); + return; + } + this.saveService_.save(extent, map); + } + + /** + * Ask the provided service to restore the saved data on the map + * @param {ol.Map} map The map to work on. + * @return {Promise} + */ + restore(map) { + if (!this.restoreService_) { + console.warn('You must register a restoreService first'); + return Promise.reject(); + } + return this.restoreService_.restore(map); + } +}; + +/** + * @type {!angular.Module} + */ +exports.module = angular.module('ngeoOfflineServiceManager', []); +exports.module.service('ngeoOfflineServiceManager', exports); + + +export default exports; diff --git a/src/offline/TilesDownloader.js b/src/offline/TilesDownloader.js new file mode 100644 index 000000000000..68b0533b0bf7 --- /dev/null +++ b/src/offline/TilesDownloader.js @@ -0,0 +1,196 @@ +/** + * @module ngeo.offline.TilesDownloader + */ +import googAsserts from 'goog/asserts.js'; + + +/** + * @param {!Blob} blob A blob + * @return {Promise} data URL + */ +function blobToDataUrl(blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function() { + resolve(reader.result); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} + +const exports = class { + + /** + * @param {Array} tiles An array of tiles to download. + * @param {ngeox.OfflineOnTileDownload} callbacks The callbacks. + * @param {number} workers The maximum number of workers. + */ + constructor(tiles, callbacks, workers) { + /** + * @private + * @type {number} + */ + this.maxNumberOfWorkers_ = workers; + + /** + * @private + */ + this.wasStarted_ = false; + + /** + * @type {Array} + * @private + */ + this.tiles_ = tiles; + + /** + * @private + * @type {ngeox.OfflineOnTileDownload} + */ + this.callbacks_ = callbacks; + + /** + * @private + */ + this.allCount_ = 0; + + /** + * @private + */ + this.okCount_ = 0; + + /** + * @private + */ + this.koCount_ = 0; + + /** + * @private + */ + this.requestedCount_ = 0; + + /** + * @private + * @type {function()} + */ + this.resolvePromise_; + + /** + * @private + * @type {Promise} + */ + this.promise_ = null; + + /** + * @type {number} + * @private + */ + this.tileIndex_ = 0; + + /** + * @type {boolean} + * @private + */ + this.cancel_ = false; + } + + cancel() { + this.cancel_ = true; + } + + /** + * @private to download. + */ + downloadTile_() { + if (this.cancel_ || this.tileIndex_ >= this.tiles_.length) { + return; + } + const tile = this.tiles_[this.tileIndex_++]; + const tileUrl = tile.url; + const xhr = new XMLHttpRequest(); + xhr.tileUrl = tile.url; + xhr.open('GET', tileUrl, true); + xhr.responseType = 'blob'; + const onTileDownloaded = () => { + if (this.allCount_ === this.tiles_.length) { + this.resolvePromise_(); + } + this.downloadTile_(); + }; + + const errorCallback = (e) => { + if (this.cancel_) { + return; + } + ++this.allCount_; + ++this.koCount_; + const progress = this.allCount_ / this.tiles_.length; + this.callbacks_.onTileDownloadError(progress).then(onTileDownloaded, onTileDownloaded); + }; + + const onloadCallback = (e) => { + /** + * @type {Blob} + */ + const response = e.target.response; + if (response && response.size !== 0) { // non-empty tile + blobToDataUrl(response).then( + (dataUrl) => { + if (this.cancel_) { + return; + } + ++this.allCount_; + ++this.okCount_; + tile.response = dataUrl; + const progress = this.allCount_ / this.tiles_.length; + this.callbacks_.onTileDownloadSuccess(progress, tile).then(onTileDownloaded, onTileDownloaded); + }, + () => { + if (this.cancel_) { + return; + } + errorCallback(e); + } + ); + } else { + if (this.cancel_) { + return; + } + ++this.allCount_; + ++this.okCount_; + this.callbacks_.onTileDownloadSuccess(this.allCount_ / this.tiles_.length, tile).then(onTileDownloaded, onTileDownloaded); + } + }; + + xhr.onload = onloadCallback; + xhr.onerror = errorCallback; + xhr.onabort = errorCallback; + xhr.ontimeout = errorCallback; + xhr.send(); + ++this.requestedCount_; + } + + /** + * @return {Promise} A promise that resolves when the downloads are complete (failing or not) + */ + download() { + if (this.promise_) { + return this.promise_; + } + + this.promise_ = new Promise((resolve, reject) => { + this.resolvePromise_ = resolve; + }); + + googAsserts.assert(this.tiles_); + for (let i = 0; i < this.maxNumberOfWorkers_; ++i) { + this.downloadTile_(); + } + + return this.promise_; + } +}; + + +export default exports; diff --git a/src/offline/component.html b/src/offline/component.html new file mode 100644 index 000000000000..672190a52354 --- /dev/null +++ b/src/offline/component.html @@ -0,0 +1,129 @@ +
+ +
+
+ +
+
+
+ +
+
Save map
+
Abort
+
+ + +
+
{{$ctrl.progressPercents}}%
+
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/offline/component.js b/src/offline/component.js new file mode 100644 index 000000000000..5c72b1ac80ac --- /dev/null +++ b/src/offline/component.js @@ -0,0 +1,587 @@ +/** + * @module ngeo.offline.component + */ +import ngeoMapFeatureOverlayMgr from 'ngeo/map/FeatureOverlayMgr.js'; +import ngeoMessageModalComponent from 'ngeo/message/modalComponent.js'; +import {extentToRectangle} from 'ngeo/utils.js'; +import olCollection from 'ol/Collection.js'; +import {unByKey} from 'ol/Observable.js'; +import olFeature from 'ol/Feature.js'; +import olGeomPolygon from 'ol/geom/Polygon.js'; +import olGeomGeometryLayout from 'ol/geom/GeometryLayout.js'; +import {DEVICE_PIXEL_RATIO} from 'ol/has.js'; + +/** + * @type {!angular.Module} + */ +const exports = angular.module('ngeoOffline', [ + ngeoMapFeatureOverlayMgr.module.name, + ngeoMessageModalComponent.name +]); + +exports.value('ngeoOfflineTemplateUrl', + /** + * @param {angular.JQLite} element Element. + * @param {angular.Attributes} attrs Attributes. + * @return {string} Template URL. + */ + (element, attrs) => { + const templateUrl = attrs['ngeoOfflineTemplateurl']; + return templateUrl !== undefined ? templateUrl : + 'ngeo/offline/component.html'; + }); + +exports.run(/* @ngInject */ ($templateCache) => { + $templateCache.put('ngeo/offline/component.html', require('./component.html')); +}); + +/** + * @param {!angular.JQLite} $element Element. + * @param {!angular.Attributes} $attrs Attributes. + * @param {!function(!angular.JQLite, !angular.Attributes): string} ngeoOfflineTemplateUrl Template function. + * @return {string} Template URL. + * @ngInject + */ +function ngeoOfflineTemplateUrl($element, $attrs, ngeoOfflineTemplateUrl) { + return ngeoOfflineTemplateUrl($element, $attrs); +} + + +/** + * Provides the "offline" component. + * + * Example: + * + * + * ngeo-offline-mask-margin="::100" + * ngeo-offline-min_zoom="::11" + * ngeo-offline-max_zoom="::15" + * + * + * See our live example: [../examples/offline.html](../examples/offline.html) + * + * @htmlAttribute {ol.Map} ngeo-offline-map The map. + * @htmlAttribute {number} ngeo-offline-extentsize The size, in map units, of a side of the extent. + * @private + * @ngdoc component + * @ngname ngeoOffline + */ +exports.component_ = { + bindings: { + 'map': '} + * @private + */ + this.postComposeListenerKey_ = null; + + + /** + * @type {ol.geom.Polygon} + * @private + */ + this.dataPolygon_ = null; + + /** + * Whether the current view is the extent selection. + * @type {boolean} + * @export + */ + this.selectingExtent = false; + + /** + * Whether the current view is downloading one. + * @type {boolean} + * @export + */ + this.downloading = false; + + /** + * The progression of the data loading (0-100%). + * @type {number} + * @export + */ + this.progressPercents = 0; + + /** + * Whether the menu is currently displayed. + * @type {boolean} + * @export + */ + this.menuDisplayed = false; + + /** + * Whether the cancel download modal is displayed. + * @type {boolean} + * @export + */ + this.displayAlertAbortDownload = false; + + /** + * Whether the load data modal is displayed. + * @type {boolean} + * @export + */ + this.displayAlertLoadData = false; + + /** + * Whether the "no layer" modal is displayed. + * @type {boolean} + * @export + */ + this.displayAlertNoLayer = false; + + /** + * Offline mask minimum margin in pixels. + * @type {number} + * @export + */ + this.maskMargin; + + /** + * Minimum zoom where offline is enable. + * @type {number} + * @export + */ + this.minZoom; + + /** + * Maximum zoom where offline is enable. + * @type {number} + * @export + */ + this.maxZoom; + + /** + * Map view max zoom constraint. + * @type {number} + * @export + */ + this.originalMinZoom; + + /** + * Map view min zoom constraint. + * @type {number} + * @export + */ + this.originalMaxZoom; + + /** + * @type {number} + * @export + */ + this.estimatedLoadDataSize; + + /** + * @private + * @param {ngeo.CustomEvent} event the progress event. + */ + this.progressCallback_ = (event) => { + const progress = event.detail['progress']; + this.progressPercents = Math.floor(progress * 100); + if (progress === 1) { + this.finishDownload_(); + } + this.$timeout_(() => {}, 0); // FIXME: force redraw + }; + } + + $onInit() { + this.offlineMode.registerComponent(this); + this.postcomposeListener_ = this.createMaskPostcompose_(); + this.ngeoOfflineConfiguration_.on('progress', this.progressCallback_); + this.maskMargin = this.maskMargin || 100; + this.minZoom = this.minZoom || 10; + this.maxZoom = this.maxZoom || 15; + } + + $onDestroy() { + this.ngeoOfflineConfiguration_.un('progress', this.progressCallback_); + } + + /** + * @return {boolean} True if data are accessible offline. + * @export + */ + hasData() { + return this.ngeoOfflineConfiguration_.hasOfflineData(); + } + + /** + * @export + */ + computeSizeAndDisplayAlertLoadData() { + this.estimatedLoadDataSize = this.ngeoOfflineConfiguration_.estimateLoadDataSize(this.map); + if (this.estimatedLoadDataSize > 0) { + this.displayAlertLoadData = true; + } else { + this.displayAlertNoLayer = true; + } + } + /** + * Toggle the selecting extent view. + * @param {boolean=} finished If just finished downloading. + * @export + */ + toggleViewExtentSelection(finished) { + this.menuDisplayed = false; + this.selectingExtent = !this.selectingExtent; + + if (this.postComposeListenerKey_) { + unByKey(this.postComposeListenerKey_); + this.postComposeListenerKey_ = null; + this.removeZoomConstraints_(); + } + if (this.selectingExtent && !this.postComposeListenerKey_) { + this.addZoomConstraints_(); + this.postComposeListenerKey_ = this.map.on('postcompose', this.postcomposeListener_); + } + this.map.render(); + } + + /** + * Validate the current extent and download data. + * @export + */ + validateExtent() { + this.progressPercents = 0; + const extent = this.getDowloadExtent_(); + this.ngeoOfflineServiceManager_.save(extent, this.map); + this.downloading = true; + } + + + /** + * @private + */ + finishDownload_() { + this.downloading = false; + this.toggleViewExtentSelection(true); + } + + /** + * Ask to abort the download of data. + * @export + */ + askAbortDownload() { + this.displayAlertAbortDownload = true; + } + + /** + * Abort the download of data. + * @export + */ + abortDownload() { + this.downloading = false; + this.ngeoOfflineServiceManager_.cancel(); + this.deleteData(); + } + + /** + * Show the main menu. + * @export + */ + showMenu() { + this.menuDisplayed = true; + } + + /** + * Activate offline mode. + * Zoom to the extent of that data and restore the data. + * @export + */ + activateOfflineMode() { + this.ngeoOfflineServiceManager_.restore(this.map).then((extent) => { + this.dataPolygon_ = this.createPolygonFromExtent_(extent); + const size = /** @type {ol.Size} */ (this.map.getSize()); + this.map.getView().fit(extent, {size}); + this.menuDisplayed = false; + this.displayExtent_(); + this.offlineMode.enable(); + }); + } + + /** + * + * Deactivate offline mode. + * Reload the page. + * @export + */ + deactivateOfflineMode() { + window.location.reload(); + } + + /** + * Toggle the visibility of the data's extent. + * @export + */ + toggleExtentVisibility() { + if (this.isExtentVisible()) { + this.overlayCollection_.clear(); + } else { + this.displayExtent_(); + } + } + + /** + * @return {boolean} True if the extent is currently visible. False otherwise. + * @export + */ + isExtentVisible() { + return this.overlayCollection_.getLength() > 0; + } + + /** + * Delete the saved data. + * @export + */ + deleteData() { + this.overlayCollection_.clear(); + this.dataPolygon_ = null; + if (this.networkStatus.isDisconnected()) { + this.menuDisplayed = false; + } + + const reloadIfInOfflineMode = () => { + if (this.offlineMode.isEnabled()) { + this.deactivateOfflineMode(); + } + }; + this.ngeoOfflineConfiguration_.clear().then(reloadIfInOfflineMode); + } + + /** + * @private + */ + displayExtent_() { + if (!this.isExtentVisible()) { + const feature = new olFeature(this.dataPolygon_); + this.overlayCollection_.push(feature); + } + } + + /** + * When enabling mask extent, zoom the view to the defined zoom range and + * add constraints to the view to not allow user to move out of this range. + * @private + */ + addZoomConstraints_() { + const view = this.map.getView(); + const zoom = view.getZoom(); + + this.originalMinZoom = view.getMinZoom(); + this.originalMaxZoom = view.getMaxZoom(); + + if (zoom < this.minZoom) { + view.setZoom(this.minZoom); + } else if (zoom > this.maxZoom) { + view.setZoom(this.maxZoom); + } + view.setMaxZoom(this.maxZoom); + view.setMinZoom(this.minZoom); + } + + /** + * @private + */ + removeZoomConstraints_() { + const view = this.map.getView(); + view.setMaxZoom(this.originalMaxZoom); + view.setMinZoom(this.originalMinZoom); + } + + /** + * @return {function(ol.render.Event)} Function to use as a map postcompose listener. + * @private + */ + createMaskPostcompose_() { + return (evt) => { + const context = evt.context; + const frameState = evt.frameState; + const resolution = frameState.viewState.resolution; + + const viewportWidth = frameState.size[0] * frameState.pixelRatio; + const viewportHeight = frameState.size[1] * frameState.pixelRatio; + + const center = [viewportWidth / 2, viewportHeight / 2]; + + const extentLength = this.extentSize ? + this.extentSize / resolution * DEVICE_PIXEL_RATIO : + Math.min(viewportWidth, viewportHeight) - this.maskMargin * 2; + + const extentHalfLength = Math.ceil(extentLength / 2); + + // Draw a mask on the whole map. + context.beginPath(); + context.moveTo(0, 0); + context.lineTo(viewportWidth, 0); + context.lineTo(viewportWidth, viewportHeight); + context.lineTo(0, viewportHeight); + context.lineTo(0, 0); + context.closePath(); + + // Draw the get data zone + const extent = this.createExtent_(center, extentHalfLength); + + context.moveTo(extent[0], extent[1]); + context.lineTo(extent[0], extent[3]); + context.lineTo(extent[2], extent[3]); + context.lineTo(extent[2], extent[1]); + context.lineTo(extent[0], extent[1]); + context.closePath(); + + // Fill the mask + context.fillStyle = 'rgba(0, 5, 25, 0.5)'; + context.fill(); + }; + } + + /** + * A polygon on the whole extent of the projection, with a hole for the offline extent. + * @param {ol.Extent} extent + * @return {ol.geom.Polygon} Polygon to save, based on the projection extent, the center of the map and + * the extentSize property. + * @private + */ + createPolygonFromExtent_(extent) { + const projExtent = this.map.getView().getProjection().getExtent(); + return new olGeomPolygon([ + extentToRectangle(projExtent), + extentToRectangle(extent), + ], olGeomGeometryLayout.XY); + } + + /** + * @param {ol.Coordinate} center, a xy point. + * @param {number} halfLength a half length of a square's side. + * @return {Array.} an extent. + * @private + */ + createExtent_(center, halfLength) { + const minx = center[0] - halfLength; + const miny = center[1] - halfLength; + const maxx = center[0] + halfLength; + const maxy = center[1] + halfLength; + return [minx, miny, maxx, maxy]; + } + + /** + * @return {Array.} the download extent. + * @private + */ + getDowloadExtent_() { + const center = /** @type {ol.Coordinate}*/(this.map.getView().getCenter()); + const halfLength = Math.ceil(this.extentSize || this.getExtentSize_()) / 2; + return this.createExtent_(center, halfLength); + } + + getExtentSize_() { + const mapSize = this.map.getSize(); + const maskSizePixel = DEVICE_PIXEL_RATIO * Math.min(mapSize[0], mapSize[1]) - this.maskMargin * 2; + const maskSizeMeter = maskSizePixel * this.map.getView().getResolution() / DEVICE_PIXEL_RATIO; + return maskSizeMeter; + } +}; + + +exports.controller('ngeoOfflineController', exports.Controller); + + +export default exports; diff --git a/src/offline/module.js b/src/offline/module.js new file mode 100644 index 000000000000..e8a2bdc13602 --- /dev/null +++ b/src/offline/module.js @@ -0,0 +1,26 @@ +/** + * @module ngeo.offline.module + */ +import ngeoOfflineComponent from 'ngeo/offline/component.js'; +import ngeoOfflineNetworkStatus from 'ngeo/offline/NetworkStatus.js'; +import ngeoOfflineServiceManager from 'ngeo/offline/ServiceManager.js'; +import downloader from 'ngeo/offline/Downloader.js'; +import restorer from 'ngeo/offline/Restorer.js'; +import mode from 'ngeo/offline/Mode.js'; + +/** + * @type {!angular.Module} + */ +const exports = angular.module('ngeoOfflineModule', [ + ngeoOfflineComponent.name, + ngeoOfflineNetworkStatus.module.name, + ngeoOfflineServiceManager.module.name, + downloader.module.name, + restorer.module.name, + mode.module.name +]); + +exports.value('ngeoOfflineGutter', 96); + + +export default exports; diff --git a/src/offline/utils.js b/src/offline/utils.js new file mode 100644 index 000000000000..5643efd7c1b4 --- /dev/null +++ b/src/offline/utils.js @@ -0,0 +1,34 @@ +/** + * @module ngeo.offline.utils + */ +const exports = {}; +import olLayerGroup from 'ol/layer/Group.js'; + + +/** + * @param {ol.layer.Base} layer A layer tree. + * @param {!Array} ancestors The groups to which the layer belongs to. + * @param {function(ol.layer.Base, Array): boolean} visitor A function which will return false if descend must stop. + */ +exports.traverseLayer = function(layer, ancestors, visitor) { + const descend = visitor(layer, ancestors); + if (descend && layer instanceof olLayerGroup) { + layer.getLayers().forEach((childLayer) => { + exports.traverseLayer(childLayer, [...ancestors, layer], visitor); + }); + } +}; + +const extractor = new RegExp('[^/]*//[^/]+/(.*)'); +/** + * Extract the part after the URL authority. + * @param {string} url + * @return {string} + */ +exports.normalizeURL = function(url) { + const matches = url.match(extractor); + return matches[1]; +}; + + +export default exports; diff --git a/src/utils.js b/src/utils.js index 20fe7dd5ebaa..c30b1a45f92b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -9,7 +9,7 @@ import olGeomMultiLineString from 'ol/geom/MultiLineString.js'; import olGeomMultiPolygon from 'ol/geom/MultiPolygon.js'; import olGeomPoint from 'ol/geom/Point.js'; import olGeomPolygon from 'ol/geom/Polygon.js'; - +import {getTopLeft, getTopRight, getBottomLeft, getBottomRight} from 'ol/extent.js'; /** * Utility method that converts a simple geometry to its multi equivalent. If @@ -117,5 +117,19 @@ exports.deleteCondition = function(event) { return olEventsCondition.noModifierKeys(event) && olEventsCondition.singleClick(event); }; +/** + * Takes an ol.Extent and return an Array of ol.Coordinate representing a rectangle polygon. + * @param {ol.Extent} extent The extent. + * @return {Array.} The Array of coordinate of the rectangle. + */ +export function extentToRectangle(extent) { + return [ + getTopLeft(extent), + getTopRight(extent), + getBottomRight(extent), + getBottomLeft(extent), + getTopLeft(extent), + ]; +} export default exports; From a1404652c6ced092b2adab4829239dcfd1778ffa Mon Sep 17 00:00:00 2001 From: Guillaume Beraudo Date: Thu, 1 Nov 2018 17:13:41 +0100 Subject: [PATCH 2/8] Make eslint happy --- .eslintrc.yaml | 1 + src/offline/Configuration.js | 69 ++++++++++++++------------- src/offline/Mode.js | 2 +- src/offline/Restorer.js | 13 +++-- src/offline/SerializerDeserializer.js | 56 +++++++++++----------- src/offline/ServiceManager.js | 2 +- src/offline/component.js | 4 +- src/offline/utils.js | 4 +- 8 files changed, 76 insertions(+), 75 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 28239e7e185f..01f152b5ff52 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -8,6 +8,7 @@ rules: no-console: 0 comma-dangle: 0 globals: + localforage: false, angular: false Cesium: false google: false diff --git a/src/offline/Configuration.js b/src/offline/Configuration.js index f0d6779ee1d0..8722a6e7715b 100644 --- a/src/offline/Configuration.js +++ b/src/offline/Configuration.js @@ -25,8 +25,8 @@ const exports = class extends olObservable { /** * @ngInject * @param {!angular.Scope} $rootScope The rootScope provider. - * @param {ngeo.map.BackgroundLayerMgr} ngeoBackgroundLayerMgr - * @param {number} ngeoOfflineGutter + * @param {ngeo.map.BackgroundLayerMgr} ngeoBackgroundLayerMgr The background layer manager + * @param {number} ngeoOfflineGutter A gutter around the tiles to download (to avoid cut symbols) */ constructor($rootScope, ngeoBackgroundLayerMgr, ngeoOfflineGutter) { super(); @@ -103,34 +103,34 @@ const exports = class extends olObservable { /** * Hook to allow measuring get/set item performance. - * @param {string} msg - * @param {string} key - * @param {Promise} promise - * @return {Promise} + * @param {string} msg A message + * @param {string} key The key to work on + * @param {Promise} promise A promise + * @return {Promise} The promise we passed */ traceGetSetItem(msg, key, promise) { return promise; } /** - * @param {string} key - * @return {Promise} + * @param {string} key The key + * @return {Promise} A promise */ getItem(key) { return this.traceGetSetItem('getItem', key, localforage.getItem(key)); } /** - * @param {string} key - * @param {*} value - * @return {Promise} + * @param {string} key The key + * @param {*} value A value + * @return {Promise} A promise */ setItem(key, value) { return this.traceGetSetItem('setItem', key, localforage.setItem(key, value)); } /** - * @return {Promise} + * @return {Promise} A promise */ clear() { this.setHasOfflineData(false); @@ -138,15 +138,15 @@ const exports = class extends olObservable { } /** - * @param {!ol.Map} map - * @return {number} + * @param {!ol.Map} map A map + * @return {number} An "estimation" of the size of the data to download */ estimateLoadDataSize(map) { return 50; } /** - * @param {ngeox.OfflineLayerMetadata} layerItem + * @param {ngeox.OfflineLayerMetadata} layerItem The layer metadata * @return {string} A key identifying an offline layer and used during restore. */ getLayerKey(layerItem) { @@ -155,9 +155,9 @@ const exports = class extends olObservable { /** * @override - * @param {number} progress - * @param {ngeox.OfflineTile} tile - * @return {Promise} + * @param {number} progress The download progress + * @param {ngeox.OfflineTile} tile The tile + * @return {Promise} A promise */ onTileDownloadSuccess(progress, tile) { this.dispatchProgress_(progress); @@ -170,8 +170,8 @@ const exports = class extends olObservable { /** * @override - * @param {number} progress - * @return {Promise} + * @param {number} progress The progress + * @return {Promise} A promise */ onTileDownloadError(progress) { this.dispatchProgress_(progress); @@ -179,11 +179,11 @@ const exports = class extends olObservable { } /** - * @param {ol.Map} map - * @param {ol.layer.Layer} layer - * @param {Array} ancestors + * @param {ol.Map} map A map + * @param {ol.layer.Layer} layer A layer + * @param {Array} ancestors The ancestors of that layer * @param {ol.Extent} userExtent The extent selected by the user. - * @return {Array} + * @return {Array} The extent to download per zoom level */ getExtentByZoom(map, layer, ancestors, userExtent) { const currentZoom = map.getView().getZoom(); @@ -201,9 +201,9 @@ const exports = class extends olObservable { /** * @protected - * @param {ol.source.Source} source - * @param {ol.proj.Projection} projection - * @return {ol.source.Source} + * @param {ol.source.Source} source An ImageWMS source + * @param {ol.proj.Projection} projection The projection + * @return {ol.source.Source} A tiled equivalent source */ sourceImageWMSToTileWMS(source, projection) { if (source instanceof olSourceImageWMS && source.getUrl() && source.getImageLoadFunction() === defaultImageLoadFunction) { @@ -247,8 +247,9 @@ const exports = class extends olObservable { layerType = 'vector'; } + const backgroundLayer = this.ngeoBackgroundLayerMgr_.get(map) === layer; layersItems.push({ - backgroundLayer: this.ngeoBackgroundLayerMgr_.get(map) === layer, + backgroundLayer, map, extentByZoom, layerType, @@ -268,15 +269,15 @@ const exports = class extends olObservable { /** * @private - * @param {ngeox.OfflinePersistentLayer} offlineLayer - * @return {function(ol.ImageTile, string)} + * @param {ngeox.OfflinePersistentLayer} offlineLayer The offline layer + * @return {function(ol.ImageTile, string)} the tile function */ createTileLoadFunction_(offlineLayer) { const that = this; /** * Load the tile from persistent storage. - * @param {ol.ImageTile} imageTile - * @param {string} src + * @param {ol.ImageTile} imageTile The image tile + * @param {string} src The tile URL */ const tileLoadFunction = function(imageTile, src) { that.getItem(utils.normalizeURL(src)).then((content) => { @@ -291,7 +292,7 @@ const exports = class extends olObservable { } /** - * @param {ngeox.OfflinePersistentLayer} offlineLayer + * @param {ngeox.OfflinePersistentLayer} offlineLayer The layer to recreate * @return {ol.layer.Layer} the layer. */ recreateOfflineLayer(offlineLayer) { @@ -305,7 +306,7 @@ const exports = class extends olObservable { } /** - * @return {number} + * @return {number} The number */ getMaxNumberOfParallelDownloads() { return 11; diff --git a/src/offline/Mode.js b/src/offline/Mode.js index a8c558220650..f61292478be4 100644 --- a/src/offline/Mode.js +++ b/src/offline/Mode.js @@ -35,7 +35,7 @@ const exports = class { /** * Return if we are in offline mode. - * @return {boolean} + * @return {boolean} whether offline mode is enabled * @export */ isEnabled() { diff --git a/src/offline/Restorer.js b/src/offline/Restorer.js index 19691fbd2242..af9572db9d2b 100644 --- a/src/offline/Restorer.js +++ b/src/offline/Restorer.js @@ -9,7 +9,7 @@ class Restorer { /** * @ngInject * @param {ngeo.offline.Configuration} ngeoOfflineConfiguration A service for customizing offline behaviour. - * @param {ngeo.map.BackgroundLayerMgr} ngeoBackgroundLayerMgr + * @param {ngeo.map.BackgroundLayerMgr} ngeoBackgroundLayerMgr The background layer manager. */ constructor(ngeoOfflineConfiguration, ngeoBackgroundLayerMgr) { /** @@ -26,9 +26,8 @@ class Restorer { } /** - * @public - * @param {ol.Map} map - * @return {Promise} + * @param {ol.Map} map The map to work on. + * @return {Promise} A promise to the extent of the restored area. */ restore(map) { return this.configuration_.getItem('offline_content').then(offlineContent => this.doRestore(map, offlineContent)); @@ -36,9 +35,9 @@ class Restorer { /** * @protected - * @param {ol.Map} map - * @param {ngeox.OfflinePersistentContent} offlineContent - * @return {ol.Extent} + * @param {ol.Map} map A map + * @param {ngeox.OfflinePersistentContent} offlineContent The offline content + * @return {ol.Extent} The extent of the restored area */ doRestore(map, offlineContent) { map.getLayerGroup().getLayers().clear(); diff --git a/src/offline/SerializerDeserializer.js b/src/offline/SerializerDeserializer.js index 56b2ccd3786c..19df1655457c 100644 --- a/src/offline/SerializerDeserializer.js +++ b/src/offline/SerializerDeserializer.js @@ -11,7 +11,7 @@ import olLayerTile from 'ol/layer/Tile.js'; const SerDes = class { /** - * @param {Object} options + * @param {Object} options The options */ constructor({gutter}) { /** @@ -22,8 +22,8 @@ const SerDes = class { /** * @private - * @param {ol.Object} olObject - * @return {Object} + * @param {ol.Object} olObject An OL object + * @return {Object} The serializable properties of the object */ createBaseObject_(olObject) { const properties = olObject.getProperties(); @@ -39,8 +39,8 @@ const SerDes = class { } /** - * @param {ol.tilegrid.TileGrid} tilegrid - * @return {string} + * @param {ol.tilegrid.TileGrid} tilegrid . + * @return {string} . */ serializeTilegrid(tilegrid) { const obj = {}; @@ -53,7 +53,7 @@ const SerDes = class { } /** - * @param {string} serialization + * @param {string} serialization . * @return {ol.tilegrid.TileGrid} tilegrid */ deserializeTilegrid(serialization) { @@ -62,8 +62,8 @@ const SerDes = class { } /** - * @param {ol.tilegrid.WMTS} tilegrid - * @return {string|undefined} + * @param {ol.tilegrid.WMTS} tilegrid . + * @return {string|undefined} . */ serializeTilegridWMTS(tilegrid) { if (!tilegrid) { @@ -84,8 +84,8 @@ const SerDes = class { } /** - * @param {string} serialization - * @return {ol.tilegrid.WMTS} tilegrid + * @param {string} serialization . + * @return {ol.tilegrid.WMTS} tilegrid . */ deserializeTilegridWMTS(serialization) { const options = /** @type {olx.tilegrid.WMTSOptions} */ (JSON.parse(serialization)); @@ -94,8 +94,8 @@ const SerDes = class { /** - * @param {ol.source.TileWMS} source - * @return {string} + * @param {ol.source.TileWMS} source . + * @return {string} . */ serializeSourceTileWMS(source) { const obj = this.createBaseObject_(source); @@ -111,9 +111,9 @@ const SerDes = class { } /** - * @param {string} serialization - * @param {function(ol.ImageTile, string)=} tileLoadFunction - * @return {ol.source.TileWMS} source + * @param {string} serialization . + * @param {function(ol.ImageTile, string)=} tileLoadFunction . + * @return {ol.source.TileWMS} source . */ deserializeSourceTileWMS(serialization, tileLoadFunction) { const options = /** @type {olx.source.TileWMSOptions} */ (JSON.parse(serialization)); @@ -126,8 +126,8 @@ const SerDes = class { } /** - * @param {ol.source.WMTS} source - * @return {string} + * @param {ol.source.WMTS} source . + * @return {string} . */ serializeSourceWMTS(source) { const obj = this.createBaseObject_(source); @@ -151,9 +151,9 @@ const SerDes = class { } /** - * @param {string} serialization - * @param {function(ol.ImageTile, string)=} tileLoadFunction - * @return {ol.source.WMTS} + * @param {string} serialization . + * @param {function(ol.ImageTile, string)=} tileLoadFunction . + * @return {ol.source.WMTS} . */ deserializeSourceWMTS(serialization, tileLoadFunction) { const options = /** @type {olx.source.WMTSOptions} */ (JSON.parse(serialization)); @@ -166,8 +166,8 @@ const SerDes = class { /** * @private - * @param {number} number - * @return {number} + * @param {number} number Some number which may be Infinity + * @return {number} The same number or an arbitrary big number instead of Infinity */ makeInfinitySerializable_(number) { if (number === Infinity) { @@ -177,9 +177,9 @@ const SerDes = class { } /** - * @param {ol.layer.Tile|ol.layer.Image} layer - * @param {ol.source.Source=} source - * @return {string} + * @param {ol.layer.Tile|ol.layer.Image} layer . + * @param {ol.source.Source=} source . + * @return {string} . */ serializeTileLayer(layer, source) { const obj = this.createBaseObject_(layer); @@ -200,9 +200,9 @@ const SerDes = class { } /** - * @param {string} serialization - * @param {function(ol.ImageTile, string)=} tileLoadFunction - * @return {ol.layer.Tile} + * @param {string} serialization . + * @param {function(ol.ImageTile, string)=} tileLoadFunction . + * @return {ol.layer.Tile} . */ deserializeTileLayer(serialization, tileLoadFunction) { const options = /** @type {olx.layer.TileOptions} */ (JSON.parse(serialization)); diff --git a/src/offline/ServiceManager.js b/src/offline/ServiceManager.js index 46820cb4621c..0731a1b69cf5 100644 --- a/src/offline/ServiceManager.js +++ b/src/offline/ServiceManager.js @@ -86,7 +86,7 @@ const exports = class { /** * Ask the provided service to restore the saved data on the map * @param {ol.Map} map The map to work on. - * @return {Promise} + * @return {Promise} A promise to the extent of the downloaded area */ restore(map) { if (!this.restoreService_) { diff --git a/src/offline/component.js b/src/offline/component.js index 5c72b1ac80ac..4514cc70e7ff 100644 --- a/src/offline/component.js +++ b/src/offline/component.js @@ -34,7 +34,7 @@ exports.value('ngeoOfflineTemplateUrl', exports.run(/* @ngInject */ ($templateCache) => { $templateCache.put('ngeo/offline/component.html', require('./component.html')); }); - + /** * @param {!angular.JQLite} $element Element. * @param {!angular.Attributes} $attrs Attributes. @@ -535,7 +535,7 @@ exports.Controller = class { /** * A polygon on the whole extent of the projection, with a hole for the offline extent. - * @param {ol.Extent} extent + * @param {ol.Extent} extent An extent * @return {ol.geom.Polygon} Polygon to save, based on the projection extent, the center of the map and * the extentSize property. * @private diff --git a/src/offline/utils.js b/src/offline/utils.js index 5643efd7c1b4..cf4777139dc4 100644 --- a/src/offline/utils.js +++ b/src/offline/utils.js @@ -22,8 +22,8 @@ exports.traverseLayer = function(layer, ancestors, visitor) { const extractor = new RegExp('[^/]*//[^/]+/(.*)'); /** * Extract the part after the URL authority. - * @param {string} url - * @return {string} + * @param {string} url A URL to normalize + * @return {string} The normalized string. */ exports.normalizeURL = function(url) { const matches = url.match(extractor); From c4e92a705277b72bdf039aff95a73bdc856d8a36 Mon Sep 17 00:00:00 2001 From: Guillaume Beraudo Date: Tue, 27 Nov 2018 13:52:29 +0100 Subject: [PATCH 3/8] Simplify imports --- .eslintrc.yaml | 1 - package.json | 4 +++- src/message/modalComponent.js | 2 ++ src/offline/Configuration.js | 1 + src/search/createGeoJSONBloodhound.js | 2 +- src/search/createLocationSearchBloodhound.js | 2 +- 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 01f152b5ff52..28239e7e185f 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -8,7 +8,6 @@ rules: no-console: 0 comma-dangle: 0 globals: - localforage: false, angular: false Cesium: false google: false diff --git a/package.json b/package.json index 7db996d85600..cd8d139590a1 100644 --- a/package.json +++ b/package.json @@ -109,5 +109,7 @@ "webpack-dev-server": "3.1.4", "webpack-merge": "4.1.2" }, - "dependencies": {} + "dependencies": { + "localforage": "^1.7.3" + } } diff --git a/src/message/modalComponent.js b/src/message/modalComponent.js index 8262baf7cd82..586d29972514 100644 --- a/src/message/modalComponent.js +++ b/src/message/modalComponent.js @@ -1,6 +1,8 @@ /** * @module ngeo.message.modalComponent */ +import 'jquery'; +import 'jquery-ui'; import 'jquery-ui/ui/widgets/draggable.js'; import 'bootstrap/js/modal.js'; import googAsserts from 'goog/asserts.js'; diff --git a/src/offline/Configuration.js b/src/offline/Configuration.js index 8722a6e7715b..1e9996b076f0 100644 --- a/src/offline/Configuration.js +++ b/src/offline/Configuration.js @@ -16,6 +16,7 @@ import ngeoCustomEvent from 'ngeo/CustomEvent.js'; import utils from 'ngeo/offline/utils.js'; const defaultImageLoadFunction = olSourceImage.defaultImageLoadFunction; +import * as localforage from 'localforage'; /** * @implements {ngeox.OfflineOnTileDownload} diff --git a/src/search/createGeoJSONBloodhound.js b/src/search/createGeoJSONBloodhound.js index fe81e78e8e3d..6dec4b111339 100644 --- a/src/search/createGeoJSONBloodhound.js +++ b/src/search/createGeoJSONBloodhound.js @@ -4,7 +4,7 @@ import olFormatGeoJSON from 'ol/format/GeoJSON.js'; import * as olObj from 'ol/obj.js'; -import 'corejs-typeahead'; +import Bloodhound from 'corejs-typeahead'; /** diff --git a/src/search/createLocationSearchBloodhound.js b/src/search/createLocationSearchBloodhound.js index feae5d3ae513..0012167a9ebe 100644 --- a/src/search/createLocationSearchBloodhound.js +++ b/src/search/createLocationSearchBloodhound.js @@ -10,7 +10,7 @@ import ngeoProjEPSG21781 from 'ngeo/proj/EPSG21781.js'; import olGeomPoint from 'ol/geom/Point.js'; import olFeature from 'ol/Feature.js'; -import 'corejs-typeahead'; +import Bloodhound from 'corejs-typeahead'; /** From 8531beb4199b5c3f86eeb3c0dc03a90fe61350a5 Mon Sep 17 00:00:00 2001 From: Guillaume Beraudo Date: Wed, 12 Dec 2018 11:33:54 +0100 Subject: [PATCH 4/8] Use same signature for webpack.dev.js and webpack.prod.js --- buildtools/webpack.dev.js | 26 ++++++++++++++------------ buildtools/webpack.prod.js | 1 + karma-conf.js | 3 ++- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/buildtools/webpack.dev.js b/buildtools/webpack.dev.js index 49bfec60e90f..090c5c390990 100644 --- a/buildtools/webpack.dev.js +++ b/buildtools/webpack.dev.js @@ -15,15 +15,17 @@ const loaderOptionsPlugin = new webpack.LoaderOptionsPlugin({ debug: false }); - -module.exports = { - mode: 'development', - output: { - filename: '[name].js' - }, - module: { - rules: [ - resourcesRule, - ] - }, -}; +// Same signature as for webpack.prod.js +module.exports = function(_) { + return { + mode: 'development', + output: { + filename: '[name].js' + }, + module: { + rules: [ + resourcesRule, + ] + }, + }; +} diff --git a/buildtools/webpack.prod.js b/buildtools/webpack.prod.js index d7abfa926e7d..b070aecb52df 100644 --- a/buildtools/webpack.prod.js +++ b/buildtools/webpack.prod.js @@ -24,6 +24,7 @@ const fontRule = { } }; +// Same signature as for webpack.dev.js module.exports = function(UglifyJsPluginCache) { return { mode: 'production', diff --git a/karma-conf.js b/karma-conf.js index 936abd25e961..36d20c7fe0b1 100644 --- a/karma-conf.js +++ b/karma-conf.js @@ -6,7 +6,8 @@ var isDebug = process.argv.some(function(argument) { const webpackMerge = require('webpack-merge'); const commons = require('./buildtools/webpack.commons'); -let webpackConfig = commons.config(); +let webpackConfig = commons.config({}, false); + webpackConfig = webpackMerge(webpackConfig, require('./buildtools/webpack.dev')); webpackConfig = webpackMerge(webpackConfig, { devtool: 'inline-source-map', From 1469f6e745b9254de748ecdcf226a29dc3bf1b62 Mon Sep 17 00:00:00 2001 From: Guillaume Beraudo Date: Thu, 13 Dec 2018 09:23:41 +0100 Subject: [PATCH 5/8] Revert "Use same signature for webpack.dev.js and webpack.prod.js" This reverts commit 8531beb4199b5c3f86eeb3c0dc03a90fe61350a5. --- buildtools/webpack.dev.js | 26 ++++++++++++-------------- buildtools/webpack.prod.js | 1 - karma-conf.js | 3 +-- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/buildtools/webpack.dev.js b/buildtools/webpack.dev.js index 090c5c390990..49bfec60e90f 100644 --- a/buildtools/webpack.dev.js +++ b/buildtools/webpack.dev.js @@ -15,17 +15,15 @@ const loaderOptionsPlugin = new webpack.LoaderOptionsPlugin({ debug: false }); -// Same signature as for webpack.prod.js -module.exports = function(_) { - return { - mode: 'development', - output: { - filename: '[name].js' - }, - module: { - rules: [ - resourcesRule, - ] - }, - }; -} + +module.exports = { + mode: 'development', + output: { + filename: '[name].js' + }, + module: { + rules: [ + resourcesRule, + ] + }, +}; diff --git a/buildtools/webpack.prod.js b/buildtools/webpack.prod.js index b070aecb52df..d7abfa926e7d 100644 --- a/buildtools/webpack.prod.js +++ b/buildtools/webpack.prod.js @@ -24,7 +24,6 @@ const fontRule = { } }; -// Same signature as for webpack.dev.js module.exports = function(UglifyJsPluginCache) { return { mode: 'production', diff --git a/karma-conf.js b/karma-conf.js index 36d20c7fe0b1..936abd25e961 100644 --- a/karma-conf.js +++ b/karma-conf.js @@ -6,8 +6,7 @@ var isDebug = process.argv.some(function(argument) { const webpackMerge = require('webpack-merge'); const commons = require('./buildtools/webpack.commons'); -let webpackConfig = commons.config({}, false); - +let webpackConfig = commons.config(); webpackConfig = webpackMerge(webpackConfig, require('./buildtools/webpack.dev')); webpackConfig = webpackMerge(webpackConfig, { devtool: 'inline-source-map', From 30dcb78b52f0be1590ba324a369bea1549fe908b Mon Sep 17 00:00:00 2001 From: Guillaume Beraudo Date: Fri, 21 Dec 2018 09:16:00 +0100 Subject: [PATCH 6/8] Allow passing a plain service to the Offline service manager --- src/offline/ServiceManager.js | 47 +++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/offline/ServiceManager.js b/src/offline/ServiceManager.js index 0731a1b69cf5..884c2334a701 100644 --- a/src/offline/ServiceManager.js +++ b/src/offline/ServiceManager.js @@ -33,33 +33,44 @@ const exports = class { } /** - * Set the service to call on 'save'. - * @param {string|null} saveServiceName A service name that can be injected and that have a 'save' method. + * @param {string|Object} serviceLike A service like. + * @param {string} method A method. + * @return {Object} A returned object. */ - setSaveService(saveServiceName) { - if (saveServiceName && this.$injector_.has(saveServiceName)) { - const saveService = this.$injector_.get(saveServiceName); - if (!saveService.save) { - console.warn('Your offline save service must have a "save" function'); + getOfflineService_(serviceLike, method) { + if (typeof serviceLike === 'string') { + if (!this.$injector_.has(serviceLike)) { + console.error(`The offline ${method} service could not be found`); + return; + } + const service = this.$injector_.get(serviceLike); + if (!service[method]) { + console.error(`The offline service ${serviceLike} does not have a ${method} method`); return; } - this.saveService_ = saveService; + return service; } + if (!serviceLike[method]) { + console.error(`The provided offline service does not have a ${method} method`); + return; + } + return serviceLike; + } + + /** + * Set the service to call on 'save'. + * @param {string|{save: Function}} saveLikeService A service name that can be injected or an object that have a 'save' method. + */ + setSaveService(saveLikeService) { + this.saveService_ = this.getOfflineService_(saveLikeService, 'save'); } /** * Set the service to call on 'restore' - * @param {string|null} restoreServiceName A service name that can be injected and that have a 'restore' method. + * @param {string|{restore: Function}} restoreLikeService A service name that can be injected or an object that have a 'restore' method. */ - setRestoreService(restoreServiceName) { - if (restoreServiceName && this.$injector_.has(restoreServiceName)) { - const restoreService = this.$injector_.get(restoreServiceName); - if (!restoreService.restore) { - console.warn('Your offline restore service must have a "restore" function'); - return; - } - this.restoreService_ = restoreService; - } + setRestoreService(restoreLikeService) { + this.restoreService_ = this.getOfflineService_(restoreLikeService, 'restore'); } cancel() { From c19561f29851a4781e5b275d7d08269b2f247994 Mon Sep 17 00:00:00 2001 From: Guillaume Beraudo Date: Fri, 1 Feb 2019 14:55:42 +0100 Subject: [PATCH 7/8] Add improvements for apps --- .eslintrc.yaml | 1 + contribs/gmf/src/less/base.less | 13 +-- src/message/modalComponent.js | 8 +- src/offline/AbstractLocalforageWrapper.js | 108 +++++++++++++++++++ src/offline/Configuration.js | 68 +++++++++--- src/offline/LocalforageAndroidWrapper.js | 33 ++++++ src/offline/LocalforageCordovaWrapper.js | 21 ++++ src/offline/LocalforageIosWrapper.js | 37 +++++++ src/offline/TilesDownloader.js | 9 +- src/offline/component.js | 2 +- src/search/createGeoJSONBloodhound.js | 3 +- src/search/createLocationSearchBloodhound.js | 2 +- 12 files changed, 272 insertions(+), 33 deletions(-) create mode 100644 src/offline/AbstractLocalforageWrapper.js create mode 100644 src/offline/LocalforageAndroidWrapper.js create mode 100644 src/offline/LocalforageCordovaWrapper.js create mode 100644 src/offline/LocalforageIosWrapper.js diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 28239e7e185f..2aae625f707e 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -13,3 +13,4 @@ globals: google: false Bloodhound: false DateFormatter: false + localforage: false diff --git a/contribs/gmf/src/less/base.less b/contribs/gmf/src/less/base.less index fb640dfddc8d..0fa230eb6c11 100644 --- a/contribs/gmf/src/less/base.less +++ b/contribs/gmf/src/less/base.less @@ -75,20 +75,21 @@ i, cite, em, var, address, dfn { line-height: 1; } -.ui-draggable { +.ui-draggable-handle { cursor: grab; cursor: -webkit-grab; cursor:-moz-grab; +} - &.ui-draggable-dragging { +.ui-draggable-dragging { + .ui-draggable-handle { cursor: move; + } - iframe { - display: none; - } + iframe { + display: none; } } - .ui-resizable { &.ui-resizable-resizing { iframe { diff --git a/src/message/modalComponent.js b/src/message/modalComponent.js index 586d29972514..03f77481945d 100644 --- a/src/message/modalComponent.js +++ b/src/message/modalComponent.js @@ -112,9 +112,7 @@ exports.Controller_ = class { this.ngModel; } - $onInit() { - this.closable = this.closable !== false; - + $postLink() { this.modal_ = this.$element_.children(); if (!this.closable) { @@ -125,7 +123,9 @@ exports.Controller_ = class { this.resizable = !!this.resizable; const dialog = this.modal_.find('.modal-dialog'); - dialog.draggable(); + dialog.draggable({ + 'handle': '.modal-header' + }); if (this.resizable) { dialog.resizable(); } diff --git a/src/offline/AbstractLocalforageWrapper.js b/src/offline/AbstractLocalforageWrapper.js new file mode 100644 index 000000000000..e04128ab5d76 --- /dev/null +++ b/src/offline/AbstractLocalforageWrapper.js @@ -0,0 +1,108 @@ +/** + * @module ngeo.offline.AbstractLocalforageWrapper + */ +/** + * @typedef {{ + * id: number, + * plugin: string, + * command: string, + * args: !Array<*>, + * context: (*|undefined) + * }} + */ +// eslint-disable-next-line no-unused-vars +let Action; + +/** + * @abstract + */ +const exports = class AbstractLocalforageWrapper { + constructor() { + this.waitingPromises_ = new window.Map(); + this.currentId_ = 0; + } + + setItem(...args) { + return this.createAction('setItem', ...args); + } + + getItem(...args) { + return this.createAction('getItem', ...args); + } + + clear() { + return this.createAction('clear'); + } + + config(...args) { + return this.createAction('config', ...args); + } + + /** + * @export + * @param {string} command . + * @param {...*} args . + * @return {Promise} . + */ + createAction(command, ...args) { + const id = ++this.currentId_; + /** + * @type {Action} + */ + const action = { + 'plugin': 'localforage', + 'command': command, + 'args': args, + 'id': id + }; + const waitingPromise = {}; + const promise = new Promise((resolve, reject) => { + waitingPromise['resolve'] = resolve; + waitingPromise['reject'] = reject; + }); + this.waitingPromises_.set(id, waitingPromise); + this.postToBackend(action); + return promise; + } + + /** + * @export + * @param {*} event . + */ + receiveMessage(event) { + /** + * @type {Action} + */ + const action = event['data']; + const id = action['id']; + const command = action['command']; + const args = action['args'] || []; + const context = action['context']; + const msg = action['msg']; + + const waitingPromise = this.waitingPromises_.get(id); + if (command === 'error') { + console.error(msg, args, context); + if (waitingPromise) { + waitingPromise.reject(args, context); + this.waitingPromises_.delete(id); + } + } else if (command === 'response') { + waitingPromise.resolve(...args); + this.waitingPromises_.delete(id); + } else { + console.error('Unhandled command', JSON.stringify(action, null, '\t')); + } + } + + /** + * @abstract + * @protected + * @param {Action} action . + */ + postToBackend(action) { + } +}; + + +export default exports; diff --git a/src/offline/Configuration.js b/src/offline/Configuration.js index 1e9996b076f0..c3fba4565e1e 100644 --- a/src/offline/Configuration.js +++ b/src/offline/Configuration.js @@ -12,11 +12,14 @@ import olSourceImageWMS from 'ol/source/ImageWMS.js'; import olSourceTileWMS from 'ol/source/TileWMS.js'; import {createForProjection as createTileGridForProjection} from 'ol/tilegrid.js'; import SerializerDeserializer from 'ngeo/offline/SerializerDeserializer.js'; +import LocalforageCordovaWrapper from 'ngeo/offline/LocalforageCordovaWrapper.js'; +import LocalforageAndroidWrapper from 'ngeo/offline/LocalforageAndroidWrapper.js'; +import LocalforageIosWrapper from 'ngeo/offline/LocalforageIosWrapper.js'; import ngeoCustomEvent from 'ngeo/CustomEvent.js'; import utils from 'ngeo/offline/utils.js'; const defaultImageLoadFunction = olSourceImage.defaultImageLoadFunction; -import * as localforage from 'localforage'; +import * as realLocalforage from 'localforage'; /** * @implements {ngeox.OfflineOnTileDownload} @@ -31,19 +34,9 @@ const exports = class extends olObservable { */ constructor($rootScope, ngeoBackgroundLayerMgr, ngeoOfflineGutter) { super(); - localforage.config({ - 'name': 'ngeoOfflineStorage', - 'version': 1.0, - 'storeName': 'offlineStorage' - }); - /** - * @param {number} progress new progress. - */ - this.dispatchProgress_ = (progress) => { - this.dispatchEvent(new ngeoCustomEvent('progress', { - 'progress': progress - })); - }; + + this.localforage_ = this.createLocalforage(); + this.configureLocalforage(); /** * @private @@ -77,6 +70,16 @@ const exports = class extends olObservable { this.gutter_ = ngeoOfflineGutter; } + /** + * @private + * @param {number} progress new progress. + */ + dispatchProgress_(progress) { + this.dispatchEvent(new ngeoCustomEvent('progress', { + 'progress': progress + })); + } + /** * @protected */ @@ -85,6 +88,7 @@ const exports = class extends olObservable { } /** + * @export * @return {boolean} whether some offline data is available in the storage */ hasOfflineData() { @@ -113,12 +117,42 @@ const exports = class extends olObservable { return promise; } + createLocalforage() { + if (location.search.includes('localforage=cordova')) { + console.log('Using cordova localforage'); + return new LocalforageCordovaWrapper(); + } else if (location.search.includes('localforage=android')) { + console.log('Using android localforage'); + return new LocalforageAndroidWrapper(); + } else if (location.search.includes('localforage=ios')) { + console.log('Using ios localforage'); + return new LocalforageIosWrapper(); + } + return realLocalforage; + } + + configureLocalforage() { + this.localforage_.config({ + 'name': 'ngeoOfflineStorage', + 'version': 1.0, + 'storeName': 'offlineStorage' + }); + } + /** * @param {string} key The key * @return {Promise} A promise */ getItem(key) { - return this.traceGetSetItem('getItem', key, localforage.getItem(key)); + return this.traceGetSetItem('getItem', key, this.localforage_.getItem(key)); + } + + /** + * @param {string} key . + * @return {Promise} . + */ + removeItem(key) { + return this.traceGetSetItem('removeItem', key, this.localforage_.removeItem(key)); } /** @@ -127,7 +161,7 @@ const exports = class extends olObservable { * @return {Promise} A promise */ setItem(key, value) { - return this.traceGetSetItem('setItem', key, localforage.setItem(key, value)); + return this.traceGetSetItem('setItem', key, this.localforage_.setItem(key, value)); } /** @@ -135,7 +169,7 @@ const exports = class extends olObservable { */ clear() { this.setHasOfflineData(false); - return this.traceGetSetItem('clear', '', localforage.clear()); + return this.traceGetSetItem('clear', '', this.localforage_.clear()); } /** diff --git a/src/offline/LocalforageAndroidWrapper.js b/src/offline/LocalforageAndroidWrapper.js new file mode 100644 index 000000000000..ab2121b406c4 --- /dev/null +++ b/src/offline/LocalforageAndroidWrapper.js @@ -0,0 +1,33 @@ +/** + * @module ngeo.offline.LocalforageAndroidWrapper + */ +import AbstractWrapper from 'ngeo/offline/AbstractLocalforageWrapper.js'; + +const exports = class AndroidWrapper extends AbstractWrapper { + constructor() { + super(); + window['androidWrapper'] = this; + } + + /** + * @override + */ + postToBackend(action) { + const stringified = JSON.stringify(action); + window['ngeoHost']['postMessageToAndroid'](stringified); + } + + /** + * @export + * @param {string} actionString . + */ + receiveFromAndroid(actionString) { + const action = JSON.parse(actionString); + this.receiveMessage({ + 'data': action + }); + } +}; + + +export default exports; diff --git a/src/offline/LocalforageCordovaWrapper.js b/src/offline/LocalforageCordovaWrapper.js new file mode 100644 index 000000000000..7cccd05f4f01 --- /dev/null +++ b/src/offline/LocalforageCordovaWrapper.js @@ -0,0 +1,21 @@ +/** + * @module ngeo.offline.LocalforageCordovaWrapper + */ +import AbstractWrapper from 'ngeo/offline/AbstractLocalforageWrapper.js'; + +const exports = class CordovaWrapper extends AbstractWrapper { + constructor() { + super(); + window.addEventListener('message', this.receiveMessage.bind(this), false); + } + + /** + * @override + */ + postToBackend(action) { + window['parent'].postMessage(action, '*'); + } +}; + + +export default exports; diff --git a/src/offline/LocalforageIosWrapper.js b/src/offline/LocalforageIosWrapper.js new file mode 100644 index 000000000000..09435c3f39df --- /dev/null +++ b/src/offline/LocalforageIosWrapper.js @@ -0,0 +1,37 @@ +/** + * @module ngeo.offline.LocalforageIosWrapper + */ +import AbstractWrapper from 'ngeo/offline/AbstractLocalforageWrapper.js'; + +const exports = class IosWrapper extends AbstractWrapper { + constructor() { + super(); + window['iosWrapper'] = this; + } + + /** + * @override + */ + postToBackend(action) { + if (action['command'] === 'setItem') { + action['args'][1] = JSON.stringify(action['args'][1]); + } + const stringified = JSON.stringify(action); + window['webkit']['messageHandlers']['ios']['postMessage'](stringified); + } + + /** + * @export + * @param {string} actionString . + */ + receiveFromIos(actionString) { + const action = JSON.parse(actionString); + action['args'] = (action['args'] || []).map(item => JSON.parse(item)); + this.receiveMessage({ + 'data': action + }); + } +}; + + +export default exports; diff --git a/src/offline/TilesDownloader.js b/src/offline/TilesDownloader.js index 68b0533b0bf7..8d616c3ce3fd 100644 --- a/src/offline/TilesDownloader.js +++ b/src/offline/TilesDownloader.js @@ -184,8 +184,13 @@ const exports = class { }); googAsserts.assert(this.tiles_); - for (let i = 0; i < this.maxNumberOfWorkers_; ++i) { - this.downloadTile_(); + if (this.tiles_.length === 0) { + this.callbacks_.onTileDownloadError(1); // forcing progress update + this.resolvePromise_(); + } else { + for (let i = 0; i < this.maxNumberOfWorkers_; ++i) { + this.downloadTile_(); + } } return this.promise_; diff --git a/src/offline/component.js b/src/offline/component.js index 4514cc70e7ff..1b4811722d21 100644 --- a/src/offline/component.js +++ b/src/offline/component.js @@ -343,8 +343,8 @@ exports.Controller = class { validateExtent() { this.progressPercents = 0; const extent = this.getDowloadExtent_(); - this.ngeoOfflineServiceManager_.save(extent, this.map); this.downloading = true; + this.ngeoOfflineServiceManager_.save(extent, this.map); } diff --git a/src/search/createGeoJSONBloodhound.js b/src/search/createGeoJSONBloodhound.js index 6dec4b111339..2fe22c566b15 100644 --- a/src/search/createGeoJSONBloodhound.js +++ b/src/search/createGeoJSONBloodhound.js @@ -4,8 +4,7 @@ import olFormatGeoJSON from 'ol/format/GeoJSON.js'; import * as olObj from 'ol/obj.js'; -import Bloodhound from 'corejs-typeahead'; - +import 'corejs-typeahead'; /** * @param {string} url an URL to a search service. diff --git a/src/search/createLocationSearchBloodhound.js b/src/search/createLocationSearchBloodhound.js index 0012167a9ebe..feae5d3ae513 100644 --- a/src/search/createLocationSearchBloodhound.js +++ b/src/search/createLocationSearchBloodhound.js @@ -10,7 +10,7 @@ import ngeoProjEPSG21781 from 'ngeo/proj/EPSG21781.js'; import olGeomPoint from 'ol/geom/Point.js'; import olFeature from 'ol/Feature.js'; -import Bloodhound from 'corejs-typeahead'; +import 'corejs-typeahead'; /** From 34a8d917a0da77ebdc63139488cfc4285f603086 Mon Sep 17 00:00:00 2001 From: Laurent Lienher Date: Wed, 22 May 2019 11:44:40 +0200 Subject: [PATCH 8/8] Fix various typescript errors --- examples/offline.js | 3 ++- src/offline/AbstractLocalforageWrapper.js | 2 +- src/offline/Configuration.js | 21 +++++++++++---------- src/offline/Mode.js | 5 ++++- src/offline/NetworkStatus.js | 21 +++++++++++---------- src/offline/Restorer.js | 1 + src/offline/ServiceManager.js | 9 ++++++--- src/offline/component.js | 23 +++++++++++------------ src/offline/module.js | 4 +++- 9 files changed, 50 insertions(+), 39 deletions(-) diff --git a/examples/offline.js b/examples/offline.js index 6aea4faa1bb8..adff7405c894 100644 --- a/examples/offline.js +++ b/examples/offline.js @@ -14,6 +14,7 @@ import ngeoMapModule from 'ngeo/map/module.js'; import ngeoOfflineModule from 'ngeo/offline/module.js'; import ngeoOfflineConfiguration from 'ngeo/offline/Configuration.js'; import NgeoOfflineServiceManager from 'ngeo/offline/ServiceManager.js'; +import angular from 'angular'; // Useful to work on example - remove me later @@ -21,7 +22,7 @@ import 'bootstrap/js/modal.js'; import 'jquery-ui/ui/widgets/resizable.js'; import 'jquery-ui/ui/widgets/draggable.js'; -/** @type {!angular.Module} **/ +/** @type {!angular.IModule} **/ exports.module = angular.module('app', [ 'gettext', ngeoMapModule.name, diff --git a/src/offline/AbstractLocalforageWrapper.js b/src/offline/AbstractLocalforageWrapper.js index e04128ab5d76..f5e76d4a905f 100644 --- a/src/offline/AbstractLocalforageWrapper.js +++ b/src/offline/AbstractLocalforageWrapper.js @@ -18,7 +18,7 @@ let Action; */ const exports = class AbstractLocalforageWrapper { constructor() { - this.waitingPromises_ = new window.Map(); + this.waitingPromises_ = new Map(); this.currentId_ = 0; } diff --git a/src/offline/Configuration.js b/src/offline/Configuration.js index c3fba4565e1e..75fa0c65e1fd 100644 --- a/src/offline/Configuration.js +++ b/src/offline/Configuration.js @@ -7,7 +7,7 @@ import olLayerVector from 'ol/layer/Vector.js'; import olLayerTile from 'ol/layer/Tile.js'; import olLayerImage from 'ol/layer/Image.js'; import * as olProj from 'ol/proj.js'; -import olSourceImage from 'ol/source/Image.js'; +import {defaultImageLoadFunction} from 'ol/source/Image.js'; import olSourceImageWMS from 'ol/source/ImageWMS.js'; import olSourceTileWMS from 'ol/source/TileWMS.js'; import {createForProjection as createTileGridForProjection} from 'ol/tilegrid.js'; @@ -17,7 +17,7 @@ import LocalforageAndroidWrapper from 'ngeo/offline/LocalforageAndroidWrapper.js import LocalforageIosWrapper from 'ngeo/offline/LocalforageIosWrapper.js'; import ngeoCustomEvent from 'ngeo/CustomEvent.js'; import utils from 'ngeo/offline/utils.js'; -const defaultImageLoadFunction = olSourceImage.defaultImageLoadFunction; +const defaultImageLoadFunction_ = defaultImageLoadFunction; import * as realLocalforage from 'localforage'; @@ -28,8 +28,9 @@ const exports = class extends olObservable { /** * @ngInject - * @param {!angular.Scope} $rootScope The rootScope provider. - * @param {ngeo.map.BackgroundLayerMgr} ngeoBackgroundLayerMgr The background layer manager + * @param {!angular.IScope} $rootScope The rootScope provider. + * @param {!import("ngeo/map/BackgroundLayerMgr.js").MapBackgroundLayerManager} ngeoBackgroundLayerMgr + * Background layer manager. * @param {number} ngeoOfflineGutter A gutter around the tiles to download (to avoid cut symbols) */ constructor($rootScope, ngeoBackgroundLayerMgr, ngeoOfflineGutter) { @@ -40,7 +41,7 @@ const exports = class extends olObservable { /** * @private - * @type {!angular.Scope} + * @type {!angular.IScope} */ this.rootScope_ = $rootScope; @@ -53,7 +54,7 @@ const exports = class extends olObservable { /** * @private - * @type {ngeo.map.BackgroundLayerMgr} + * @type {!import("ngeo/map/BackgroundLayerMgr.js").MapBackgroundLayerManager} */ this.ngeoBackgroundLayerMgr_ = ngeoBackgroundLayerMgr; @@ -99,7 +100,7 @@ const exports = class extends olObservable { * @param {boolean} value whether there is offline data available in the storage. */ setHasOfflineData(value) { - const needDigest = value ^ this.hasData; + const needDigest = value !== this.hasData; this.hasData = value; if (needDigest) { this.rootScope_.$applyAsync(); // force update of the UI @@ -181,7 +182,7 @@ const exports = class extends olObservable { } /** - * @param {ngeox.OfflineLayerMetadata} layerItem The layer metadata + * @param {OfflineLayerMetadata} layerItem The layer metadata * @return {string} A key identifying an offline layer and used during restore. */ getLayerKey(layerItem) { @@ -191,7 +192,7 @@ const exports = class extends olObservable { /** * @override * @param {number} progress The download progress - * @param {ngeox.OfflineTile} tile The tile + * @param {OfflineTile} tile The tile * @return {Promise} A promise */ onTileDownloadSuccess(progress, tile) { @@ -241,7 +242,7 @@ const exports = class extends olObservable { * @return {ol.source.Source} A tiled equivalent source */ sourceImageWMSToTileWMS(source, projection) { - if (source instanceof olSourceImageWMS && source.getUrl() && source.getImageLoadFunction() === defaultImageLoadFunction) { + if (source instanceof olSourceImageWMS && source.getUrl() && source.getImageLoadFunction() === defaultImageLoadFunction_) { const tileGrid = createTileGridForProjection(source.getProjection() || projection, 42, 256); source = new olSourceTileWMS({ gutter: this.gutter_, diff --git a/src/offline/Mode.js b/src/offline/Mode.js index f61292478be4..2ab306310ac6 100644 --- a/src/offline/Mode.js +++ b/src/offline/Mode.js @@ -2,6 +2,9 @@ * @module ngeo.offline.Mode */ +import angular from 'angular'; + + const exports = class { /** @@ -77,7 +80,7 @@ const exports = class { }; /** - * @type {!angular.Module} + * @type {!angular.IModule} */ exports.module = angular.module('ngeoOfflineMode', []); exports.module.service('ngeoOfflineMode', exports); diff --git a/src/offline/NetworkStatus.js b/src/offline/NetworkStatus.js index 71ed69c0c677..c6ffea650910 100644 --- a/src/offline/NetworkStatus.js +++ b/src/offline/NetworkStatus.js @@ -2,14 +2,15 @@ * @module ngeo.offline.NetworkStatus */ import ngeoMiscDebounce from 'ngeo/misc/debounce.js'; +import angular from 'angular'; /** * @ngInject - * @param {angular.$q} $q The Angular $q service. - * @param {ngeox.miscDebounce} ngeoDebounce ngeo debounce service. + * @param {angular.IQService} $q The Angular $q service. + * @param {ngeoMiscDebounce} ngeoDebounce ngeo debounce service. * @param {ngeo.offline.NetworkStatus} ngeoNetworkStatus ngeo network status service. - * @return {angular.$http.Interceptor} the interceptor + * @return {angular.IHttpInterceptor} the interceptor */ const httpInterceptor = function($q, ngeoDebounce, ngeoNetworkStatus) { const debouncedCheck = ngeoDebounce(() => ngeoNetworkStatus.check(undefined), 2000, false); @@ -48,9 +49,9 @@ const Service = class { * * @ngInject * @param {!jQuery} $document Angular document service. - * @param {angular.$window} $window Angular window service. - * @param {!angular.$timeout} $timeout Angular timeout service. - * @param {angular.Scope} $rootScope The root scope. + * @param {angular.IWindowService} $window Angular window service. + * @param {angular.ITimeoutService} $timeout Angular timeout service. + * @param {angular.IScope} $rootScope The root scope. * @param {string} ngeoOfflineTestUrl Url of the test page. */ constructor($document, $window, $timeout, $rootScope, ngeoOfflineTestUrl) { @@ -69,13 +70,13 @@ const Service = class { /** * @private - * @type {!angular.$timeout} + * @type {!angular.ITimeoutService} */ this.$timeout_ = $timeout; /** * @private - * @type {angular.Scope} + * @type {angular.IScope} */ this.$rootScope_ = $rootScope; @@ -99,7 +100,7 @@ const Service = class { /** * @private - * @type {angular.$q.Promise|undefined} + * @type {angular.IPromise|undefined} */ this.promise_; @@ -197,7 +198,7 @@ Service.module.service(name, Service); /** * @ngInject * @private - * @param {angular.$HttpProvider} $httpProvider . + * @param {angular.IHttpProvider} $httpProvider . */ Service.module.configFunction_ = function($httpProvider) { $httpProvider.interceptors.push('httpInterceptor'); diff --git a/src/offline/Restorer.js b/src/offline/Restorer.js index af9572db9d2b..2f98a75e34fc 100644 --- a/src/offline/Restorer.js +++ b/src/offline/Restorer.js @@ -2,6 +2,7 @@ * @module ngeo.offline.Restorer */ import ngeoMapBackgroundLayerMgr from 'ngeo/map/BackgroundLayerMgr.js'; +import angular from 'angular'; class Restorer { diff --git a/src/offline/ServiceManager.js b/src/offline/ServiceManager.js index 884c2334a701..08c1d07f8446 100644 --- a/src/offline/ServiceManager.js +++ b/src/offline/ServiceManager.js @@ -2,10 +2,13 @@ * @module ngeo.offline.ServiceManager */ +import angular from 'angular'; + + const exports = class { /** - * @param {angular.$injector} $injector Main injector. + * @param {angular.auto.IInjectorService} $injector Main injector. * @struct * @ngInject * @ngdoc service @@ -14,7 +17,7 @@ const exports = class { constructor($injector) { /** - * @type {angular.$injector} + * @type {angular.auto.IInjectorService} * @private */ this.$injector_ = $injector; @@ -109,7 +112,7 @@ const exports = class { }; /** - * @type {!angular.Module} + * @type {!angular.IModule} */ exports.module = angular.module('ngeoOfflineServiceManager', []); exports.module.service('ngeoOfflineServiceManager', exports); diff --git a/src/offline/component.js b/src/offline/component.js index 1b4811722d21..6d5f89aa68af 100644 --- a/src/offline/component.js +++ b/src/offline/component.js @@ -10,19 +10,20 @@ import olFeature from 'ol/Feature.js'; import olGeomPolygon from 'ol/geom/Polygon.js'; import olGeomGeometryLayout from 'ol/geom/GeometryLayout.js'; import {DEVICE_PIXEL_RATIO} from 'ol/has.js'; +import angular from 'angular'; /** - * @type {!angular.Module} + * @type {!angular.IModule} */ const exports = angular.module('ngeoOffline', [ - ngeoMapFeatureOverlayMgr.module.name, + ngeoMapFeatureOverlayMgr.name, ngeoMessageModalComponent.name ]); exports.value('ngeoOfflineTemplateUrl', /** - * @param {angular.JQLite} element Element. - * @param {angular.Attributes} attrs Attributes. + * @param {JQuery} element Element. + * @param {angular.IAttributes} attrs Attributes. * @return {string} Template URL. */ (element, attrs) => { @@ -36,9 +37,9 @@ exports.run(/* @ngInject */ ($templateCache) => { }); /** - * @param {!angular.JQLite} $element Element. - * @param {!angular.Attributes} $attrs Attributes. - * @param {!function(!angular.JQLite, !angular.Attributes): string} ngeoOfflineTemplateUrl Template function. + * @param {!JQuery} $element Element. + * @param {!angular.IAttributes} $attrs Attributes. + * @param {!function(!JQuery, !angular.IAttributes): string} ngeoOfflineTemplateUrl Template function. * @return {string} Template URL. * @ngInject */ @@ -91,7 +92,7 @@ exports.Controller = class { /** * @private - * @param {angular.$timeout} $timeout Angular timeout service. + * @param {angular.ITimeoutService} $timeout Angular timeout service. * @param {ngeo.map.FeatureOverlayMgr} ngeoFeatureOverlayMgr ngeo feature overlay manager service. * @param {ngeo.offline.ServiceManager} ngeoOfflineServiceManager ngeo offline service Manager. * @param {ngeo.offline.Configuration} ngeoOfflineConfiguration ngeo offline configuration service. @@ -104,7 +105,7 @@ exports.Controller = class { constructor($timeout, ngeoFeatureOverlayMgr, ngeoOfflineServiceManager, ngeoOfflineConfiguration, ngeoOfflineMode, ngeoNetworkStatus) { /** - * @type {angular.$timeout} + * @type {angular.ITimeoutService} * @private */ this.$timeout_ = $timeout; @@ -500,8 +501,6 @@ exports.Controller = class { const viewportWidth = frameState.size[0] * frameState.pixelRatio; const viewportHeight = frameState.size[1] * frameState.pixelRatio; - const center = [viewportWidth / 2, viewportHeight / 2]; - const extentLength = this.extentSize ? this.extentSize / resolution * DEVICE_PIXEL_RATIO : Math.min(viewportWidth, viewportHeight) - this.maskMargin * 2; @@ -518,7 +517,7 @@ exports.Controller = class { context.closePath(); // Draw the get data zone - const extent = this.createExtent_(center, extentHalfLength); + const extent = this.createExtent_([viewportWidth / 2, viewportHeight / 2], extentHalfLength); context.moveTo(extent[0], extent[1]); context.lineTo(extent[0], extent[3]); diff --git a/src/offline/module.js b/src/offline/module.js index e8a2bdc13602..0f360a9f276d 100644 --- a/src/offline/module.js +++ b/src/offline/module.js @@ -7,9 +7,11 @@ import ngeoOfflineServiceManager from 'ngeo/offline/ServiceManager.js'; import downloader from 'ngeo/offline/Downloader.js'; import restorer from 'ngeo/offline/Restorer.js'; import mode from 'ngeo/offline/Mode.js'; +import angular from 'angular'; + /** - * @type {!angular.Module} + * @type {!angular.IModule} */ const exports = angular.module('ngeoOfflineModule', [ ngeoOfflineComponent.name,