import { ContextMenu } from "../ui/ContextMenu.js";
import { SidebarElement } from "../ui/sidebar/SidebarElement.js";
import { GeoJSONLayer } from "./layer/GeoJSONLayer/GeoJSONLayer.js";
import { WMSLayer } from "./layer/WMSLayer/WMSLayer.js";
import { LayerLegend } from "./utils/LayerLegend.js";
import { CsvLayer } from "./layer/CsvLayer/CsvLayer.js";
import { displayMessage, saveAsFile } from "../utils/utils.js";
import { retrieveDataTransfer } from "../ui/dnd/util.js";
import { Window } from "../ui/Window.js";
import { ImporterWindow } from "./importer/ImporterWindow.js";
import { Folder } from "./layer/Folder/Folder.js";
import { FilterLayer } from "./layer/FilterLayer/FilterLayer.js";
import { WFSLayer } from "./layer/WFSLayer/WFSLayer.js";
import { StyleSettings } from "./utils/StyleSettings.js";
import { LayerSettings } from "./utils/LayerSettings.js";
import "./LayerTree.css";
export { LayerTree };
/**
* LayerTree class that defines the layer tree and manipulates it
*
* @author awalter <andreas.walter@awi.de>
* @author sjaswal <shahzeib.jaswal@awi.de>
* @author rhess <robin.hess@awi.de>
*
* @memberof vef.map
*/
class LayerTree extends SidebarElement {
constructor(target, layerManager, options) {
super(target, {
"layertree_add_layer": [],
"layertree_select": [],
"layertree_deselect": [],
"layertree_show_info": [],
"layertree_zoom_layer": [],
"layertree_rename_layer": [],
"layertree_reorder": []
});
const defaultOptions = {
title: "Layer Tree",
addLayerMethods: [],
indexUrl: null,
cswUrl: null,
maxClickedLayers: 10
};
// Merge default options with given options
this.options_ = { ...defaultOptions, ...options };
this.layerManager_ = layerManager;
this.items_ = {};
this.deleteMode_ = false;
this.editMode_ = false;
this.contextMenu_ = new ContextMenu();
this.openedFoldersOnSearch_ = [];
this.initHtmlElement_();
if (options.structure) this.setStructure(options.structure);
// remove event for layers
this.layerManager_.on("layermanager_remove_layer", layer => {
try {
this.removeItem(layer.uniqueId);
} catch (e) {
console.warn("could not remove layer from LayerTree: " + layer.uniqueId);
}
});
}
/**
* @private
*/
download_(item) {
if (item.layer instanceof Folder) {
const features = [];
const children = item.element.querySelectorAll('.list-item');
for (let i = 0; i < children.length; ++i) {
const id = children[i].dataset.id;
const layer = this.getItem(id).layer;
if (layer instanceof GeoJSONLayer) {
features.push(layer.exportGeometry("object"));
}
}
const json = JSON.stringify({
type: "FeatureCollection",
features: features
});
const blob = new Blob([json], { type: "text/plain;charset=utf-8" });
saveAsFile(blob, item.layer.title + ".json");
} else if (item.layer instanceof GeoJSONLayer) {
item.layer.exportGeometry("download");
}
}
/**
* @private
*/
openStyleSettings_(pointerEvent, layer) {
if (!this.styleSettings_) this.styleSettings_ = new StyleSettings();
this.styleSettings_.setOptions({ pointerEvent: pointerEvent });
this.styleSettings_.open(layer);
}
/**
* @private
*/
openLayerSettings_(pointerEvent, layer) {
if (!this.layerSettings_) this.layerSettings_ = new LayerSettings();
this.layerSettings_.setOptions({ pointerEvent: pointerEvent });
this.layerSettings_.open(layer);
}
clearTree() {
for (let i in this.items_) {
this.deselectNodes(i);
}
const children = this.list_.querySelectorAll(".list-item");
for (let i = 0; i < children.length; ++i) {
children[i].remove();
}
this.items_ = {};
}
setStructure(structure) {
this.clearTree();
const activeNodes = [];
for (let i in structure) {
const parent = structure[i].parent;
const children = structure[i].children;
for (let j in children) {
const id = children[j];
const layer = this.layerManager_.getLayerById(id);
if (layer) {
const role = (layer instanceof Folder) ? "folder" : "layer";
const item = this.addItem_(parent, id, role);
if ((item.role == "layer") && item.layer.active) activeNodes.push(id);
}
}
}
for (let i in activeNodes) {
this.selectNodes(activeNodes[i]);
}
this.initDragDrop_();
this.setLayerIndex_();
}
/**
* Get the Layer order structure of the LayerTree
*
* @returns {object[]} structure
*/
getStructure() {
const nodes = this.getNodes();
const structure = {};
for (let i = 0; i < nodes.length; ++i) {
const id = nodes[i].parentElement.dataset.id || "#";
if (!(id in structure)) structure[id] = [];
structure[id].push(nodes[i].dataset.id);
}
const orderedStructure = [];
const recursiveSort = (id) => {
orderedStructure.push({
parent: id,
children: structure[id]
});
if (id in structure) {
for (let key of structure[id]) {
if (key in structure) recursiveSort(key)
}
}
};
recursiveSort("#");
return orderedStructure;
}
/**
* Get the layertree's options-object
*/
getOptions() {
return {
title: this.options_.title,
addLayerMethods: this.options_.addLayerMethods,
indexUrl: this.options_.indexUrl,
cswUrl: this.options_.cswUrl,
maxClickedLayers: this.options_.maxClickedLayers,
structure: this.getStructure()
};
}
/**
* Get a layerManager list-Item by passing a uniqueId or
* an instance of a HTMLElement list-item
*
* @param {string | HTMLElement} target
*/
getItem(target) {
if (target instanceof HTMLElement) {
target = target.dataset.id;
}
if (typeof target == "string") {
const item = this.items_[target];
if (typeof item == "object") {
// copy the object, so the original cannot be changed
return Object.assign({}, item);
}
throw new Error("invalid id: layer with the id '" + target + "' does not exist");
}
throw new Error("invalid type: target must be a string or an instance of HTMLElement");
}
/**
* Select a node by passing a uniqueId or
* an instance of a HTMLElement list-item
*
* @param {string/HTMLElement} target
*/
selectNodes(target) {
for (let i = 0; i < arguments.length; ++i) {
const item = this.getItem(arguments[0]);
const node = item.element;
if (node.getAttribute("selected") == "true") continue;
this.fullSelected_(node);
this.fire("layertree_select", item.layer);
const children = node.querySelectorAll('.list-item[selected="false"]');
for (let c = 0; c < children.length; ++c) {
if (children[c].getAttribute("disabled") == "true") continue;
this.selectNodes(children[c]);
}
const messageIcon = node.querySelector(".td-item-message i.item-message");
if (messageIcon) messageIcon.style.display = "block";
this.setParent_(node.parentNode);
}
}
/**
* Deselect a node by passing a uniqueId or
* an instance of a HTMLElement list-item
*
* @param {string/HTMLElement} target
*/
deselectNodes(target) {
for (let i = 0; i < arguments.length; ++i) {
const item = this.getItem(arguments[0]);
const node = item.element;
if (node.getAttribute("selected") == "false") continue;
this.deselected_(node);
this.fire("layertree_deselect", item.layer);
const children = node.querySelectorAll('.list-item[selected="true"]');
for (let c = 0; c < children.length; ++c) {
if (children[c].getAttribute("disabled") == "true") continue;
this.deselectNodes(children[c]);
}
const messageIcon = node.querySelector(".td-item-message i.item-message");
if (messageIcon && (messageIcon.dataset.level < 1)) messageIcon.style.display = "none";
this.setParent_(node.parentNode);
}
}
/**
* Enable a node by passing a uniqueId or
* an instance of a HTMLElement list-item
*
* @param {string/HTMLElement} target
*/
enableNodes(target) {
for (let i = 0; i < arguments.length; ++i) {
const item = this.getItem(arguments[0]);
const node = item.element;
const children = node.querySelectorAll('.list-item');
for (let c = 0; c < children.length; ++c) {
if (children[c].getAttribute("disabled") == "false") continue;
this.enableNodes(children[c]);
}
if (node.getAttribute("disabled") == "false") continue;
node.setAttribute("disabled", "false");
}
}
/**
* Disable a node by passing a uniqueId or
* an instance of a HTMLElement list-item
*
* @param {string/HTMLElement} target
*/
disableNodes(target) {
for (let i = 0; i < arguments.length; ++i) {
const item = this.getItem(arguments[0]);
const node = item.element;
const children = node.querySelectorAll('.list-item');
for (let c = 0; c < children.length; ++c) {
if (children[c].getAttribute("disabled") == "true") continue;
this.disableNodes(children[c]);
}
if (node.getAttribute("disabled") == "true") continue;
node.setAttribute("disabled", "true");
}
}
/**
* show loading animation for a given target layer.
* uses recursion for handling folders
*
* @param {string/HTMLElement} target
*/
showLoadingIcon(target) {
try {
const el = (target instanceof HTMLElement) ? target : this.getItem(target).element;
el.querySelector(`.item-svg`).style.display = "flex";
if (el.parentNode.classList.contains("list-item")) {
this.showLoadingIcon(el.parentNode);
}
} catch (e) {
console.warn("Layer not found in the LayerTree: cannot show loading icon");
}
}
/**
* hide loading animation for a given target layer.
* uses recursion for handling folders
*
* @param {string/HTMLElement} target
*/
hideLoadingIcon(target) {
try {
const el = (target instanceof HTMLElement) ? target : this.getItem(target).element;
let isLoading = false;
const children = el.querySelectorAll(".list-item");
for (let i = 0; i < children.length; ++i) {
const icon = children[i].querySelector('.item-svg');
if (icon.style.display != "none") {
isLoading = true;
break;
}
}
if (!isLoading) {
el.querySelector(`.item-svg`).style.display = "none";
if (el.parentNode.classList.contains("list-item")) {
this.hideLoadingIcon(el.parentNode);
}
}
} catch (e) {
console.warn("Layer not found in the LayerTree: cannot hide loading icon");
}
}
/**
* Display a message icon with a hover text
*
* @param {string/HTMLElement} target
* @param {object[]} messages
*/
showMessages(target, messages) {
try {
target = (target instanceof HTMLElement) ? target : this.getItem(target).element;
} catch (e) {
console.warn("Layer not found in the LayerTree: cannot show loading icon");
}
const wrapper = target.querySelector(`.td-item-message`);
wrapper.innerHTML = "";
if (messages.length == 0) return;
// let maxLevel = 0;
let messageContent = document.createElement("div");
messageContent.classList.add("layer-tree-message-content");
const levels = [{ color: "var(--primary-color)", icon: "warning" }, { color: "#ffbb00", icon: "warning-filled" }, { color: "var(--error-color)", icon: "warning-filled" }];
for (let i = 0; i < messages.length; ++i) {
const message = messages[i];
if (message.level >= levels.length) message.level = levels.length - 1;
// if (message.level > maxLevel) maxLevel = message.level;
messageContent.innerHTML += `
<div class="message-wrapper">
<div class="color-bar"><i class="vef vef-${levels[message.level].icon}" style="color:${levels[message.level].color};"></i></div>
<div class="message-content">${message.content}</div>
</div>
`;
}
const icon = document.createElement("i");
// icon.classList.add("item-message", "vef", "vef-" + levels[maxLevel].icon);
icon.classList.add("item-message", "vef", "vef-" + levels[0].icon);
// icon.style.color = levels[maxLevel].color;
icon.style.color = levels[0].color;
icon.style.width = "100%";
icon.style.height = "100%";
icon.style.display = "block";
// icon.dataset.level = maxLevel;
icon.dataset.level = 0;
wrapper.appendChild(icon);
if (!this.messageWindow_) {
this.messageWindow_ = new Window(this.getElement(), {
width: "330px",
height: "unset",
open: false,
mode: "slim",
title: null
});
const windowElement = this.messageWindow_.getElement();
windowElement.classList.add("pointer-left", "layer-tree-message-window");
windowElement.style.pointerEvents = "none";
windowElement.querySelector(".fas.fa-times").style.display = "none";
}
icon.addEventListener("mouseover", () => {
let rect = icon.getBoundingClientRect();
this.messageWindow_.setOptions({
top: (rect.top - 3) + "px",
left: (rect.left + rect.width + 15) + "px",
open: true,
content: messageContent
});
});
// close info window when leaving
icon.addEventListener("mouseleave", () => this.messageWindow_.close());
}
/**
* @returns {NodeList} NodeList containing all selected list-item elements
*/
getSelectedNodes() {
return this.list_.querySelectorAll('.list-item[selected="true"]');
}
getSelectedNodeLayers() {
return this.list_.querySelectorAll('.list-item[selected="true"].list-item[role="layer"]');
}
getSelectedLayers() {
const selectedNodes = this.getSelectedNodes();
const selectedLayers = {};
for (let i = 0; i < selectedNodes.length; ++i) {
const id = selectedNodes[i].dataset.id;
selectedLayers[id] = this.layerManager_.getLayerById(id);
}
return selectedLayers;
}
/**
* @returns {NodeList} NodeList containing all list-item elements
*/
getNodes() {
return this.list_.querySelectorAll('.list-item');
}
/**
* @returns {Object[]} Array of selected items
*/
getSelectedItems() {
const selectedItems = [];
for (let i in this.items_) {
const item = this.items_[i];
if (item.element.getAttribute("selected") == "true") {
selectedItems.push(Object.assign({}, item));
}
}
return selectedItems;
}
/**
* @returns {Object[]} Array of all items
*/
getItems() {
const items = [];
for (let i in this.items_) {
items.push({ ...{}, ...this.items_[i] });
}
return items;
}
/**
* @param {string | HTMLElement} target
* @returns {number} get the z-index of a layer
*/
getLayerIndex(target) {
const item = this.getItem(target);
return (typeof item == "object") ? item.index : null;
}
/**
* remove context menu events from a layer
*
* @param {object} item
*/
removeContextMenuEntry_(item) {
this.contextMenu_.unregisterElement(item.element.querySelector(".layertree-content-table"));
}
/**
* bind context menu events to a layer
*
* @param {object} item
*/
addContextMenuEntry_(item) {
// available for all items
const callbacks = [
{
icon: "vef vef-rename",
text: "Rename",
callback: () => this.editTitle(item.element.querySelector(".table-title"), item)
},
{
icon: "fas fa-trash",
text: "Delete",
callback: () => this.removeItem(item.id)
}
];
const l = item.layer;
if (l instanceof Folder) {
// add download button to folders containing GeoJSONLayers
callbacks.unshift({
icon: "fas fa-download",
text: "Download",
callback: () => this.download_(item),
condition: () => {
const children = item.element.querySelectorAll('.list-item');
for (let i = 0; i < children.length; ++i) {
const id = children[i].dataset.id;
const layer = this.getItem(id).layer;
if (layer instanceof GeoJSONLayer) return true;
}
return false;
}
});
// add upload button to folders containing CsvLayers
callbacks.unshift({
icon: "fas fa-upload",
text: "Reload Data File",
callback: () => CsvLayer.requestFileUpload(),
condition: () => {
const children = item.element.querySelectorAll('.list-item');
for (let i = 0; i < children.length; ++i) {
const id = children[i].dataset.id;
const layer = this.getItem(id).layer;
if (layer instanceof CsvLayer) return true;
}
return false;
}
});
} else {
callbacks.unshift({
icon: "fas fa-expand-arrows-alt",
text: "Zoom to Layer",
callback: () => this.fire('layertree_zoom_layer', item.layer)
});
if (l instanceof GeoJSONLayer) {
callbacks.unshift({
icon: "fas fa-download",
text: "Download",
callback: () => this.download_(item)
});
}
if ((l instanceof WMSLayer) && !l.filterDisabled) {
callbacks.unshift({
icon: "vef vef-legend-small",
text: "Show Legend",
callback: () => {
const legend = new LayerLegend(null, this.layerManager_);
legend.on("close", () => {
legend.off("close");
legend.dispose();
});
legend.open(l);
}
});
}
if ((l instanceof WMSLayer) || (l instanceof WFSLayer)) {
callbacks.unshift({
icon: "fas fa-sync-alt",
text: "Reload Metadata",
callback: () => this.layerManager_.reloadServiceMetadata(l.serviceUrl, l.type)
});
}
if (l instanceof CsvLayer) {
callbacks.unshift({
icon: "fas fa-upload",
text: "Reload Data File",
callback: () => CsvLayer.requestFileUpload(l.fileIdentifier)
});
}
}
//callbacks.push({
// icon: "fas fa-cog",
// text: "Options",
// callback: (e) => this.openLayerSettings_(e, item.layer)
//});
if (l.opacitySettings || l.styleSettings || l.colorScaleSettings) {
callbacks.push({
icon: "fas fa-palette",
text: "Style",
callback: (e) => this.openStyleSettings_(e, item.layer)
});
}
callbacks.unshift({
icon: "fas fa-info",
text: "Information",
callback: () => this.fire("layertree_show_info", item.layer)
});
this.contextMenu_.registerElement(item.element.querySelector(".layertree-content-table"), callbacks);
}
/**
* internal method for adding items
*
* @param {string | HTMLElement} parent
* @param {string | Object} layer
* @param {string} role layer or folder
* @param {string} position top, bottom (default: bottom)
*
* @private
*/
addItem_(parent, layer, role, position) {
parent = parent || "#";
role = (role == "folder") ? "folder" : "layer";
position = (position) ? position.toLowerCase() : "bottom";
// validate layer
if (typeof layer == "string") {
const cacheLayer = this.layerManager_.getLayerById(layer);
if (!cacheLayer) throw new Error(`layer with id="${layer}" does not exist in the LayerManager`);
layer = cacheLayer;
} else if (typeof layer == "object") {
const cacheLayer = this.layerManager_.getLayerById(layer.uniqueId);
if (!cacheLayer) this.layerManager_.addLayer(layer);
} else {
throw new Error("invalid type for layer argument");
}
// check if layer already exists in the LayerTree
if (this.items_[layer.uniqueId]) {
throw new Error(`Layer with id "${layer.uniqueId}" already exists in the LayerTree`);
}
const item = {
id: layer.uniqueId,
layer: layer,
role: role,
title: layer.title,
index: 0,
element: null
};
this.createListElement_(item);
// set parent element
if (parent == '#') {
this.list_.insertAdjacentElement((position == "top") ? "afterbegin" : "beforeend", item.element);
} else {
if (position == "top") {
this.getItem(parent).element.querySelector(".layertree-content-table").insertAdjacentElement("afterend", item.element);
} else {
this.getItem(parent).element.insertAdjacentElement("beforeend", item.element);
}
}
this.items_[layer.uniqueId] = item;
// fire add layer event
this.fire("layertree_add_layer", layer);
// add context menu event
this.addContextMenuEntry_(item);
// display messages if available
this.showMessages(item.element, layer.getMessages());
layer.on("layer_message", messages => this.showMessages(item.element, messages));
layer.on("layer_update", l => {
const span = item.element.querySelector(".table-title");
item.title = l.title;
span.innerText = l.title;
span.title = l.title;
});
return item;
}
/**
* Add an item (layer or folder) to the LayerTree.
* {@code layer} can be uniqueId or a layer-object
*
* @param {string | HTMLElement} parent
* @param {string | Object} layer
* @param {string} role layer or folder
* @param {string} position top, bottom (default: bottom)
*/
addItem(parent, layer, role, position) {
const item = this.addItem_(parent, layer, role, position);
if (item.layer.active) this.selectNodes(layer.uniqueId);
if (!(item.layer instanceof FilterLayer)) this.initDragDrop_(item.element);
this.setLayerIndex_();
return item;
}
/**
* Add layer to the LayerTree. {@code layer}
* can be uniqueId or a layer-object
*
* @param {string | HTMLElement} parent
* @param {string | Object} layer
* @param {string} position top, bottom (default: bottom)
*/
addLayer(parent, layer, position) {
return this.addItem(parent, layer, "layer", position);
}
/**
* Add folder to the LayerTree. {@code layer}
* can be uniqueId or a layer-object
*
* @param {string | HTMLElement} parent
* @param {string | Object} layer
* @param {string} position top, bottom (default: bottom)
*/
addFolder(parent, layer, position) {
return this.addItem(parent, layer, "folder", position);
}
/**
* Remove a Layer or Folder from the layertree
*
* @param {string | HTMLElement} target
*/
removeItem(target) {
const item = this.getItem(target);
const element = item.element;
this.deselectNodes(target);
// remove children
const children = element.querySelectorAll(".list-item");
for (let i = 0; i < children.length; ++i) {
this.removeContextMenuEntry_(this.items_[children[i].dataset.id]);
delete this.items_[children[i].dataset.id];
}
this.removeContextMenuEntry_(item);
delete this.items_[item.id];
element.remove();
this.setLayerIndex_();
}
/**
* @private
*/
setLayerIndex_() {
const items = this.list_.querySelectorAll('.list-item')
for (let i = 0; i < items.length; ++i) {
const item = this.items_[items[i].dataset.id];
item.index = 1000 - i;
if (item.layer.setZIndex) {
item.layer.setZIndex(item.index);
} else if (item.layer.bringToBack) {
item.layer.bringToBack();
}
// Check if an element hast children to set the attribute data-hasChildren.
// Used for in CSS to recognize Folders, because the .list-item:has(.list-item) does not Work in Firefox
const children = items[i].querySelectorAll('.list-item');
items[i].dataset.hasChildren = (children.length > 0);
// limit max amount of layers that can be selected
if ((this.options_.maxClickedLayers > 0) && (item.layer instanceof Folder)) {
let layerCount = 0;
for (let j = 0; j < children.length; ++j) {
const child = this.items_[children[j].dataset.id];
if (!(child.layer instanceof Folder) && !(child.layer instanceof FilterLayer)) {
++layerCount
}
}
const icon = items[i].querySelector(".fa-stack");
if (layerCount > this.options_.maxClickedLayers) {
items[i].dataset.preventSelection = true;
icon.title = "This folder cannot be selected directly, because contains too many layers.";
} else {
items[i].dataset.preventSelection = false;
icon.title = "";
}
}
}
}
/**
* Create the main structure of the LayerTree
*
* -----------------------------------------------------------
* <div>
* <h3>TITLE</h3>
* <ul class="list-layers">
* <li data-id="LAYER_NAME" class="list-item" selected="true" role="layer"></li>
* <ul data-id="LAYER_NAME" class="list-item" selected="true" role="folder" active="true">
* <li data-id="LAYER_NAME" class="list-item" selected="true" role="layer"></li>
* </ul>
* </ul>
* </div>
* -----------------------------------------------------------
* @private
*/
initHtmlElement_() {
this.setTitle(this.options_.title);
this.getElement().classList.add("layer-tree-container");
const list = document.createElement("ul");
list.classList.add("list-layers");
const tools = this.initTools_();
// add elements to content container
let layers = this.getContentContainer();
layers.classList.add('layer-tree');
layers.appendChild(tools);
layers.appendChild(list);
this.list_ = list;
this.tools_ = tools;
}
/**
* @private
*/
initTools_() {
const tools = document.createElement("div");
// delete button
this.addTool("fas fa-trash delete-button", (enabled) => this.setDeleteMode_(enabled), "Remove Layers", true);
// edit button
this.addTool("vef vef-rename edit-button", (enabled) => this.setEditMode_(enabled), "Rename Layers", true);
// Add Layer Button
this.addTool("vef vef-add-layer", (toggle, e) => {
this.initImporter_();
this.dataImporter_.setOptions({
pointerEvent: e,
open: true
});
}, "Add Layers");
// Add Folder Button
this.addFolderForm_ = this.initAddFolderForm_();
tools.appendChild(this.addFolderForm_);
// search button
this.addSearchForm_ = this.initAddSearchForm_();
tools.appendChild(this.addSearchForm_);
return tools;
}
/**
* @private
*/
initFormTool_(placeholder, buttonText, toolIcon, toolText, toolEvent, formEvent) {
const formContainer = document.createElement("div");
formContainer.classList.add("text-input-field");
formContainer.style.display = "none";
formContainer.innerHTML = `
<i class="fas fa-times"></i><!--
--><input spellcheck="false" placeholder="${placeholder}" type="text"/><button><i class="fas fa-plus"></i> ${buttonText}<!--
--></button>
`;
const form = formContainer.querySelector("input");
form.addEventListener("keypress", (e) => {
if (e.code == "Enter") formEvent(form)
});
this.addTool(toolIcon, toolEvent, toolText);
formContainer.querySelector(".fa-times").setAttribute("tabindex", "0");
formContainer.querySelector("button").setAttribute("tabindex", "0");
formContainer.querySelector("button").addEventListener("click", () => formEvent(form));
formContainer.querySelector(".fa-times").addEventListener("click", () => formContainer.style.display = "none");
formContainer.querySelector(".fa-times").addEventListener("keydown", (e) => {
if (e.key != "Enter") return;
formContainer.style.display = "none"
});
return formContainer;
}
/**
* @private
*/
initSearchTool_(placeholder, buttonText, toolIcon, toolText, toolEvent, formEvent) {
const formContainer = document.createElement("div");
formContainer.classList.add("text-input-field");
formContainer.style.display = "none";
formContainer.innerHTML = `
<i class="fas fa-times"></i><!--
--><input spellcheck="false" placeholder="${placeholder}" type="text"/><button><i class="fas fa-search"></i> ${buttonText}<!--
--></button>
`;
const form = formContainer.querySelector("input");
form.addEventListener("keyup", (e) => {
formEvent(form)
});
formContainer.querySelector(".fa-times").setAttribute("tabindex", "0");
formContainer.querySelector("button").setAttribute("tabindex", "-1");
formContainer.querySelector("button").addEventListener("click", () => formEvent(form));
formContainer.querySelector(".fa-times").addEventListener("click", (e) => {
formContainer.style.display = "none";
this.resetSearch();
});
formContainer.querySelector(".fa-times").addEventListener("keydown", (e) => {
if (e.key != "Enter") return;
formContainer.style.display = "none";
this.resetSearch();
});
this.addTool(toolIcon, toolEvent, toolText);
return formContainer;
}
/**
* @private
*/
initAddFolderForm_() {
let formContainer;
const addFolder = (form) => {
const value = form.value;
if (value) {
this.addFolder("#", new Folder({ title: value }), "top");
formContainer.style.display = "none";
}
};
formContainer = this.initFormTool_("New Folder", "FOLDER", "vef vef-add-folder", "Add Folder", () => this.showAddFolderForm_(), addFolder);
return formContainer;
}
/**
* @private
*/
initAddSearchForm_() {
let formContainer;
const doSearch = (form) => {
const value = form.value;
if (value) {
this.doSearch(value);
} else {
this.resetSearch();
}
};
formContainer = this.initSearchTool_("Seachbar ...", "SEARCH", "fas fa-search", "Search", () => this.showSearchForm_(), doSearch);
return formContainer;
}
/**
* @private
*/
initImporter_() {
if (this.dataImporter_) return;
const importer = new ImporterWindow(null, {
catalog: {
url: this.options_.indexUrl
}
});
importer.on("add_layers", data => {
if (!Array.isArray(data)) data = [data];
for (let i in data) {
if (!data[i]) continue;
displayMessage("Added Layers to the Layer-Tree", 7000);
this.addStructuredLayers(data[i].layers, data[i].structure, "top");
}
});
importer.on("show_info", layer => {
this.fire("layertree_show_info", layer);
});
this.dataImporter_ = importer;
}
/**
* Add layers according to a predefined directory structure
*
* @param {object} layers
* @param {object} structure
* @param {string} position top, bottom (default=bottom). Only relevant for root level layers
*/
addStructuredLayers(layers, structure, position) {
for (let parent in structure) {
for (let id of structure[parent]) {
const pos = (parent == "#") ? position : "bottom";
if (id in structure) {
this.addFolder(parent, layers[id], pos);
} else {
this.addLayer(parent, layers[id], pos);
}
}
}
}
/**
* @private
*/
showAddFolderForm_() {
const input = this.addFolderForm_.querySelector("input");
input.value = "";
this.addSearchForm_.style.display = "none";
this.addFolderForm_.style.display = "flex";
input.focus();
}
/**
* @private
*/
showSearchForm_() {
const input = this.addSearchForm_.querySelector("input");
input.value = "";
this.addFolderForm_.style.display = "none";
this.addSearchForm_.style.display = "flex";
input.focus();
}
/**
* enable or disable delete mode
*
* @param {boolean} enabled
*
* @private
*/
setDeleteMode_(enabled) {
const tree = this.getContentContainer();
if (enabled) {
tree.classList.add('delete-mode');
} else {
tree.classList.remove('delete-mode');
}
this.deleteMode_ = enabled;
}
setEditMode_(enabled) {
const tree = this.getContentContainer();
if (enabled) {
tree.classList.add('edit-mode');
} else {
tree.classList.remove('edit-mode');
}
this.editMode_ = enabled;
}
getTemplate_(content) {
return `
<table class="layertree-content-table">
<tr>
<td class="td-item-chevron" data-id="item-chevron-${content.uniqueId}">
<i class="fas"></i>
</td>
<td class="td-item-select" data-id="item-select-${content.uniqueId}" select-id="item-select-${content.uniqueId}" tabindex="0">
${content.selectElement}
</td>
<td class="td-item-icon" data-id="item-icon-${content.uniqueId}" select-id="item-select-${content.uniqueId}"><i></i></td>
<td class="td-item-title" select-id="item-select-${content.uniqueId}">
<span data-id="item-title-${content.uniqueId}" class="table-title" title="${content.title}">${content.title}</span>
</td>
<td class="td-item-svg">
<span class="item-svg" style="display: none">
<svg xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" style="margin: auto; shape-rendering: auto;" width="20px" height="20px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" r="32" stroke-width="8" stroke="var(--primary-color)" stroke-dasharray="50.26548245743669 50.26548245743669" fill="none" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" keyTimes="0;1" values="0 50 50;360 50 50"></animateTransform>
</circle>
</svg>
</span>
</td>
<td class="td-item-message"></td>
<td class="td-item-menu">
<span class="item-menu context-dropdown"><i class="fas fa-caret-down"></i></span>
</td>
</tr>
</table>
`;
}
/**
* combines the searching values
*
* @param {Object} obj
* @returns {String} returns modified string
* @private
*/
combineSearchFilterOptions(obj) {
if (typeof obj == "string") {
return obj.toLowerCase();
} else if (Array.isArray(obj)) {
return obj.reduce((acc, val) => {
if (typeof val != "string") return acc;
return acc + " " + val.toLowerCase();
}, "");
}
return "";
}
/**
* searches for a value and shows folder and layer that match
*
* @param {String} value
*
* @private
*/
doSearch(value) {
this.resetSearch();
value = value.toLowerCase();
const properties = ["title", "name", "serviceKeywords", "keywords", "originalTitle", "serviceTitle", , "serviceAbstract", "abstract"];
const keep = [];
const removed = [];
for (let i in this.items_) {
let searchingValues = "";
const item = this.items_[i];
for (let property of properties) {
if (property in item.layer) {
searchingValues += this.combineSearchFilterOptions(item.layer[property]);
}
}
if (searchingValues.includes(value)) {
const parents = this.getParentElements(item.element);
parents.forEach(parent => {
const parentItem = this.getItem(parent);
if (!parentItem.layer.expanded) {
this.open_(parentItem);
this.openedFoldersOnSearch_.push(parentItem);
}
});
keep.push(item.element, ...parents, ...this.getChildrenElements(item.element));
} else {
removed.push(item.element);
}
}
removed.forEach(removedElement => {
if (!keep.includes(removedElement)) {
removedElement.classList.add("search-hidden");
}
});
}
/**
* returns all parent elements of a given element
*
* @param {HTMLElement} element
*
* @returns {HTMLElement} parent
*/
getParentElements(element) {
let parents = [];
const parent = element.parentElement;
if (parent.classList.contains("list-item")) {
parents.push(parent);
parents = parents.concat(this.getParentElements(parent));
}
return parents;
}
/**
* returns the children elements of an item
*
* @param {HTMLElement} item
* @returns {HTMLElement[]} Array of children Elements
*/
getChildrenElements(element) {
const arr = [];
const children = element.querySelectorAll(".list-item");
for (let i = 0; i < children.length; ++i) {
arr.push(children[i]);
}
return arr;
}
/**
* reset structure without clearing the selected layer or folder
*/
resetSearch() {
for (let i in this.items_) {
this.items_[i].element.classList.remove("search-hidden");
}
this.openedFoldersOnSearch_.forEach(folder => {
this.close_(folder);
});
this.openedFoldersOnSearch_ = [];
}
/**
* @private
*/
open_(item) {
const el = item.element;
const chevronElement = el.querySelector('.td-item-chevron>i')
chevronElement.classList.remove("fa-chevron-right");
chevronElement.classList.add("fa-chevron-down");
item.layer.expanded = true;
el.setAttribute("expanded", "true");
}
/**
* @private
*/
close_(item) {
const el = item.element;
const chevronElement = el.querySelector('.td-item-chevron>i')
chevronElement.classList.remove("fa-chevron-down");
chevronElement.classList.add("fa-chevron-right");
item.layer.expanded = false;
el.setAttribute("expanded", "false");
}
/**
* @private
*/
deselected_(el) {
const selectionElement = el.querySelector('.td-item-select>span>i[class*="tree-checkbox"]');
selectionElement.classList.remove("tree-checkbox-full-selected");
selectionElement.classList.remove("tree-checkbox-part-selected");
selectionElement.classList.add("tree-checkbox");
el.setAttribute('selected', 'false');
}
/**
* @private
*/
fullSelected_(el) {
const selectionElement = el.querySelector('.td-item-select>span>i[class*="tree-checkbox"]');
selectionElement.classList.remove("tree-checkbox");
selectionElement.classList.remove("tree-checkbox-part-selected");
selectionElement.classList.add("tree-checkbox-full-selected");
el.setAttribute('selected', 'true');
}
/**
* @private
*/
partlySelected_(el) {
const selectionElement = el.querySelector('.td-item-select>span>i[class*="tree-checkbox"]');
selectionElement.classList.remove("tree-checkbox-full-selected");
selectionElement.classList.remove("tree-checkbox");
selectionElement.classList.add("tree-checkbox-part-selected");
el.setAttribute('selected', 'true');
}
/**
* Create a layer or folder ui-element with events for the LayerTree
* @private
*/
createListElement_(item) {
const fragment = document.createDocumentFragment();
const el = document.createElement("ul");
item.element = el;
el.dataset.id = item.id;
el.classList.add('list-item');
el.setAttribute('role', item.role);
el.setAttribute('selected', "false");
el.setAttribute('disabled', "false");
// Add Table element to Document Fragment
fragment.appendChild(el);
// Folder specific Icon Elements
const selectDummy = `
<span class="fa-stack">
<i class="far fa-square fa-stack-1x"></i>
<i class="fas fa-check fa-stack-1x tree-checkbox"></i>
<i class="fas fa-times fa-stack-1x delete-button"></i>
<i class="fas fa-square fa-stack-1x prevent-select-icon"></i>
</span>
`;
// Get Table Template and fill it with Icons
el.innerHTML = this.getTemplate_({
uniqueId: item.id,
title: item.title,
selectElement: selectDummy
});
// active=true => folder is 'open'
// active=false => folder is 'closed'
const triggerFolder = (setActive) => {
if (setActive) {
this.open_(item);
} else {
this.close_(item);
}
}
el.querySelector('.td-item-icon>i').classList = item.layer.icon;
triggerFolder(item.layer.expanded);
// Add click event for open/close folder (use timeout to not close instantly on double click)
let folderClickTimeout = null;
let elChevron = fragment.querySelector("[data-id='item-chevron-" + item.id + "']");
elChevron.onclick = (event) => {
if (!folderClickTimeout) {
triggerFolder(!item.layer.expanded);
folderClickTimeout = setTimeout(() => {
folderClickTimeout = null;
}, 400);
}
};
const elSelect = fragment.querySelector(".td-item-select");
const elIcon = fragment.querySelector(".td-item-icon");
const elTitle = fragment.querySelector(".td-item-title");
const elContextMenu = fragment.querySelector(".item-menu.context-dropdown");
// Add click event for select/deselect
let clickTimeout = null;
const triggerSelect = () => {
if ((el.dataset.preventSelection === "true") && (el.getAttribute("selected") !== "true")) {
this.open_(item);
return;
}
if (clickTimeout) {
// open/close folder on double click
clearTimeout(clickTimeout);
clickTimeout = null;
triggerFolder(!item.layer.expanded);
} else {
// select folder on single click (with timeout)
if (this.deleteMode_ || this.editMode_) return;
if (el.getAttribute("disabled") == "true") return;
if (el.getAttribute("selected") === "true") {
this.deselectNodes(item.id);
} else {
this.selectNodes(item.id);
}
clickTimeout = setTimeout(() => {
clickTimeout = null;
}, 400);
}
};
elIcon.addEventListener("click", triggerSelect);
elTitle.addEventListener("click", (e) => {
if (this.editMode_) {
this.editTitle(e.target, item);
} else {
triggerSelect();
}
});
elSelect.addEventListener("click", () => {
if (this.deleteMode_) {
this.removeItem(item.id);
} else {
triggerSelect();
}
});
elSelect.addEventListener("keydown", (e) => {
if (e.key != "Enter") return;
if (this.deleteMode_) {
this.removeItem(item.id);
} else {
triggerSelect();
}
})
elContextMenu.addEventListener("click", (e) => {
this.contextMenu_.open(el.querySelector(".layertree-content-table"), e.pageX, e.pageY);
});
}
/**
* @private
*/
setParent_(parent) {
if (parent.classList.contains("list-layers")) return;
const childrenLength = parent.querySelectorAll('.list-item').length;
const selectedChildrenLength = parent.querySelectorAll('.list-item[selected="true"]').length;
const selected = (parent.getAttribute("selected") == "true") ? true : false;
if (selectedChildrenLength == 0) {
this.deselected_(parent);
if (selected) this.fire("layertree_deselect", this.getItem(parent)?.layer);
}
else if (childrenLength == selectedChildrenLength) {
this.fullSelected_(parent);
if (!selected) this.fire("layertree_select", this.getItem(parent)?.layer);
}
else if (childrenLength > selectedChildrenLength) {
this.partlySelected_(parent);
if (!selected) this.fire("layertree_select", this.getItem(parent)?.layer);
}
this.setParent_(parent.parentNode);
}
/**
* Moves Items up and down by the given amount of steps
*
* @param {object} item
* @param {number} steps
*/
moveItem(item, steps) {
item = this.getItem(item);
const nodes = this.getNodes();
for (let i = 0; i < nodes.length; ++i) {
if (item.element == nodes[i]) {
const oldParent = nodes[i].parentElement;
let target, newParent;
let offset = i + steps;
while ((offset < nodes.length) && (offset >= 0) && (!newParent || !newParent.contains(nodes[i]) || nodes[i].contains(target))) {
target = nodes[offset];
newParent = target.parentElement;
offset = offset + steps;
}
if (!nodes[i].contains(target)) {
if ((oldParent != newParent) || (offset < i)) {
target.insertAdjacentElement("beforebegin", nodes[i]);
if (oldParent != newParent) {
this.setParent_(oldParent);
this.setParent_(newParent);
}
} else if (offset > i) {
target.insertAdjacentElement("afterend", nodes[i]);
}
this.setLayerIndex_();
this.fire("layertree_reorder", item.layer);
}
return;
}
}
}
/**
* @private
*/
initDragDrop_(targetElement) {
this.draggedLayer_ = null;
this.dropTimeout = null;
this.dropInside = false;
const setDropMarker = (item, position) => {
switch (position) {
case "before":
item.classList.add("drop-insert-before");
item.classList.remove("drop-insert-after");
item.classList.remove("drop-insert-inside");
break;
case "inside":
item.classList.add("drop-insert-inside");
item.classList.remove("drop-insert-before");
item.classList.remove("drop-insert-after");
break;
case "after":
item.classList.add("drop-insert-after");
item.classList.remove("drop-insert-before");
item.classList.remove("drop-insert-inside");
break;
default:
item.classList.remove("drop-insert-before");
item.classList.remove("drop-insert-after");
item.classList.remove("drop-insert-inside");
}
};
const getDropPosition = (table, e) => {
const item = table.parentElement;
const isFolder = item.getAttribute("role") == "folder";
if (isFolder && this.dropInside) {
return "inside";
} else {
const bounds = table.getBoundingClientRect();
const center = bounds.top + (bounds.height / 2);
if (e.clientY > center) {
if (isFolder && (item.getAttribute("active") === "true")) {
return "inside";
} else {
return "after";
}
} else {
return "before";
}
}
};
const applyDragDrop = (dropItem, draggedLayer, position) => {
const oldParent = draggedLayer.parentNode;
const loading = draggedLayer.querySelector(`.item-svg`).style.display == "flex";
// hide the loading icon, so the parent folder gets updated
if (loading) this.hideLoadingIcon(draggedLayer);
// move layer to the new parent
if (position == "before") {
dropItem.insertAdjacentElement("beforebegin", draggedLayer);
} else if (position == "inside") {
const table = dropItem.querySelector(".layertree-content-table");
table.insertAdjacentElement("afterend", draggedLayer);
} else {
dropItem.insertAdjacentElement("afterend", draggedLayer);
}
// update parent folder checked/unchecked status
const newParent = draggedLayer.parentNode;
if (newParent != oldParent) {
this.setParent_(oldParent);
this.setParent_(newParent);
}
// show the loading icon, so the new parent folder gets updated
if (loading) this.showLoadingIcon(draggedLayer);
this.fire("layertree_reorder", draggedLayer);
};
const clearDropTimeout = () => {
if (this.dropTimeout) clearTimeout(this.dropTimeout);
this.dropTimeout = null;
this.dropInside = false;
}
const setDropTimeout = (item) => {
setTimeout(() => {
clearDropTimeout(true);
if (item.getAttribute("role") == "folder") {
this.dropTimeout = setTimeout(() => {
this.dropInside = true;
this.dropTimeout = null;
}, 1000);
}
}, 0);
};
const initEvents = (item) => {
item.setAttribute('draggable', true);
// use the table as a drop target (not the item)
// easier to define the position in folders
const table = item.querySelector(".layertree-content-table");
item.addEventListener('dragstart', (e) => {
e.stopPropagation();
e.dataTransfer.setData("layer_id", item.dataset.id);
e.target.classList.add("drag");
this.draggedLayer_ = e.target;
});
item.addEventListener('dragend', (e) => {
e.stopPropagation();
setTimeout(() => {
clearDropTimeout();
if (!this.draggedLayer_) return;
this.draggedLayer_.classList.remove("drag");
this.draggedLayer_ = null;
}, 0);
});
// dragleave is fired when hovering child elements
// a counter helps to identify when the parent is truly left
let dragEnterCount = 0;
const isLayer = (e) => {
return !!this.draggedLayer_ || e.dataTransfer.types.includes("layer_transfer_id");
};
table.addEventListener('dragenter', e => {
e.preventDefault();
if (!isLayer(e)) return;
if (dragEnterCount++ == 0) setDropTimeout(item);
});
table.addEventListener('dragleave', e => {
e.preventDefault();
if (!isLayer(e)) return;
if (--dragEnterCount == 0) {
clearDropTimeout();
setDropMarker(item);
}
});
table.addEventListener('dragover', e => {
e.dataTransfer.dropEffect = "move";
e.preventDefault();
if (!isLayer(e)) return;
if (!this.draggedLayer_ || !this.draggedLayer_.contains(item)) {
setDropMarker(item, getDropPosition(table, e));
}
});
table.addEventListener('drop', e => {
e.preventDefault();
e.stopPropagation();
if (!isLayer(e)) return;
dragEnterCount = 0;
setDropMarker(item);
if (this.draggedLayer_ && (item != this.draggedLayer_) && !this.draggedLayer_.contains(item)) {
applyDragDrop(item, this.draggedLayer_, getDropPosition(table, e));
this.setLayerIndex_();
} else {
const transferId = e.dataTransfer.getData("layer_transfer_id");
if (!transferId) return;
const layer = retrieveDataTransfer(transferId);
if (!layer) return;
const layerItem = this.addLayer("#", layer);
applyDragDrop(item, layerItem.element, getDropPosition(table, e));
this.setLayerIndex_();
}
})
}
if (targetElement) {
// init drag drop only for one element
initEvents(targetElement);
} else {
// init drag drop for all elements
for (let id in this.items_) {
const item = this.items_[id];
if (!(item.layer instanceof FilterLayer)) initEvents(item.element);
}
}
}
/**
* Checks if an entry already exists in layerTree
* returns true if yes
*
* @param {String} id
*/
hasEntry(id) {
let items = this.getItems();
for (let i = 0; i < items.length; i++) {
if (items[i].id == id) {
return true;
}
}
return false;
}
/**
* Method to edit Layer and Folder names
*
* @param {HTMLElement} span
*/
editTitle(span, item) {
let that = this;
let triggered = false;
if (span && span.tagName.toUpperCase() === "SPAN") {
// hide the span
span.style.display = "none";
// get the title clicked on
let title = span.innerText;
// create input field
let input = document.createElement("input");
input.draggable = true;
input.type = "text";
input.value = title;
input.style = "width: 100%;"
span.parentNode.insertBefore(input, span);
// focus on input and attach events for blur and pressing enter key
input.focus();
input.addEventListener("blur", () => {
if (!triggered) {
triggered = true;
restoreSpan();
}
});
input.addEventListener("keydown", (e) => {
if (e.key == "Enter") {
if (!triggered) {
triggered = true;
restoreSpan();
}
}
});
input.addEventListener("click", (e) => {
e.stopPropagation();
});
input.addEventListener("dragstart", (e) => {
e.preventDefault();
e.stopPropagation();
});
// restore the span with the new title
function restoreSpan() {
// remove input
span.parentNode.removeChild(input);
// update span and layer
const value = input.value.trim();
item.layer.title = (value.length) ? value : title
span.innerText = item.layer.title;
span.title = item.layer.title;
// show the span again
span.style.display = "";
that.fire("layertree_rename_layer", item.layer);
}
}
}
getFolderTitle(elem) {
return elem.querySelector(".td-item-title > span").innerHTML;
}
}