import { Layer } from "../Layer/Layer.js";
import { FilterBuilder } from "../../filters/FilterBuilder.js";
import { ServiceLayerSchema } from "./ServiceLayer.schema.js";
import { getUniqueValuesFromWPS } from "../../utils/getUniqueValuesFromWPS.js";
export { ServiceLayer }
/**
* Abstract Service Layer Implementation
*
* @author rhess <robin.hess@awi.de>
* @author sjaswal <shahzeib.jaswal@awi.de>
* @memberof vef.map.layer
*/
class ServiceLayer extends Layer {
/**
* 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);
// init default values and define getters and setters
this.setSchema_(ServiceLayerSchema.getSchema());
// init getters and setters for configs required in this.config
// to prevent saving to "cache" or "default"
this.addConfig_({
queryParams: {},
staticFilter: []
});
this.filter = config.filter || [];
this.timeColumns_ = ["time", "datetime", "date_time", "date_time_start", "date_time_end", "begin_date", "end_date", "dateTimeStart", "dateTimeEnd", "beginDate", "endDate", "DATA_DATETIME"];
this.getometryColumns_ = ["geometry", "geom", "the_geom", "geom_multi"];
// current messages for possible info about unapplied filters
this.filterMessages_ = [];
}
get capabilitiesUrl() {
return this.serviceUrl + `?REQUEST=GetCapabilities&SERVICE=${this.type.toUpperCase()}&VERSION=${this.serviceVersion}`;
}
/**
* Get all active messages
*
* @returns {object[]} messages
*/
getMessages() {
// implementation of layer-specific messages needs to be done in child classes
return [...this.messages_, ...this.filterMessages_];
}
/**
* Get Layer name for making requests in case names differ for each projection
*/
getRequestName() {
if (typeof this.name == "string") {
return this.name;
} else if (typeof this.name == "object") {
if (this.crs_ in this.name) {
return this.name[this.crs_];
} else if ("default" in this.name) {
return this.name["default"];
}
}
return "";
}
/**
* Iterates through the array of timestamps and returns the
* one closest to the given value
*
* @param {string} value
* @param {number[]} timestamps
* @param {string[]} isoTimestamps
*/
getClosestTimestamp_(value, timestamps, isoTimestamps) {
value = new Date(value).getTime();
let closestIndex = 0;
let closestDistance = Math.abs(timestamps[0] - value);
for (let i = 0; i < timestamps.length; ++i) {
const distance = Math.abs(timestamps[i] - value);
if (distance < closestDistance) {
closestDistance = distance;
closestIndex = i;
}
}
return isoTimestamps[closestIndex];
}
/**
* Adjusts time filter values according to the dimension config
*
* @param {object} filter filter values
*
* @private
*/
adjustTimeValues_(filter, dimension) {
if (!dimension) return;
// only execute if any of the used properties of "dimension" is set
if (!dimension.range && !dimension.resolution && !dimension.defaultTime && !dimension.values && !dimension.operators) return;
for (let i = 0; i < filter.values.length; ++i) {
const values = filter.values[i];
const originalValues = values.join(", ");
// override the values if a range is given and set it to a single day
if (dimension.range == "day") {
for (let j = 1; j < values.length; ++j) {
if (values[0] == "bt") {
const day = values[2].substring(0, 10);
values[1] = day + "T00:00:00Z";
values[2] = day + "T23:59:59Z";
}
}
}
// apply timestamp resolution and defaultTime settings
if (dimension.resolution || dimension.defaultTime) {
for (let j = 1; j < values.length; ++j) {
if (dimension.resolution == "date") {
values[j] = values[j].substring(0, 10);
} else if (dimension.defaultTime) {
values[j] = values[j].substring(0, 10) + "T" + dimension.defaultTime;
}
}
}
if (Array.isArray(dimension.values) && (dimension.values.length > 0)) {
// if values are set, a single value is matched from the list. dimension.mode is ignored
const timeValues = [];
for (let i = 0; i < dimension.values.length; ++i) {
if (dimension.values[i].length == 24) {
timeValues.push(new Date(dimension.values[i]).getTime());
}
}
filter.values[i] = ["eq", this.getClosestTimestamp_(values[1], timeValues, dimension.values)];
} else if (Array.isArray(dimension.operators) && (dimension.operators.length > 0)) {
if (!dimension.operators.includes(values[0])) {
const operator = dimension.operators[0];
if (operator == "bt") {
const begin = values[1].substring(0, 10) + "T00:00:00Z";
const end = values[1].substring(0, 10) + "T23:59:59Z";
filter.values[i] = [operator, begin, end];
} else {
const index = (values[0] == "bt") ? 2 : 1;
filter.values[i] = [operator, values[index]];
}
}
}
let newValues = values.join(", ")
if (originalValues != newValues) {
switch (values[0]) {
case "bt":
newValues = "range from <b>" + values[1] + "</b> to <b>" + values[2] + "</b>";
break;
case "eq":
newValues = "single timestamp <b>" + values[0] + "</b>";
break;
case "lt":
newValues = "less than <b>" + values[0] + "</b>";
break;
case "gt":
newValues = "greater than <b>" + values[0] + "</b>";
break;
case "lteq":
newValues = "less than or equal to <b>" + values[0] + "</b>";
break;
case "gteq":
newValues = "greater than or equal to <b>" + values[0] + "</b>";
break;
}
this.filterMessages_.push({
level: 0,
content: "Time filter adjusted to:<br/>" + newValues + "."
});
}
}
return filter;
}
adjustTimeColumn_(filter) {
let columns = this.timeColumns_;
// Combine dimensions and attributeFields into a single array of keys and filter them as fallback columns containing "date" or "time"
columns.push(
...Object.keys({ ...this.dimensions, ...this.attributeFields }).filter(key => /date|time/i.test(key))
);
for (let i = 0; i < columns.length; ++i) {
if (columns[i] in this.dimensions) {
filter.column = columns[i];
return this.dimensions[filter.column];
} else if (columns[i] in this.attributeFields) {
filter.column = columns[i];
return this.attributeFields[filter.column];
}
}
this.filterMessages_.push({
level: 0,
content: "No valid time column found for the filter."
});
}
/**
* Automatically detect the geometry column if a column name is not set
*
* @private
* @param {*} f filter
*/
adjustGeometryColumn_(f) {
f.column = this.getometryColumns_.find(col => col in this.attributeFields)
|| Object.keys(this.attributeFields).find(key => key.toLowerCase().includes("geom"));
if (!f.column) {
this.filterMessages_.push({
level: 0,
content: "No valid geometry column found for the filter."
});
}
}
applyColumnMapping(filter) {
const mapping = this?.mapping?.filter?.mapping?.properties;
if (!mapping) return;
const column = filter.column;
if (column in mapping) {
filter.column = mapping[column];
this.filterMessages_.push({
level: 0,
content: `The filtered column <b>${column}</b> was mapped to the layers internal column <b>${filter.column}</b>`
});
}
}
/**
* Handles the error message for attribute filtering.
* @private
* @param {String} attributeName
* @param {String|null} reasonId
*/
showAttributeErrorMessage_(attributeName, reasonId = null) {
const baseMessage = `Cannot be filtered by attribute <b>${attributeName}</b>`;
const reasons = {
'reason#0': 'This layer is excluded in the filter configuration.',
'reason#1': 'It is disabled in the layer configuration.'
};
const content = [baseMessage, reasonId ? ': ' + reasons[reasonId] : '.'].join('')
this.filterMessages_.push({
'level': 0,
'content': content
});
if (this.throwFilterErrors) {
throw Error(content);
}
}
/**
* Gets the filter URL params based on the
* Filter config and the layer's dimensions
* @private
* @param {object[]} config
*/
createFilterParams_(config) {
this.filterMessages_ = [];
if (!Array.isArray(config)) config = [];
// merge the static filters and copy config so the original is unchanged
config = JSON.parse(JSON.stringify(this.staticFilter.concat(config)));
const params = {};
const attributeFilters = [];
for (let f of config) {
// apply filter column mapping
this.applyColumnMapping(f);
// find and assign the time dimension/attribute
if (f?.type?.toLowerCase() == "time") {
// only adjust the time column if it is not already set
if (!f.column) {
// mapping of possible time column names if a filter.column is not set
const timeColumn = this.adjustTimeColumn_(f);
// adjust the time values according to the dimension config
this.adjustTimeValues_(f, timeColumn);
} else {
// adjust the time values according to the dimension config
this.adjustTimeValues_(f, this.dimensions[f.column] || this.attributeFields[f.column]);
}
if (!f.column) continue;
// Automatically detect the geometry column and a column name is not set
} else if (!f.column && (f?.type?.toLowerCase() == "geometry")) {
this.adjustGeometryColumn_(f);
if (!f.column) continue;
}
if (f.column in this.dimensions) {
const dimension = this.dimensions[f.column];
// adjust the colum name according to the mapping
f.column = dimension.name || f.column;
if (!dimension.disabled) {
if (dimension.type == "attribute_field") {
// fallback for older configs, where a dimensions is not absolutely a WMS dimension filter
attributeFilters.push(f);
} else {
Object.assign(params, FilterBuilder.getFilterParams([f], this.serviceSoftware, this.type, "dimension", this.name));
}
} else {
this.showAttributeErrorMessage_(f.column, 'reason#1')
}
} else if (f.column in this.attributeFields) {
const field = this.attributeFields[f.column];
if (!field.disabled) {
attributeFilters.push(f);
} else {
this.showAttributeErrorMessage_(f.column, 'reason#1')
}
} else if (!f.column && Array.isArray(f.values)) {
// syntax [operator, column, values, ...] not supported for type "dimension"
const values = [];
for (let i = 0; i < f.values.length; ++i) {
const col = f.values[i][1];
if ((col in this.dimensions) && (this.dimensions[col].type == "attribute_field")) {
if (!this.dimensions[col].disabled) {
// adjust the colum name according to the mapping
f.values[i][1] = this.dimensions[col].name || col;
values.push(f.values[i]);
} else {
this.showAttributeErrorMessage_(col, 'reason#1')
}
} else if (col in this.attributeFields) {
if (!this.attributeFields[col].disabled) {
values.push(f.values[i]);
} else {
this.showAttributeErrorMessage_(col, 'reason#1')
}
} else {
this.showAttributeErrorMessage_(col)
}
}
if (values.length) attributeFilters.push({
type: f.type,
column: null,
values: values
});
} else {
this.showAttributeErrorMessage_(f.column)
}
}
Object.assign(params, FilterBuilder.getFilterParams(attributeFilters, this.serviceSoftware, this.type, this.filterType, this.name));
this.fire("layer_message", this.getMessages())
// merge the params with static parameters
return params;
}
async getUniqueValues(column) {
if (!this.config.attributeCache) this.config.attributeCache = {};
if (!this.updatedUniqueValuesAtRuntime_) this.updatedUniqueValuesAtRuntime_ = [];
const fetchValues_ = async () => {
const values = await getUniqueValuesFromWPS(this.serviceUrl, this.getRequestName(), column);
this.config.attributeCache[column] = values
this.updatedUniqueValuesAtRuntime_.push(column);
return values;
};
const getCachedValues_ = () => {
if (this.config.attributeCache[column]) {
return this.config.attributeCache[column];
} else {
return [];
};
};
try {
if (this.updatedUniqueValuesAtRuntime_.includes(column)) {
return getCachedValues_();
} else {
return await fetchValues_();
}
} catch (error) {
console.error("Error fetching unique values from WPS:", error);
return getCachedValues_();
}
}
}