import { cloneDeep } from "lodash";
import { GeoJSONLayer } from "../GeoJSONLayer/GeoJSONLayer.js";
import { NETWORK_LOADING_ERROR } from "../../utils/messageTemplates.js";
import { STABuilder } from "../../filters/builders/STABuilder.js";
import { StaLayerSchema } from "./StaLayer.schema.js";
import { FilterBuilder } from "../../filters/index.js";
/**
* STA Layer Implementation
*
* @author rhess <robin.hess@awi.de>
*
* @memberof vef.map.layer
*/
class StaLayer extends GeoJSONLayer {
/**
* Set all properties for the layer based on the options object.
*
* @param {object} config
* @param {object} cache
* @param {string} id
*/
constructor(config, cache, id) {
// calling parent constructor
super(config, cache, id);
this.setSchema_(StaLayerSchema.getSchema());
// overwrite geoJson getter/setter, so the geojson is not exported when saving the viewer
Object.defineProperty(this, "geoJSON", {
get() { return this.defaults.geoJSON; },
set(val) { this.defaults.geoJSON = val; },
configurable: true
});
this.loadThings_();
}
/**
*
* @param {string} baseUrl
* @param {number} max
* @param {object} params
* @returns {object[]} values
*/
async fetch_(baseUrl, max, params) {
this.fire("layer_loading", this);
const values = [];
Object.assign(params, {
$count: "false"
})
let json = {};
do {
let url = json?.["@iot.nextLink"];
if (!url) {
let query = "";
const addParam = (key, value) => {
if (value.length == 0) return;
query += ((query.length) ? "&" : "?") + key + "=" + value;
}
for (let key in params) {
if (Array.isArray(params[key])) {
for (let i = 0; i < params[key].length; ++i) {
addParam(key, params[key][i])
}
} else {
addParam(key, params[key])
}
}
url = baseUrl + query;
}
try {
const response = await fetch(url);
json = await response.json();
} catch (e) {
this.addMessage(NETWORK_LOADING_ERROR);
break;
}
for (let i = 0; i < json.value.length; ++i) {
values.push(json.value[i]);
}
} while (("@iot.nextLink" in json) && (!max || (values.length < max)))
this.fire("layer_loaded", this);
return values;
}
/**
* @private
*/
async loadThings_() {
const geoJson = {
type: "FeatureCollection",
features: []
};
const params = {
$expand: "Locations",
$top: "1000"
}
if (this.filter) { // add the STA specific filter params to the params
Object.assign(params, {
$filter: this.filter
})
}
// load things applying the params
const values = await this.fetch_(this.url + "Things", null, params);
for (let i = 0; i < values.length; ++i) {
const value = values[i];
if (!value?.Locations?.length) continue;
geoJson.features.push({
"type": "Feature",
"geometry": cloneDeep(value.Locations[0].location),
"properties": {
name: value?.name,
description: value?.description,
selfLink: value?.['@iot.selfLink'],
images: value?.properties?.images,
sourceLink: value?.properties?.['jsonld.id'] || "",
datastreams: null,
sensors: []
}
})
}
this.setData(geoJson);
}
async applyFilter() {
// create a copy of the filter to allow editing the filter without changing the original filter
const filters = cloneDeep(this.filter);
// If no column is defined for the bounding box filter, use a default column
for (let i = 0; i < filters.length; ++i) {
if (!filters[i].column && (filters[i].type === "geometry")) {
filters[i].column = "Locations/location";
}
}
const harmonizedFilter = FilterBuilder.harmonizeFilter(filters);
const sta_filter = STABuilder.getFilterParams(harmonizedFilter);
this.filter = sta_filter["sta_filter"];
await this.loadThings_();
}
/**
* Return all datastreams for the given thing.
* @param {*} thingUrl
* @returns {Promise}
*/
async getDatastreams(thingUrl) {
return await this.fetch_(`${thingUrl}/Datastreams`, null, {
$expand: [
"Sensor($select=@iot.id,name)",
"ObservedProperty($select=@iot.id,name)"
],
$top: "1000"
});
}
/**
* Return the observations for an observations url of a selected datastream
* @param {*} observationsUrl
* @returns {Promise}
*/
async getObservations(observationsUrl) {
return await this.fetch_(observationsUrl, 1000, {
$select: "resultTime,phenomenonTime,result",
$orderby: "phenomenonTime desc, resultTime desc",
$top: "1000"
});
}
/**
* Get Info of the last clicked features, requests datastreams for each clicked thing
* @override
* @returns {Promise} feature info
*/
async getFeatureInfo() {
const info = this.activeLayer_.getClickedFeatures();
if (info?.data?.features) {
for (let i = 0; i < info.data.features.length; ++i) {
const properties = info.data.features[i]?.properties;
if (!(Array.isArray(properties?.datastreams)) && properties?.selfLink) {
properties.datastreams = await this.getDatastreams(properties.selfLink);
}
}
}
return info;
}
/**
* @param {string} attributeField
* @returns {string[]} array of unique values
*/
async getUniqueValues(attributeField) {
if (!attributeField.includes('/')) {
throw new Error("Malformed attribute field definition: " + attributeField);
}
let slashIndex = attributeField.indexOf('/');
let staEntity = attributeField.substring(0, slashIndex);
let staAttributePath = attributeField.substring(slashIndex + 1);
let json = await this.fetch_(this.url + staEntity, 1000, {
$select: staAttributePath
});
let values = json.map(item => this.getJSONValue(item, staAttributePath));
return [...new Set(values)];
}
/**
* @param {*} obj
* @param {*} path
* @returns value at the given path in the given JSON object or else undefined
*/
getJSONValue = (obj, path) =>
path.split('/').reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj);
/**
* Returns an array of possible attribute names for the checkbox filter with the following format convention:
*
* "<STA Entity>/<path to the attribute>"
*
* Examples: 'Sensors/properties/isVariantOf/name', 'Datastreams/unitOfMeasurement/symbol', 'ObservedProperties/name', 'Thing/name'
*
* @returns {string[]} array of attribute names
*/
getAttributeNames() {
return ['Sensors/properties/isVariantOf/name', 'Datastreams/unitOfMeasurement/symbol', 'ObservedProperties/name', 'Things/name'];
}
}
export { StaLayer };