Skip to content

Commit

Permalink
feat: Make possible to update a layer in realtime based on events fro…
Browse files Browse the repository at this point in the history
…m any service (closes #927)
  • Loading branch information
claustres committed Aug 14, 2024
1 parent a66b994 commit 4179fb6
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 8 deletions.
7 changes: 6 additions & 1 deletion map/client/mixins/globe/mixin.geojson-layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,18 @@ export const geojsonLayers = {
this.$engineEvents.emit('layer-updated', layer, cesiumLayer, data)
},
onCurrentTimeChangedGeoJsonLayers (time) {
// Need to update layers that require an update at a given frequency
const geoJsonlayers = _.values(this.layers).filter(sift({
// Possible for realtime layers only
'cesium.type': 'geoJson',
'cesium.realtime': true,
$or: [ // Supported by template URL or time-based features
$or: [ // Supported by template URL or time-based features service
{ 'cesium.sourceTemplate': { $exists: true } },
{ service: { $exists: true } }
],
// Skip layers powered by realtime service events
serviceEvents: { $ne: true },
// Skip invisible layers
isVisible: true
}))
geoJsonlayers.forEach(async geoJsonlayer => {
Expand Down
7 changes: 6 additions & 1 deletion map/client/mixins/map/mixin.geojson-layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,13 +522,18 @@ export const geojsonLayers = {
this.$engineEvents.emit('layer-updated', layer, leafletLayer, data)
},
onCurrentTimeChangedGeoJsonLayers (time) {
// Need to update layers that require an update at a given frequency
const geoJsonlayers = _.values(this.layers).filter(sift({
// Possible for realtime layers only
'leaflet.type': 'geoJson',
'leaflet.realtime': true,
$or: [ // Supported by template URL or time-based features
$or: [ // Supported by template URL or time-based features service
{ 'leaflet.sourceTemplate': { $exists: true } },
{ service: { $exists: true } }
],
// Skip layers powered by realtime service events
serviceEvents: { $ne: true },
// Skip invisible layers
isVisible: true
}))
geoJsonlayers.forEach(async geoJsonlayer => {
Expand Down
56 changes: 50 additions & 6 deletions map/client/mixins/mixin.feature-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import sift from 'sift'
import { getType, getGeom } from '@turf/invariant'
import logger from 'loglevel'
import * as features from '../utils/utils.features.js'
import * as layers from '../utils/utils.layers.js'

export const featureService = {
methods: {
Expand Down Expand Up @@ -88,11 +89,11 @@ export const featureService = {
editFeaturesGeometry: features.editFeaturesGeometry,
editFeaturesProperties: features.editFeaturesProperties,
removeFeatures: features.removeFeatures,
onFeaturesUpdated (feature) {
onFeaturesUpdated (feature, layerId) {
// We only support single feature edition
if (!getType(feature) || !getGeom(feature)) return
// Find related layer
const layer = this.getLayerById(feature.layer)
const layer = this.getLayerById(layerId || feature.layer)
if (!layer || !this.isLayerVisible(layer.name)) return
// Only possible when not edited by default
if ((typeof this.isLayerEdited === 'function') && this.isLayerEdited(layer)) return
Expand All @@ -104,11 +105,11 @@ export const featureService = {
if (filteredFeature.length > 0) this.updateLayer(layer.name, feature, { removeMissing: false })
}
},
onFeaturesRemoved (feature) {
onFeaturesRemoved (feature, layerId) {
// We only support single feature edition
if (!getType(feature) || !getGeom(feature)) return
// Find related layer
const layer = this.getLayerById(feature.layer)
const layer = this.getLayerById(layerId || feature.layer)
if (!layer || !this.isLayerVisible(layer.name)) return
// Only possible when not edited by default
if ((typeof this.isLayerEdited === 'function') && this.isLayerEdited(layer)) return
Expand All @@ -118,24 +119,67 @@ export const featureService = {
const filteredFeature = [feature].filter(sift(_.omit(layer.baseQuery || {}, ['$skip', '$sort', '$limit', '$select'])))
if (filteredFeature.length > 0) this.updateLayer(layer.name, feature, { remove: true })
}
},
listenToServiceEvents (layer) {
// User-defined layers are already managed
if (!layer.service || !layer.serviceEvents || layers.isInMemoryLayer(layer) || layers.isFeatureLayer(layer)) return
const service = this.$api.getService(layer.service)
// Check if service available and not already registered
if (!service || this.layerServiceEventListeners[layer._id]) return
// Generate listeners targetting the right layer as in this case the features won't hold it contrary to user-defined layers
const onFeaturesUpdated = (feature) => this.onFeaturesUpdated(feature, layer._id)
const onFeaturesRemoved = (feature) => this.onFeaturesRemoved(feature, layer._id)
this.layerServiceEventListeners[layer._id] = { layerService: layer.service, onFeaturesUpdated, onFeaturesRemoved }
service.on('created', onFeaturesUpdated)
service.on('patched', onFeaturesUpdated)
service.on('removed', onFeaturesRemoved)
},
unlistenToServiceEvents (layer) {
// Check if listeners are registered for layer
if (!this.layerServiceEventListeners[layer._id]) return
const { layerService, onFeaturesUpdated, onFeaturesRemoved } = this.layerServiceEventListeners[layer._id]
const service = this.$api.getService(layerService)
// Check if service still available
if (!service) return
service.off('created', onFeaturesUpdated)
service.off('patched', onFeaturesUpdated)
service.off('removed', onFeaturesRemoved)
delete this.layerServiceEventListeners[layer._id]
}
},
created () {
// Extend timeout for large write operations
this.$api.getService('features').timeout = 60 * 60 * 1000 // 1h should be sufficient since we also have size limits
},
mounted () {
// Listen to user layer changes
// Here we need to listen to service events for all realtime layers triggered by it
// 1) user-defined layers targetting the features service
const featuresService = this.$api.getService('features')
featuresService.on('created', this.onFeaturesUpdated)
featuresService.on('patched', this.onFeaturesUpdated)
featuresService.on('removed', this.onFeaturesRemoved)
// 2) built-in layers targetting specific services
// As we don't know target services upfront we register listeners when layer are added, we track it in a map
this.layerServiceEventListeners = {}
this.$engineEvents.on('layer-added', this.listenToServiceEvents)
this.$engineEvents.on('layer-removed', this.unlistenToServiceEvents)
},
beforeUnmount () {
// Remove event connections
// Remove all listeners
const featuresService = this.$api.getService('features')
featuresService.off('created', this.onFeaturesUpdated)
featuresService.off('patched', this.onFeaturesUpdated)
featuresService.off('removed', this.onFeaturesRemoved)
_.forOwn(this.layerServiceEventListeners, listeners => {
const { layerService, onFeaturesUpdated, onFeaturesRemoved } = listeners
const service = this.$api.getService(layerService)
// Check if service still available
if (!service) return
service.off('created', onFeaturesUpdated)
service.off('patched', onFeaturesUpdated)
service.off('removed', onFeaturesRemoved)
})
this.$engineEvents.off('layer-added', this.listenToServiceEvents)
this.$engineEvents.off('layer-removed', this.unlistenToServiceEvents)
}
}

0 comments on commit 4179fb6

Please sign in to comment.