diff --git a/grails-app/assets/javascripts/MapUtilities.js b/grails-app/assets/javascripts/MapUtilities.js
index b33f28484..1f29f251b 100644
--- a/grails-app/assets/javascripts/MapUtilities.js
+++ b/grails-app/assets/javascripts/MapUtilities.js
@@ -191,11 +191,11 @@ Biocollect.MapUtilities = {
break;
case 'maptilersatellite':
option = {
- url: url || 'https://api.maptiler.com/maps/hybrid/{z}/{x}/{y}.jpg?key=O11Deo7fBLatChkUYGIH',
+ url: url || 'https://api.maptiler.com/maps/hybrid/256/{z}/{x}/{y}.jpg?key=O11Deo7fBLatChkUYGIH',
options: {
attribution: '© MapTiler © OpenStreetMap contributors',
maxZoom: 21,
- maxNativeZoom: 13
+ maxNativeZoom: 21
}
};
layer = L.tileLayer(option.url, option.options);
diff --git a/grails-app/assets/javascripts/pwa-index.js b/grails-app/assets/javascripts/pwa-index.js
index b5b5ea8dc..5cd13d8cd 100644
--- a/grails-app/assets/javascripts/pwa-index.js
+++ b/grails-app/assets/javascripts/pwa-index.js
@@ -1,8 +1,9 @@
async function downloadMapTiles(bounds, tileUrl, minZoom, maxZoom, callback) {
minZoom = minZoom || 0; // Minimum zoom level
maxZoom = maxZoom || 20; // Maximum zoom level
+ const MAX_PARALLEL_REQUESTS = 10;
- var deferred = $.Deferred();
+ var deferred = $.Deferred(), requestArray = [];
// Check if the browser supports the Cache API
if ('caches' in window) {
// Function to fetch and cache the vector basemap tiles for a bounding box at different zoom levels
@@ -30,14 +31,19 @@ async function downloadMapTiles(bounds, tileUrl, minZoom, maxZoom, callback) {
if (!cachedResponse) {
console.log(`Tile at zoom ${zoom}, x ${x}, y ${y} not found in cache. Fetching and caching...`);
- // Fetch the tile from the server
- const response = await fetch(requestUrl);
-
- // Clone the response, as it can only be consumed once
- const responseClone = response.clone();
+ // run x number of queries in parallel
+ if (requestArray.length <= MAX_PARALLEL_REQUESTS) {
+ requestArray.push(fetch(requestUrl).then(function (response) {
+ // Clone the response, as it can only be consumed once
+ const responseClone = response.clone();
- // Cache the response
- await cache.put(requestUrl, responseClone);
+ // Cache the response
+ cache.put(requestUrl, responseClone);
+ }));
+ } else {
+ await Promise.all(requestArray);
+ requestArray = [];
+ }
console.log(`Tile at zoom ${zoom}, x ${x}, y ${y} cached.`);
} else {
@@ -52,6 +58,9 @@ async function downloadMapTiles(bounds, tileUrl, minZoom, maxZoom, callback) {
}
}
+ if (requestArray.length > 0) {
+ await Promise.all(requestArray);
+ }
console.log('Vector basemap tiles cached for the bounding box.');
deferred.resolve();
} catch (error) {
@@ -184,18 +193,25 @@ function OfflineViewModel(config) {
minZoom = config.minZoom || 0,
maxZoom = config.maxZoom || 20,
mapId = config.mapId,
+ overlayLayersMapControlConfig = Biocollect.MapUtilities.getOverlayConfig(),
mapOptions = {
autoZIndex: false,
+ zoomToObject: true,
preserveZIndex: true,
addLayersControlHeading: false,
drawControl: false,
showReset: false,
draggableMarkers: false,
useMyLocation: true,
+ maxAutoZoom: maxZoom,
+ maxZoom: maxZoom,
+ minZoom: minZoom,
allowSearchLocationByAddress: true,
allowSearchRegionByAddress: true,
trackWindowHeight: false,
- baseLayer: L.tileLayer(config.baseMapUrl, config.baseMapOptions)
+ baseLayer: L.tileLayer(config.baseMapUrl, config.baseMapOptions),
+ wmsFeatureUrl: overlayLayersMapControlConfig.wmsFeatureUrl,
+ wmsLayerUrl: overlayLayersMapControlConfig.wmsLayerUrl
},
alaMap = new ALA.Map(mapId, mapOptions),
mapImpl = alaMap.getMapImpl(),
@@ -390,33 +406,90 @@ function OfflineViewModel(config) {
return area;
}
+ /**
+ * Downloads base map tiles and wms layer of a site for offline use.
+ * It is done for all sites of a project activity.
+ * @returns {Promise}
+ */
async function startDownloadingSites() {
- var sites = pa.sites || [], zoom = 15;
+ const TIMEOUT = 3000, // 3 seconds
+ MAP_LOAD_TIMEOUT = 2000, // 2 seconds
+ MAX_ZOOM=20,
+ MIN_ZOOM= 10;
+ var sites = pa.sites || [], zoom = 15, mapZoomedInIndicator, tileLoadedPromise, cancelTimer,
+ callback = function () {
+ cancelTimer && clearTimeout(cancelTimer);
+ cancelTimer = null;
+ // resolve it in the next event loop
+ if(mapZoomedInIndicator && mapZoomedInIndicator.state() == 'pending') {
+ // setTimeout(function () {
+ mapZoomedInIndicator && mapZoomedInIndicator.resolve();
+ // }, 0);
+ }
+ };
self.currentStage(self.stages.sites);
self.sitesStatus(self.statuses.doing);
+ alaMap.registerListener('dataload', callback);
+
try {
self.numberOfSiteTilesDownloaded(0);
self.totalSiteTilesDownload(sites.length);
for (var i = 0; i < sites.length; i++) {
var site = sites[i],
+ zoomIntoMap = true,
geoJson = Biocollect.MapUtilities.featureToValidGeoJson(site.extent.geometry),
- layer = L.geoJson(geoJson),
- bounds = layer.getBounds();
-
- alaMap.addLayer(layer);
- alaMap.fitBounds();
- zoom = mapImpl.getZoom();
- alaMap.removeLayer(layer);
- await downloadMapTiles(bounds, config.baseMapUrl, zoom, zoom);
+ geoJsonLayer = alaMap.setGeoJSON(geoJson, {
+ wmsFeatureUrl: overlayLayersMapControlConfig.wmsFeatureUrl,
+ wmsLayerUrl: overlayLayersMapControlConfig.wmsLayerUrl,
+ maxZoom: MAX_ZOOM
+ });
+
+ geoJsonLayer.on('tileload', function () {
+ if(tileLoadedPromise && tileLoadedPromise.state() == 'pending') {
+ tileLoadedPromise.resolve();
+ }
+ });
+ // so that layer zooms beyond default max zoom of 18
+ geoJsonLayer.options.maxZoom = MAX_ZOOM;
+ mapZoomedInIndicator = $.Deferred();
+ // cancel waiting for map to load feature data
+ cancelTimer = setTimeout(function (){
+ zoomIntoMap = false;
+ mapZoomedInIndicator && mapZoomedInIndicator.resolve();
+ }, TIMEOUT);
+
+ // no need to wait if promise is resolved.
+ if (mapZoomedInIndicator && mapZoomedInIndicator.state() == 'pending') {
+ // wait for map layer to load feature data from spatial server for pid.
+ await mapZoomedInIndicator.promise();
+ }
+
+ if (zoomIntoMap) {
+ // zoom into to map to get tiles and feature from spatial server
+ for (zoom = MIN_ZOOM; zoom <= MAX_ZOOM; zoom++) {
+ tileLoadedPromise = $.Deferred();
+ mapImpl.setZoom(zoom, {animate: false});
+ timer(MAP_LOAD_TIMEOUT, tileLoadedPromise);
+ await tileLoadedPromise.promise();
+ }
+ }
+
+ alaMap.clearLayers();
self.numberOfSiteTilesDownloaded(self.numberOfSiteTilesDownloaded() + 1);
}
+ alaMap.removeListener('dataload', callback);
completedSitesDownload();
} catch (e) {
+ console.error(e);
errorSitesDownload();
}
}
+ function timer(ms, deferred) {
+ return setTimeout(deferred.resolve, ms);
+ }
+
function completedSitesDownload() {
updateSitesProgressBar(self.totalCount(), self.totalCount());
self.sitesStatus(self.statuses.done);
diff --git a/grails-app/assets/javascripts/sw.js b/grails-app/assets/javascripts/sw.js
index e0a86d84d..75244aa4c 100644
--- a/grails-app/assets/javascripts/sw.js
+++ b/grails-app/assets/javascripts/sw.js
@@ -54,7 +54,11 @@ self.addEventListener('fetch', e => {
return res;
}
else if (isFetchingBaseMap(e.request.url)) {
- return fetch(pwaConfig.noCacheTileFile);
+ return caches.match(pwaConfig.noCacheTileFile).then(res => {
+ if (res) {
+ return res;
+ }
+ });
}
});
}
@@ -97,6 +101,11 @@ function isFetchingBaseMap (url) {
async function precache() {
const cache = await caches.open(pwaConfig.cacheName);
+
+ for(var i = 0; i < pwaConfig.filesToPreCache.length; i++) {
+ await cache.delete(pwaConfig.filesToPreCache[i]);
+ }
+
return cache.addAll(pwaConfig.filesToPreCache);
}
console.debug("SW Script: end reading");
\ No newline at end of file
diff --git a/grails-app/conf/application.groovy b/grails-app/conf/application.groovy
index 62830ce6d..00e855417 100644
--- a/grails-app/conf/application.groovy
+++ b/grails-app/conf/application.groovy
@@ -676,6 +676,9 @@ if (!app.file.script.path) {
script.read.extensions.list = ['js','min.js','png', 'json', 'jpg', 'jpeg']
// yml interpreter doesn't evaluate expression in deep nested objects such as baseLayers below
-if (pwa.mapConfig.baseLayers?.size() > 1) {
- pwa.mapConfig.baseLayers[0].url = pwa.baseMapUrl
+pwaMapConfig = { def config ->
+ Map pwa = config.getProperty('pwa', Map)
+ Map mapConfig = pwa.mapConfig
+ mapConfig.baseLayers.getAt(0).url = pwa.baseMapUrl + pwa.apiKey
+ mapConfig
}
\ No newline at end of file
diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml
index 81ebd051f..7c4606799 100644
--- a/grails-app/conf/application.yml
+++ b/grails-app/conf/application.yml
@@ -315,7 +315,7 @@ pwa:
cache:
ignore: ["/image/upload", "/ws/attachment/upload"]
maxAreaInKm: 25
- tileSize: 512
+ tileSize: 256
apiKey: ""
cacheVersion: "v3"
oldCacheToDelete: "v2"
@@ -323,14 +323,15 @@ pwa:
pathsToIgnoreCache: [ "/image/upload", "/ws/attachment/upload", "/ajax/keepSessionAlive", "/noop", '/pwa/sw.js', '/pwa/config.js', "/ws/species/speciesDownload" ]
cachePathForRequestsStartingWith: [ "/pwa/bioActivity/edit/", "/pwa/createOrEditFragment/", "/pwa/bioActivity/index/", "/pwa/indexFragment/", "/pwa/offlineList" ]
filesToPreCache: ["webjars/leaflet/0.7.7/dist/images/layers.png", "webjars/leaflet/0.7.7/dist/images/layers-2x.png", "webjars/leaflet/0.7.7/dist/images/marker-icon.png", "webjars/leaflet/0.7.7/dist/images/marker-icon-2x.png", "webjars/leaflet/0.7.7/dist/images/marker-shadow.png", "map-not-cached.png", "font-awesome/5.15.4/svgs/regular/image.svg"]
- baseMapPrefixUrl: "https://api.maptiler.com/maps/hybrid"
+ baseMapPrefixUrl: "https://api.maptiler.com/maps/hybrid/256"
noCacheTileFile: "map-not-cached.png"
- baseMapUrl: "${pwa.serviceWorkerConfig.baseMapPrefixUrl}/{z}/{x}/{y}.jpg?key=${pwa.apiKey}"
+ baseMapUrl: "${pwa.serviceWorkerConfig.baseMapPrefixUrl}/{z}/{x}/{y}.jpg?key="
mapConfig:
baseLayers:
- code: 'maptilersatellite'
displayText: 'Satellite'
isSelected: true
+ attribution: '© MapTiler © OpenStreetMap contributors'
overlays: [ ]
---
diff --git a/grails-app/views/bioActivity/pwa.gsp b/grails-app/views/bioActivity/pwa.gsp
index 98c8754be..e26fcfb0d 100644
--- a/grails-app/views/bioActivity/pwa.gsp
+++ b/grails-app/views/bioActivity/pwa.gsp
@@ -25,9 +25,15 @@
var params = getParams(), initialized = false;
var fcConfig = {
+ intersectService: "${createLink(controller: 'proxy', action: 'intersect')}",
+ featuresService: "${createLink(controller: 'proxy', action: 'features')}",
+ featureService: "${createLink(controller: 'proxy', action: 'feature')}",
+ spatialWms: "${grailsApplication.config.spatial.geoserverUrl}",
+ layersStyle: "${createLink(controller: 'regions', action: 'layersStyle')}",
createActivityUrl: "/pwa/bioActivity/edit/" + params.projectActivityId + "?cache=true",
indexActivityUrl: "/pwa/bioActivity/index/" + params.projectActivityId+ "?cache=true",
- baseMapUrl: "${grailsApplication.config.getProperty("pwa.baseMapUrl")}",
+ baseMapUrl: "${grailsApplication.config.getProperty("pwa.baseMapUrl")}${grailsApplication.config.getProperty("pwa.apiKey")}",
+ baseMapAttribution: "${grailsApplication.config.getProperty("pwa.mapConfig.baseLayers", List)?.getAt(0)?.attribution?.encodeAsJavaScript()}",
fetchSpeciesUrl: "${createLink(controller: 'search', action: 'searchSpecies')}",
metadataURL: "/ws/projectActivity/activity",
siteUrl: '${createLink(controller: 'site', action: 'index' )}',
@@ -78,7 +84,7 @@
totalUrl: fcConfig.totalUrl,
downloadSpeciesUrl: fcConfig.downloadSpeciesUrl,
baseMapOptions: {
- attribution: 'Tiles from Esri — Sources: Esri, DigitalGlobe, Earthstar Geographics, CNES/Airbus DS, GeoEye, USDA FSA, USGS, Aerogrid, IGN, IGP, and the GIS User Community',
+ attribution: fcConfig.baseMapAttribution,
maxZoom: 20
}
});
diff --git a/grails-app/views/bioActivity/pwaBioActivityCreateOrEdit.gsp b/grails-app/views/bioActivity/pwaBioActivityCreateOrEdit.gsp
index 345b072b0..3d76d8036 100644
--- a/grails-app/views/bioActivity/pwaBioActivityCreateOrEdit.gsp
+++ b/grails-app/views/bioActivity/pwaBioActivityCreateOrEdit.gsp
@@ -53,7 +53,7 @@
getOutputSpeciesIdUrl : "${createLink(controller: 'output', action: 'getOutputSpeciesIdentifier')}",
getGuidForOutputSpeciesUrl : "${createLink(controller: 'record', action: 'getGuidForOutputSpeciesIdentifier')}",
imageLeafletViewer: '${createLink(controller: 'resource', action: 'imageviewer', absolute: true)}',
- mapLayersConfig: ${ grailsApplication.config.getProperty('pwa.mapConfig', Map) as JSON },
+ mapLayersConfig: ${ grailsApplication.config.getProperty('pwaMapConfig', Closure)(grailsApplication.config) as JSON },
excelOutputTemplateUrl: "${createLink(controller: 'proxy', action:'excelOutputTemplate')}",
uploadImagesUrl: "${createLink(controller: 'image', action: 'upload')}",
originUrl: "${grailsApplication.config.getProperty('server.serverURL')}",
diff --git a/grails-app/views/bioActivity/pwaBioActivityIndex.gsp b/grails-app/views/bioActivity/pwaBioActivityIndex.gsp
index 786236e71..81e564180 100644
--- a/grails-app/views/bioActivity/pwaBioActivityIndex.gsp
+++ b/grails-app/views/bioActivity/pwaBioActivityIndex.gsp
@@ -44,7 +44,7 @@
speciesProfileUrl: "${createLink(controller: 'proxy', action: 'speciesProfile')}",
noImageUrl: '${asset.assetPath(src: "font-awesome/5.15.4/svgs/regular/image.svg")}',
speciesImageUrl:"${createLink(controller:'species', action:'speciesImage')}",
- mapLayersConfig: ${ grailsApplication.config.getProperty('pwa.mapConfig', Map) as JSON },
+ mapLayersConfig: ${ grailsApplication.config.getProperty('pwaMapConfig', Closure)(grailsApplication.config) as JSON },
excelOutputTemplateUrl: "${createLink(controller: 'proxy', action:'excelOutputTemplate')}",
pwaAppUrl: "${grailsApplication.config.getProperty('pwa.appUrl')}",
bulkUpload: false,