import { EventObject } from "../../../events/EventObject.js";
import { generateUniqueId } from "../../../utils/utils.js";
import { resolveComplexTemplate } from "../../../utils/template/resolveComplexTemplate.js";
import { enhanceHTML } from "../../../utils/template/enhanceHTML.js";
import { LayerProxy } from "./Layer.proxy.js";
import { NETWORK_LOADING_ERROR } from "../../utils/messageTemplates.js";
import LayerMetadata from "./Layer.metadata.md?raw";
import { LayerSchema } from "./Layer.schema.js";
export { Layer }
/**
* Abstract Layer class
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.map.layer
*/
class Layer extends EventObject {
/**
* Set all properties for the layer based on the options object.
*
* @param {object} config
* @param {object} cache
* @param {string} id
*/
constructor(config, cache, id) {
super({
"layer_select": [],
"layer_deselect": [],
"layer_update": [],
"layer_request_remove": [],
"layer_loading": [],
"layer_loaded": [],
"layer_message": [],
"layer_click": [],
"layer_mouseover": [],
"layer_mouseout": []
});
this.uniqueId = id || generateUniqueId();
// layer metadata containers
this.config = Object.assign({}, config || {});
this.cache = Object.assign({}, cache || {});
this.defaults = {};
this.schema = {};
// initialize the default properties
this.setSchema_(LayerSchema.getSchema());
// apply the catalogId to the config, if it is only defined in the cache
if ("catalogId" in this.cache) {
this.config.catalogId = this.cache.catalogId;
}
// internal properties
this.messages_ = [];
this.popupEvents_ = [];
this.projectionMatches_ = true;
this.filterMatches_ = true;
// active map layer state
this.crs_ = (this.availableCrs.length > 0) ? this.availableCrs[0] : "";
this.mapLayers_ = {};
this.activeLayer_ = new LayerProxy();
// framework specific map implementations
this.layerProxies_ = {};
}
get deactivated() {
return (!this.projectionMatches_ || !this.filterMatches_);
}
set deactivated(val) {
console.log("not implemented, only the calculated getter is relevant");
}
/**
* Helper method to get a property from the config, cache or default container
*
* @param {string} name
* @private
* @returns {any} property
*/
getProperty_(name) {
if (name in this.config) return this.config[name];
if (name in this.cache) return this.cache[name];
if (name in this.defaults) return this.defaults[name];
return undefined;
}
/**
* Register the getter and setter for a property and add default
* values for properties based on the JSON Schema
* @param {object} schema json schema
* @private
*/
setSchema_(schema) {
this.schema = schema;
for (let name in schema.properties) {
if (name in this) delete this[name];
this.defaults[name] = schema.properties[name].default;
Object.defineProperty(this, name, {
get() { return this.getProperty_(name); },
set(val) { this.config[name] = val; },
configurable: true
});
}
// set layer type based on default for all content objects
const type = this.defaults.type;
this.config.type = type;
this.cache.type = type;
}
/**
* Register the getter and setter for a property and add default values for properties
* in the exported config object. Existing values are not overwritten
*
* @private
* @param {object} config
*/
addConfig_(config) {
for (let name in config) {
if (name in this) delete this[name];
if (!(name in this.config)) this.config[name] = config[name];
Object.defineProperty(this, name, {
get() { return this.config[name]; },
set(val) { this.config[name] = val; },
configurable: true
});
}
}
getConfig_(container) {
container = JSON.parse(JSON.stringify(container));
for (let key in container) {
if (
(container[key] === null) ||
(((typeof container[key] == "string") || Array.isArray(container[key])) && (container[key].length == 0)) ||
((typeof container[key] == "object") && (Object.keys(container[key]).length == 0))
) delete container[key];
}
// remove deprecated keys
if ("settings" in container) delete container.settings;
if ("filter" in container) delete container.filter;
// remove default keys
if (container.active === false) delete container.active;
if (container.expanded === false) delete container.expanded;
return container;
}
/**
* create a map layer based on the service options
*
* @param {string} type e.g. "leaflet"
* @private
*/
createMapLayer_(type, options) {
if (type in this.layerProxies_) {
const proxy = new this.layerProxies_[type](options || {});
let error = false;
proxy.on("layerproxy_loading", () => {
error = false;
this.fire("layer_loading");
});
proxy.on("layerproxy_loaded", () => {
this.fire("layer_loaded");
if (!error) this.removeMessage(NETWORK_LOADING_ERROR);
});
proxy.on('layerproxy_error', () => {
error = true;
this.addMessage(NETWORK_LOADING_ERROR);
});
this.mapLayers_[type] = proxy;
return proxy;
} else {
return null;
}
}
/**
* Helper method to update the mapLayer according to
* the current state. Might be necessary after switching the map type
*
* @private
*/
updateMapLayer_() { }
getConfig() {
const config = this.getConfig_(this.config);
if (("catalogId" in config) && ("type" in config)) delete config.type;
return config;
}
getCache() {
return this.getConfig_(this.cache);
}
/**
* Print the Metadata from this layer as html.
*/
printMetadata(layer) {
const container = resolveComplexTemplate(LayerMetadata, {
layer: layer || this,
datasetDescription: this?.dataFrame?.description || {},
sensorMetadata: this?.sensorMetadata || {},
}, {
markdown: true,
postProcess: true
});
enhanceHTML(container, {});
return container;
}
/**
* get the events that can be triggered by buttons in a popup
*/
getPopupEvents() {
return this.popupEvents_.slice();
}
/**
* add an event for the buttons in the popup
*/
addPopupEvent(title, callback) {
if ((typeof title == "string") && (typeof callback == "function")) {
this.popupEvents_.push({
title: title,
callback: callback
});
}
}
/**
* returns the map layer proxy for the
* specific map implementation
*
* @param {string} type
* @returns {object} map layer for framework
*/
getLayerProxy(type) {
return (type in this.mapLayers_)
? this.mapLayers_[type]
: this.createMapLayer_(type);
}
/**
* enables the map layer for the
* specific map implementation
*
* @param {string} type
*/
enable(type) {
if (type in this.mapLayers_) {
this.active = true;
this.activeLayer_ = this.mapLayers_[type];
this.updateMapLayer_(type);
return true;
}
return false;
}
/**
* Unset the active state of the layer
*/
disable() {
this.active = false;
}
/**
* Returns the Legend Graphic for this layer.
* May depend on current color scale.
*
* @returns html <img>-tag (null or undefined if there is no legend graphic)
*/
getLegendGraphic() { }
/**
* Set Filter without applying it
*
* @param {object} config generic filter config
*/
setFilter(config) {
this.filter = config;
}
/**
* Apply the defined filter. Uses generic
* filter syntax and transforms it internally
*/
applyFilter() { }
/**
* Set the style of the layer.
*
* @param {string} styleName style name
* @returns {boolean} returns true if successful
*/
setStyle(styleName) { }
/**
* Get the active style of the layer.
*/
getStyle() { }
/**
* Get the active colorscale for the layer
*/
getColorScale() { }
/**
* Set the colorscale for the layer
*
* @param colorScale
*/
setColorScale(colorScale) { }
/**
* Dispose the Layer
*/
dispose() { }
/**
* Get the transparency value
*
* @returns {number} number between 0 and 1
*/
getOpacity() {
return this.opacity;
}
/**
* Set the transparency between 0 and 1
* @param {number} opacity style name
*/
setOpacity(opacity) {
this.opacity = opacity;
this.activeLayer_.setOpacity(opacity);
}
/**
* Reload the Layer on the map
*/
reload() {
this.activeLayer_.reload();
}
/**
* Switch the projection for the visualization
* of the layer. CRS hast to be included in availableCRS.
*
* @param {string} crs target crs code
*/
setProjection(crs) {
crs = crs.toUpperCase();
if (this.availableCrs.includes(crs)) {
this.crs_ = crs;
this.activeLayer_.setProjection(crs);
this.projectionMatches_ = true;
} else {
this.projectionMatches_ = false;
}
return this.projectionMatches_;
}
/**
* Get the bounding box as an object
*
* @returns {object} { min: {x, y}, max: {x, y}, crs: "CRS:84" }
*/
getBounds() {
return {
min: { y: -90, x: -180 },
max: { y: 90, x: 180 },
crs: "CRS:84"
}
}
/**
* Get all active messages
*
* @returns {object[]} messages
*/
getMessages() {
// implementation of layer-specific messages needs to be done in child classes
return [...this.messages_];
}
/**
* Add a message to the layer
* @param {object} message
*/
addMessage(message) {
if (this.messages_.indexOf(message) > -1) return;
this.messages_.push(message);
this.fire("layer_message", this.getMessages());
}
/**
* Remove a message from the layer
* @param {object} message
*/
removeMessage(message) {
const index = this.messages_.indexOf(message);
if (index > -1) {
this.messages_.splice(index, 1);
this.fire("layer_message", this.getMessages());
}
}
/**
* @param {string} attributeField
* @returns {string[]} array of unique values
*/
async getUniqueValues(attributeField) {
return [];
}
/**
* @returns {string[]} array of attribute names
*/
getAttributeNames() {
return Object.keys(this.attributeFields);
}
}