Skip to content

Commit

Permalink
feat: Provide a function to build timeseries definition from layer de…
Browse files Browse the repository at this point in the history
…finition (closes #940)
  • Loading branch information
claustres committed Aug 23, 2024
1 parent a98f804 commit 8316ed8
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 36 deletions.
20 changes: 11 additions & 9 deletions core/client/components/chart/KTimeSeriesChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@ let min = null
let max = null
// Watch
watch(() => props.timeSeries, update)
watch(() => props.xAxisKey, update)
watch(() => props.yAxisKey, update)
watch(() => props.startTime, update)
watch(() => props.endTime, update)
watch(() => props.zoomable, update)
watch(() => props.logarithmic, update)
watch(() => props.currentTime, update)
watch(() => props.options, update)
// We use debounce here to avoid pultiple refresh when initializing props
const requestUpdate = _.debounce(() => update(), 500)
watch(() => props.timeSeries, requestUpdate)
watch(() => props.xAxisKey, requestUpdate)
watch(() => props.yAxisKey, requestUpdate)
watch(() => props.startTime, requestUpdate)
watch(() => props.endTime, requestUpdate)
watch(() => props.zoomable, requestUpdate)
watch(() => props.logarithmic, requestUpdate)
watch(() => props.currentTime, requestUpdate)
watch(() => props.options, requestUpdate)
// Functions
async function onCanvasRef (ref) {
Expand Down
2 changes: 1 addition & 1 deletion core/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default async function initialize () {

logger.debug('[KDK] initializing core module')

// Delcare the module intiaization states
// Declare the module intiaization states
Store.set('kdk', { core: { initialized: false }, map: { initialized: false } })

// Initialize singletons that might be used globally first
Expand Down
29 changes: 5 additions & 24 deletions map/client/mixins/mixin.feature-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const featureService = {
getFeaturesFromQuery: features.getFeaturesFromQuery,
async getFeatures (options, queryInterval, queryLevel) {
const query = await this.getFeaturesQuery(options, queryInterval, queryLevel)
const response = await this.getFeaturesFromQuery(options, query)
const response = await features.getFeaturesFromQuery(options, query)
return response
},
async getFeaturesFromLayer (name, queryInterval) {
Expand All @@ -42,42 +42,23 @@ export const featureService = {
if (!layer) return
return this.getFeatures(layer, queryInterval)
},
getMeasureForFeatureBaseQuery (layer, feature) {
// We might have a different ID to identify measures related to a timeseries (what is called a chronicle)
// than measures displayed on a map. For instance mobile measures might appear at different locations,
// but when selecting one we would like to display the timeseries related to all locations.
let featureId = layer.chronicleId || layer.featureId
// Support compound ID
featureId = (Array.isArray(featureId) ? featureId : [featureId])
const query = featureId.reduce((result, id) =>
Object.assign(result, { ['properties.' + id]: _.get(feature, 'properties.' + id) }),
{})
query.$groupBy = featureId
return query
},
getMeasureForFeatureBaseQuery: features.getMeasureForFeatureBaseQuery,
async getMeasureForFeatureQuery (layer, feature, startTime, endTime) {
const query = await this.getFeaturesQuery(_.merge({
baseQuery: this.getMeasureForFeatureBaseQuery(layer, feature)
baseQuery: features.getMeasureForFeatureBaseQuery(layer, feature)
}, layer), {
$gte: startTime.toISOString(),
$lte: endTime.toISOString()
})
return query
},
async getMeasureForFeatureFromQuery (layer, feature, query) {
const result = await this.getFeaturesFromQuery(layer, query)
if (result.features.length > 0) {
return result.features[0]
} else {
return _.cloneDeep(feature)
}
},
getMeasureForFeatureFromQuery: features.getMeasureForFeatureFromQuery,
async getMeasureForFeature (layer, feature, startTime, endTime) {
let probedLocation
this.setCursor('processing-cursor')
try {
const query = await this.getMeasureForFeatureQuery(layer, feature, startTime, endTime)
probedLocation = await this.getMeasureForFeatureFromQuery(layer, feature, query)
probedLocation = await features.getMeasureForFeatureFromQuery(layer, feature, query)
} catch (error) {
logger.error(error)
}
Expand Down
107 changes: 105 additions & 2 deletions map/client/utils/utils.features.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import rhumbDistance from '@turf/rhumb-distance'
import rotate from '@turf/transform-rotate'
import scale from '@turf/transform-scale'
import translate from '@turf/transform-translate'
import { api, Time } from '../../../core/client/index.js'
import { api, Time, Units, i18n } from '../../../core/client/index.js'

export function processFeatures (geoJson, processor) {
const features = (geoJson.type === 'FeatureCollection' ? geoJson.features : [geoJson])
Expand Down Expand Up @@ -243,6 +243,50 @@ export async function getFeaturesFromQuery (options, query) {
return response
}

export function getMeasureForFeatureBaseQuery (layer, feature) {
// We might have a different ID to identify measures related to a timeseries (what is called a chronicle)
// than measures displayed on a map. For instance mobile measures might appear at different locations,
// but when selecting one we would like to display the timeseries related to all locations.
let featureId = layer.chronicleId || layer.featureId
// Support compound ID
featureId = (Array.isArray(featureId) ? featureId : [featureId])
const query = featureId.reduce((result, id) =>
Object.assign(result, { ['properties.' + id]: _.get(feature, 'properties.' + id) }),
{})
query.$groupBy = featureId
return query
}

export async function getMeasureForFeatureQuery (layer, feature, startTime, endTime, level) {
const query = await getFeaturesQuery(_.merge({
baseQuery: getMeasureForFeatureBaseQuery(layer, feature)
}, layer), {
$gte: startTime.toISOString(),
$lte: endTime.toISOString()
}, level)
return query
}

export async function getMeasureForFeatureFromQuery (layer, feature, query) {
const result = await getFeaturesFromQuery(layer, query)
if (result.features.length > 0) {
return result.features[0]
} else {
return _.cloneDeep(feature)
}
}

export async function getMeasureForFeature (layer, feature, startTime, endTime, level) {
let probedLocation
try {
const query = await getMeasureForFeatureQuery(layer, feature, startTime, endTime, level)
probedLocation = await getMeasureForFeatureFromQuery(layer, feature, query)
} catch (error) {
logger.error(error)
}
return probedLocation
}

export function checkFeatures (geoJson, options = {
kinks: true,
redundantCoordinates: true
Expand Down Expand Up @@ -379,4 +423,63 @@ export function getFeatureStyleType (feature) {
if (['Polygon', 'MultiPolygon'].includes(geometryType)) return 'polygon'
logger.warn(`[KDK] unsupported geometry of type of ${geometryType}`)
return
}
}

// Build timeseries to be used in charts from layer definition for target feature
export function getTimeSeriesForFeature({ feature, layer, startTime, endTime, runTime, level }) {
const variables = _.get(layer, 'variables', [])
if (variables.length === 0) return []
const properties = _.get(feature, 'properties', {})
// Fetch data function
async function fetch() {
const measure = await getMeasureForFeature(layer, feature, startTime, endTime, level)
return measure
}
// Create promise to fetch data as it will be shared by all series,
// indeed a measure stores all aggregated variables
const data = fetch()

async function getDataForVariable(variable) {
const measure = await data
const time = measure.time || measure.forecastTime
const runTime = measure.runTime
const properties = _.get(measure, 'properties', {})
// Check if we are targetting a specific level
const name = (level ? `${variable.name}-${level}` : variable.name)
let values = []
// Aggregated variable available for feature ?
if (properties[name] && Array.isArray(properties[name])) {
// Build data structure as expected by visualisation
values = properties[name].map((value, index) => ({ time: moment.utc(time[name][index]).valueOf(), [name]: value }))
// Keep only selected value if multiple are provided for the same time (eg different forecasts)
if (variable.runTimes && !_.isEmpty(_.get(runTime, name)) && runTime) {
values = values.filter((value, index) => (runTime[name][index] === runTime.toISOString()))
} else values = _.uniqBy(values, 'time')
}
return values
}

return variables.map(variable => {
// Base unit could be either directly the unit or the property of the measure storing the unit
const baseUnit = _.get(properties, 'unit', variable.unit)
// Known by the unit system ?
const unit = Units.getUnit(baseUnit) || { name: baseUnit }
return {
data: getDataForVariable(variable),
variable: {
name,
label: `${i18n.tie(variable.label)} (${Units.getTargetUnitSymbol(baseUnit)})`,
unit,
targetUnit: Units.getTargetUnit(unit),
chartjs: Object.assign({
parsing: {
xAxisKey: 'time',
yAxisKey: (level ? `${variable.name}-${level}` : variable.name)
},
cubicInterpolationMode: 'monotone',
tension: 0.4
}, variable.chartjs)
}
}
})
}
4 changes: 4 additions & 0 deletions map/client/utils/utils.layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export function isTerrainLayer (layer) {
return (cesiumOptions.type === 'Cesium') || (cesiumOptions.type === 'Ellipsoid')
}

export function isMeasureLayer (layer) {
return layer.variables && layer.service
}

export async function saveGeoJsonLayer (layer, geoJson, chunkSize = 5000) {
// Check for invalid features first
const check = checkFeatures(geoJson)
Expand Down

0 comments on commit 8316ed8

Please sign in to comment.