diff --git a/core/client/components/chart/KTimeSeriesChart.vue b/core/client/components/chart/KTimeSeriesChart.vue index 55c423a2c..17d081ba5 100644 --- a/core/client/components/chart/KTimeSeriesChart.vue +++ b/core/client/components/chart/KTimeSeriesChart.vue @@ -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) { diff --git a/core/client/index.js b/core/client/index.js index 07bb623ca..110113acb 100644 --- a/core/client/index.js +++ b/core/client/index.js @@ -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 diff --git a/map/client/mixins/mixin.feature-service.js b/map/client/mixins/mixin.feature-service.js index ff7bd5dce..728817d6f 100644 --- a/map/client/mixins/mixin.feature-service.js +++ b/map/client/mixins/mixin.feature-service.js @@ -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) { @@ -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) } diff --git a/map/client/utils/utils.features.js b/map/client/utils/utils.features.js index de5e766a6..c45426fc3 100644 --- a/map/client/utils/utils.features.js +++ b/map/client/utils/utils.features.js @@ -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]) @@ -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 @@ -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 -} \ No newline at end of file +} + +// 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) + } + } + }) +} diff --git a/map/client/utils/utils.layers.js b/map/client/utils/utils.layers.js index d50394fa0..0e38900fb 100644 --- a/map/client/utils/utils.layers.js +++ b/map/client/utils/utils.layers.js @@ -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)