import { cloneDeep } from "lodash";
import { UiElement } from "../ui/UiElement.js";
import { setCSSProperties } from "../utils/utils.js";
import { Filters } from "../map/filters/Filters.js";
import { LayerManager } from "../map/LayerManager.js";
import { Layer } from "../map/layer/Layer/Layer.js";
import { BASELAYERS } from "../defaults/BASELAYERS.js";
import { FileDropTarget } from "../ui/dnd/FileDropTarget.js";
import { shortcutManager } from "./shortcutsManager.js";
import * as loader from "./loader/index.js";
import * as templates from "./templates/index.js";
import { loadLayer } from "../map/utils/loadLayer.js";
import "./Viewer.css";
export { Viewer }
/**
* Viewer class for loading a JSON configuration
* and creating a customizable viewer from the vef modules
*
* @author sjaswal <shahzeib.jaswal@awi.de>
* @author rhess <robin.hess@awi.de>
*
* @memberof vef.viewer
*/
class Viewer extends UiElement {
/**
* @param {HTMLElement | string} target target element to put the viewer (ID or HTMLElement)
* @param {object | string} config config object or path to a viewer config file
* @param {object} options additional options for the viewer class
*/
constructor(target, config, options) {
super(target, {
"config_loaded": [],
"layers_loaded": [],
"viewer_loaded": [],
"drag_drop_viewer_loaded": []
});
this.options = Object.assign({
shortcutsEnabled: true,
allowConfigDragDrop: false,
configOverride: null
}, options || {});
this.elements = {};
this.config = config;
this.originalConfig = {};
this.filters = null;
this.activeFilters = [];
this.template = null;
this.layerManager = new LayerManager();
this.baseLayers = {};
this.getElement().classList.add("viewer");
this.promise_ = this.loadConfig_()
this.promise_.then(() => {
setTimeout(() => {
this.fire("config_loaded", this);
this.parseConfig_();
}, 0);
});
if (this.options.shortcutsEnabled) this.shortcuts = new shortcutManager(this.elements, this.options, target);
if (this.options.allowConfigDragDrop) this._initConfigDragDrop();
}
/**
* Add an element to the viewer and set the id
*
* @param {string} id
* @param {UiElement | HTMLElement} element
*/
addElement(id, element) {
this.elements[id] = element;
if (element instanceof UiElement) {
element.getElement().id = id;
} else if (element instanceof HTMLElement) {
element.id = id;
}
}
/**
* Append an element to a parent identified by id and optionally
* update the layout config of the viewer
*
* @param {string} elementId
* @param {string} parentId
* @param {boolean} updateLayout default is false
*/
appendElement(elementId, parentId, updateLayout) {
const element = this.elements[elementId];
const parent = (parentId == "#") ? this.getElement() : this.elements[parentId];
if (!(parent instanceof HTMLElement)) return;
if (element instanceof UiElement) {
element.appendTo(parent);
} else if (element instanceof HTMLElement) {
parent.appendChild(element);
} else {
return;
}
if (!updateLayout) return;
let parentLayout = null;
for (let i = 0; i < this.config.layout.length; ++i) {
const index = this.config.layout[i].children.indexOf(elementId);
if (index > -1) {
this.config.layout[i].children.splice(index, 1);
parentLayout = this.config.layout[i];
break;
}
}
if (!parentLayout) {
parentLayout = { parent: "parentId", children: [] };
this.config.layout.push(parentLayout);
}
parentLayout.children.push(elementId);
}
/**
* Remove and dispose an element from the viewer and optionally update the layout
* @param {string} elementId
* @param {boolean} updateLayout default is false
*/
removeElement(elementId, updateLayout) {
const element = this.elements[elementId];
if (element instanceof UiElement) {
element.dispose();
} else if (element instanceof HTMLElement) {
element.remove();
} else {
return;
}
if (!updateLayout) return;
for (let i = 0; i < this.config.layout.length; ++i) {
const index = this.config.layout[i].children.indexOf(elementId);
if (index > -1) {
this.config.layout[i].children.splice(index, 1);
break;
}
}
}
/**
* parse the json config to load the viewer
* @private
*/
parseConfig_() {
this.originalConfig = cloneDeep(this.config);
// assign config override
if (typeof this.options.configOverride == "object") {
Object.assign(this.config, this.options.configOverride);
}
// convert template config to regular viewer config
if (this.config.template && (this.config.template in templates)) {
this.template = this.config.template;
this.config = templates[this.config.template].getViewerConfig(this.config);
}
// copy config object
const config = cloneDeep(this.config);
// css theme
if (config.theme) setCSSProperties(config.theme);
// load baseLayers
const baseLayers = Object.assign({}, BASELAYERS);
if (config.baseLayers) Object.assign(baseLayers, config.baseLayers);
if (Array.isArray(baseLayers.excluded)) {
baseLayers.excluded.forEach(layer => {
if (layer in baseLayers) delete baseLayers[layer];
});
}
for (let name in baseLayers) {
if (name == "excluded") continue;
this.baseLayers[name] = {};
for (let key in baseLayers[name]) {
const baseLayer = baseLayers[name][key];
this.baseLayers[name][key] = (typeof baseLayer == "object")
? loadLayer(baseLayer, {})
: baseLayer;
// apply key as title, to allow identifying the baselayer in the map
if (typeof baseLayer == "object") this.baseLayers[name][key].title = name;
}
}
// load layers
this.layerManager.loadLayers(config.layers || {}, config.cache || {}).then(() => {
this.fire("layers_loaded", this);
// parse filters
if (config.filters) {
this.filters = new Filters(this.layerManager);
this.filters.on("remove", id => this.removeElement(id));
this.filters.on("add", e => {
this.elements[e.id] = e.filter;
});
this.filters.addFilters(config.filters);
const setFilter = layer => {
if ((layer instanceof Layer) && !layer.filterDisabled) {
// Filter out active filters that exclude this layer
const applicableFilters = this.activeFilters.filter(filter =>
!filter.excludedLayers.includes(layer.uniqueId)
);
// add the filter but only apply it if the layer is active
layer.setFilter(applicableFilters);
if (layer.active) layer.applyFilter();
}
};
this.layerManager.on("layermanager_add_layer", layer => setFilter(layer));
this.filters.on("change", () => {
this.activeFilters = this.filters.getActiveFilters();
this.layerManager.forEach(layer => setFilter(layer));
})
// trigger change event to apply filters to all layers
this.filters.fire("change", this.filters);
}
// load elements
for (let i = 0; i < config.elements.length; ++i) {
const element = config.elements[i];
if (element.type in loader) {
loader[element.type](element, this);
}
}
// parse layout
for (let i = 0; i < config.layout.length; ++i) {
const layout = config.layout[i];
for (let j = 0; j < layout.children.length; ++j) {
this.appendElement(layout.children[j], layout.parent);
}
}
this.fire("viewer_loaded", this);
});
}
/**
* load the config-file from the given path
* @private
*/
loadConfig_() {
return new Promise((resolve, reject) => {
if (typeof this.config == "string") {
fetch(this.config)
.then(response => response.json())
.then(json => {
this.config = json;
resolve(this);
})
.catch(e => reject(e));
} else if (typeof this.config == "object") {
resolve(this)
} else {
reject("invalid viewer config");
}
});
}
/**
* Gets all instances of LayerTree and
* returns all used layers in an array
*
* @private
* @param {object[]} elements element configs
* @returns {object[]} layer configs
*/
getUsedLayers_(elements) {
const layers = {};
const cache = {};
for (let i in elements) {
if (elements[i].type.toLowerCase() == "layertree") {
const structure = elements[i].structure;
for (let parent of structure) {
for (let child of parent.children || []) {
if (!(child in layers)) {
const layer = this.layerManager.getLayerById(child);
if (layer) {
layers[child] = layer.getConfig();
const cacheElement = layer.getCache();
if (Object.keys(cacheElement).length > 0) {
cache[child] = cacheElement;
}
}
}
}
}
}
}
return {
cache: cache,
layers: layers
};
}
/**
* Clear all viewer elements
* @returns {Promise}
*
* @override
*/
dispose() {
super.dispose();
return new Promise((resolve, reject) => {
this.promise_.finally(() => {
for (let i in this.elements) {
const element = this.elements[i];
if (element instanceof UiElement) {
element.dispose();
} else if (element instanceof HTMLElement) {
element.remove();
}
}
this.layerManager.dispose();
resolve();
});
});
}
/**
* Get the JSON config of the current viewer state
*
* @param {boolean} useTemplate convert generic config to template config (if template was used) default is false
* @returns {object} viewer config
*/
getConfig(useTemplate = false) {
let config = cloneDeep(this.config);
// parse element options
const elementOptions = {};
for (let i in this.elements) {
if (typeof this.elements[i].getOptions == "function") {
elementOptions[i] = this.elements[i].getOptions();
}
}
// merge element options
for (let i = 0; i < config.elements.length; ++i) {
const id = config.elements[i].id;
if (id in elementOptions) {
config.elements[i] = Object.assign(config.elements[i], elementOptions[id]);
}
}
// add layers
const layers = this.getUsedLayers_(config.elements);;
config.layers = layers.layers;
config.cache = layers.cache;
// re-write filter options
config.filters = [];
for (let id in this.filters.filters) {
const filterOptions = this.filters.filters[id].getOptions();
filterOptions.id = id;
config.filters.push(filterOptions);
}
// convert generic viewer config to template config
if (useTemplate && this.template && (this.template in templates)) {
config = templates[this.template].getTemplateConfig(config, this.originalConfig);
}
return config;
}
_initConfigDragDrop() {
new FileDropTarget(this.getElement()).on("drop", (files) => {
console.log(files);
if (files.length == 0) return;
// only check first file
const file = files[0];
if (file.name.endsWith("viewer.json")) {
const reader = new FileReader();
reader.onload = e => {
const viewer = new Viewer(null, JSON.parse(e.target.result), this.options)
this.fire("drag_drop_viewer_loaded", viewer);
}
reader.readAsText(file);
}
});
}
}