Skip to content

Commit

Permalink
wip: Allow to make layers available offline - fixed various issues (#833
Browse files Browse the repository at this point in the history
)
  • Loading branch information
claustres committed Oct 9, 2024
1 parent ec66da0 commit 617216f
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 43 deletions.
2 changes: 1 addition & 1 deletion core/client/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export function createClient (config) {
// Set required default hooks
hooks: _.defaultsDeep(_.get(options, 'hooks'), {
before: {
all: [hooks.removeServerSideParameters],
all: [hooks.ensureSerializable, hooks.removeServerSideParameters],
create: [hooks.generateId]
}
}),
Expand Down
11 changes: 8 additions & 3 deletions core/client/composables/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,15 @@ export function useSession (options = {}) {
api.isDisconnected = true
Events.emit('websocket-disconnected')
logger.error(new Error('Socket has been disconnected'))
// Disconnect prompt can be avoided, eg in tests
if (!LocalStorage.get(disconnectKey, true)) return
// This will ensure any operation in progress will not keep a "dead" loading indicator
// as this error might appear under-the-hood without notifying service operations
Loading.hide()
// Disconnect prompt can be avoided, eg in tests
if (!LocalStorage.get(disconnectKey, true)) {
// We flag however that we are waiting for reconnection to avoid emitting the event multiple times
pendingReconnection = true
return
}
pendingReconnection = $q.dialog({
title: i18n.t('composables.session.ALERT'),
message: i18n.t('composables.session.DISCONNECT'),
Expand Down Expand Up @@ -116,7 +120,8 @@ export function useSession (options = {}) {
function onReconnect () {
// Dismiss pending reconnection error message if any
if (pendingReconnection) {
pendingReconnection.hide()
// If reconnection prompt is avoided we simply have a boolean flag instead of a dismiss dialog function
if (typeof pendingReconnection.hide === 'function') pendingReconnection.hide()
pendingReconnection = null
}
ignoreReconnectionError = false
Expand Down
17 changes: 16 additions & 1 deletion core/client/hooks/hooks.offline.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,20 @@ export function removeServerSideParameters(context) {
}

export function generateId(context) {
if (!context.data._id) context.data._id = uid().toString()
const params = context.params
// Generate ID only when not doing a snapshot because it should keep data as it is on the server side
// Typically some services like the catalog deliver objects without any IDs (as directly coming from the configuration not the database)
// and this property is used to make some difference in the way the GUI interpret this objects
if (params.snapshot) return
const data = (Array.isArray(context.data) ? context.data : [context.data])
// Update only items without any ID
data.filter(item => !item._id).forEach(item => {
item._id = uid().toString()
})
}

export function ensureSerializable(context) {
// Serialization in localforage will raise error with structures that are not raw JSON:
// JS proxy objects, function properties, etc.
if (context.data) context.data = _.cloneDeep(context.data)
}
4 changes: 2 additions & 2 deletions core/common/utils.offline.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function makeServiceSnapshot (service, options) {
let result = await service.find({ query })
let data = _.get(result, dataPath) || result
// No pagination or first page
if (offlineService) await offlineService.create(data)
if (offlineService) await offlineService.create(data, { addId: false, snapshot: true })
items = items.concat(data)
// No pagination => stop here
if (!_.get(result, dataPath)) return items
Expand All @@ -28,7 +28,7 @@ export async function makeServiceSnapshot (service, options) {
result = await service.find({ query })
data = _.get(result, dataPath)
if (offlineService) {
await offlineService.create(data)
await offlineService.create(data, { addId: false, snapshot: true })
} else {
items = items.concat(data)
}
Expand Down
2 changes: 1 addition & 1 deletion map/client/components/catalog/KViewsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export default {
icon: 'las la-trash-alt',
message: i18n.t('KViewsPanel.UNCACHING_VIEW'),
color: 'primary',
timeout: 3000
timeout: 0
})
await uncacheView(view, this.getProjectLayers(), {
contextId: this.kActivity.contextId
Expand Down
2 changes: 1 addition & 1 deletion map/client/i18n/map_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@
"KCreateOfflineView": {
"MIN_ZOOM_FIELD_LABEL": "Zoom level from which data will be downloaded, increase will reduce data size",
"MAX_ZOOM_FIELD_LABEL": "Zoom level until which data will be downloaded, increase will increase data size",
"NB_CONCURRENT_REQUESTS_FIELD_LABEL": "Number of concurrent requests sued to download, increase will speed up download but load the device and the server"
"NB_CONCURRENT_REQUESTS_FIELD_LABEL": "Number of concurrent requests issued to download, increase will speed up download but load the device and the server"
},
"KProjectsPanel": {
"SORT_PROJECTS": "Sort projects",
Expand Down
5 changes: 2 additions & 3 deletions map/client/init.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import _ from 'lodash'
import { LocalForage } from '@kalisio/feathers-localforage'
import config from 'config'
import { reactive } from 'vue'
import logger from 'loglevel'
Expand Down Expand Up @@ -35,8 +34,8 @@ export function setupApi (configuration) {
// Set required default hooks and data path for snapshot as the service responds in GeoJson format
hooks: _.defaultsDeep(_.get(options, 'hooks'), {
before: {
all: [kCoreHooks.removeServerSideParameters, kMapHooks.removeServerSideParameters],
create: kMapHooks.referenceCountCreateHook,
all: [kCoreHooks.ensureSerializable, kCoreHooks.removeServerSideParameters, kMapHooks.removeServerSideParameters],
create: [kCoreHooks.generateId, kMapHooks.referenceCountCreateHook],
remove: kMapHooks.referenceCountRemoveHook
},
after: {
Expand Down
40 changes: 19 additions & 21 deletions map/client/mixins/mixin.activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,34 +245,32 @@ export const activity = {
this.catalogService = this.$api.getService('catalog')
// Keep track of binded listeners as we use the same function with different contexts
this.catalogListeners = {}
if (this.globalCatalogService.events) {
this.globalCatalogService.events.forEach(event => {
// Avoid reentrance
if (!this.catalogListeners[event]) {
this.catalogListeners[event] = (object) => this.onCatalogUpdated(event, object)
this.globalCatalogService.on(event, this.catalogListeners[event])
if (this.catalogService && (this.catalogService !== this.globalCatalogService)) {
this.catalogService.on(event, this.catalogListeners[event])
}
const events = ['created', 'updated', 'patched', 'removed']
events.forEach(event => {
// Avoid reentrance
if (!this.catalogListeners[event]) {
this.catalogListeners[event] = (object) => this.onCatalogUpdated(event, object)
this.globalCatalogService.on(event, this.catalogListeners[event])
if (this.catalogService && (this.catalogService !== this.globalCatalogService)) {
this.catalogService.on(event, this.catalogListeners[event])
}
})
}
}
})
},
unlistenToCatalogServiceEvents () {
// Stop listening about changes in global/contextual catalog services
if (!this.globalCatalogService) this.globalCatalogService = this.$api.getService('catalog', '')
if (!this.catalogService) this.catalogService = this.$api.getService('catalog')
if (this.globalCatalogService.events) {
this.globalCatalogService.events.forEach(event => {
// Avoid reentrance
if (this.catalogListeners[event]) {
this.globalCatalogService.removeListener(event, this.catalogListeners[event])
if (this.catalogService && (this.catalogService !== this.globalCatalogService)) {
this.catalogService.removeListener(event, this.catalogListeners[event])
}
const events = ['created', 'updated', 'patched', 'removed']
events.forEach(event => {
// Avoid reentrance
if (this.catalogListeners[event]) {
this.globalCatalogService.removeListener(event, this.catalogListeners[event])
if (this.catalogService && (this.catalogService !== this.globalCatalogService)) {
this.catalogService.removeListener(event, this.catalogListeners[event])
}
})
}
}
})
this.catalogListeners = {}
},
resetCatalogServiceEventsListeners () {
Expand Down
35 changes: 25 additions & 10 deletions map/client/utils/utils.layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ export async function setLayerCached (layer, options) {
}
}

async function cacheLayerTile(urlTemplate, x, y, z) {
const url = urlTemplate.replace('{z}', z).replace('{x}', x).replace('{y}', y)
const key = new URL(url)
key.searchParams.delete('jwt')
await LocalCache.set('layers', key.href, url)
}

export async function setBaseLayerCached (layer, options) {
const bounds = options.bounds
const minZoom = options.minZoom || 3
Expand All @@ -78,22 +85,24 @@ export async function setBaseLayerCached (layer, options) {

const urlTemplate = _.get(layer, 'leaflet.source')
let promises = []
for (let zoom = minZoom; zoom <= maxZoom; zoom++) {
for (let z = minZoom; z <= maxZoom; z++) {
let sm = new SphericalMercator()
let tilesBounds = sm.xyz([bounds[0][1], bounds[0][0], bounds[1][1], bounds[1][0]], zoom, _.get(layer, 'leaflet.tms'))
let tilesBounds = sm.xyz([bounds[0][1], bounds[0][0], bounds[1][1], bounds[1][0]], z, _.get(layer, 'leaflet.tms'))
for (let y = tilesBounds.minY; y <= tilesBounds.maxY; y++) {
for (let x = tilesBounds.minX; x <= tilesBounds.maxX; x++) {
let url = urlTemplate.replace('{z}', zoom).replace('{x}', x).replace('{y}', y)
let url = urlTemplate.replace('{z}', z).replace('{x}', x).replace('{y}', y)
let key = new URL(url)
key.searchParams.delete('jwt')
promises.push(LocalCache.set('layers', key.href, url))
promises.push(cacheLayerTile(urlTemplate, x, y, z))
if (promises.length === nbConcurrentRequests) {
await Promise.all(promises)
promises = []
}
}
}
}
// Always download top-level image as we use it as thumbnail
promises.push(cacheLayerTile(urlTemplate, 0, 0, 0))
await Promise.all(promises)
}

Expand Down Expand Up @@ -127,24 +136,30 @@ export async function setLayerUncached (layer, options) {
}
}

async function uncacheLayerTile(urlTemplate, x, y, z) {
const url = urlTemplate.replace('{z}', z).replace('{x}', x).replace('{y}', y)
const key = new URL(url)
key.searchParams.delete('jwt')
await LocalCache.unset('layers', key.href)
}

async function setBaseLayerUncached (layer, options) {
const bounds = options.bounds
const minZoom = options.minZoom || 3
const maxZoom = options.maxZoom || _.get(layer, 'leaflet.maxNativeZoom')

const urlTemplate = _.get(layer, 'leaflet.source')
for (let zoom = minZoom; zoom <= maxZoom; zoom++) {
for (let z = minZoom; z <= maxZoom; z++) {
let sm = new SphericalMercator()
let tilesBounds = sm.xyz([bounds[0][1], bounds[0][0], bounds[1][1], bounds[1][0]], zoom, _.get(layer, 'leaflet.tms'))
let tilesBounds = sm.xyz([bounds[0][1], bounds[0][0], bounds[1][1], bounds[1][0]], z, _.get(layer, 'leaflet.tms'))
for (let y = tilesBounds.minY; y <= tilesBounds.maxY; y++) {
for (let x = tilesBounds.minX; x <= tilesBounds.maxX; x++) {
let url = urlTemplate.replace('{z}', zoom).replace('{x}', x).replace('{y}', y)
let key = new URL(url)
key.searchParams.delete('jwt')
await LocalCache.unset('layers', key.href)
await uncacheLayerTile(urlTemplate, x, y, z)
}
}
}
// Always remove top-level image as we use it as thumbnail
await uncacheLayerTile(urlTemplate, 0, 0, 0)
}

async function setServiceLayerUncached (layer, options) {
Expand Down

0 comments on commit 617216f

Please sign in to comment.