styles.js

/*********************************************************************************************************************  

PRSM Participatory System Mapper 

	Copyright (C) 2022  Nigel Gilbert prsm@prsm.uk

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <https://www.gnu.org/licenses/>.


This modules handles operations related to the Styles tabs.  
 ******************************************************************************************************************** */

import {Network} from 'vis-network/peer/'
import {DataSet} from 'vis-data/peer'
import {
	listen,
	elem,
	deepMerge,
	deepCopy,
	standardize_color,
	setNodeHidden,
	setEdgeHidden,
	dragElement,
	statusMsg,
	clearStatusBar,
	factorSizeToPercent,
	setFactorSizeFromPercent,
	convertDashes,
	getDashes,
} from './utils.js'
import {
	network,
	data,
	ySamplesMap,
	yNetMap,
	selectFactors,
	selectLinks,
	updateLastSamples,
	cp,
	logHistory,
	progressBar,
} from './prsm.js'
import {styles} from './samples.js'

/**
 * The samples are each a mini vis-network showing just one node or two nodes and a link
 */
export function setUpSamples() {
	// create sample configurations
	configSamples()
	// Get all elements with class='sampleNode' and add listener and canvas
	let emptyDataSet = new DataSet([])
	let sampleElements = document.getElementsByClassName('sampleNode')
	for (let i = 0; i < sampleElements.length; i++) {
		let groupId = `group${i}`
		let sampleElement = sampleElements[i]
		let sampleOptions = styles.nodes[groupId]
		let groupLabel = styles.nodes[groupId].groupLabel
		let nodeDataSet = new DataSet([
			deepMerge(
				{
					id: '1',
					label: groupLabel === undefined ? '' : groupLabel,
				},
				sampleOptions,
				{
					chosen: false,
					widthConstraint: 50,
					heightConstraint: 50,
					margin: 10,
					scaling: {label: {enabled: false}},
				}
			),
		])
		initSample(sampleElement, {
			nodes: nodeDataSet,
			edges: emptyDataSet,
		})
		sampleElement.addEventListener('dblclick', () => {
			editNodeStyle(sampleElement, groupId)
		})
		sampleElement.addEventListener('click', () => {
			updateLastSamples(groupId, null)
		})
		sampleElement.addEventListener('contextmenu', (event) => {
			styleNodeContextMenu(event, sampleElement, groupId)
		})
		sampleElement.addEventListener('mouseover', () =>
			statusMsg(
				'Left click: apply style to selected; Double click: edit style; Right click: Select or Hide all with this style'
			)
		)
		sampleElement.addEventListener('mouseout', () => clearStatusBar())
		sampleElement.groupNode = groupId
		sampleElement.dataSet = nodeDataSet
	}
	// and to all sampleLinks
	sampleElements = document.getElementsByClassName('sampleLink')
	for (let i = 0; i < sampleElements.length; i++) {
		let sampleElement = sampleElements[i]
		let groupId = `edge${i}`
		let groupLabel = styles.edges[groupId].groupLabel
		let sampleOptions = styles.edges[groupId]
		let edgeDataSet = new DataSet([
			deepMerge(
				{
					id: '1',
					from: 1,
					to: 2,
					label: groupLabel === undefined ? '' : groupLabel,
				},
				sampleOptions,
				{
					font: {
						size: 24,
						color: 'black',
						align: 'top',
						vadjust: -40,
					},
					widthConstraint: 100,
				}
			),
		])
		let nodesDataSet = new DataSet([
			{
				id: 1,
				size: 5,
				shape: 'dot',
				fixed: true,
				chosen: false,
			},
			{
				id: 2,
				size: 5,
				shape: 'dot',
				fixed: true,
				chosen: false,
			},
		])
		initSample(sampleElement, {
			nodes: nodesDataSet,
			edges: edgeDataSet,
		})
		sampleElement.addEventListener('dblclick', () => {
			editLinkStyle(sampleElement, groupId)
		})
		sampleElement.addEventListener('click', () => {
			updateLastSamples(null, groupId)
		})
		sampleElement.addEventListener('contextmenu', (event) => {
			styleEdgeContextMenu(event, sampleElement, groupId)
		})
		sampleElement.addEventListener('mouseover', () =>
			statusMsg(
				'Left click: apply style to selected; Double click: edit style; Right click: Select all with this style'
			)
		)
		sampleElement.groupLink = groupId
		sampleElement.dataSet = edgeDataSet
	}
	// set up color pickers
	cp.createColorPicker('nodeStyleEditFillColor', nodeEditSave)
	cp.createColorPicker('nodeStyleEditBorderColor', nodeEditSave)
	cp.createColorPicker('nodeStyleEditFontColor', nodeEditSave)
	cp.createColorPicker('linkStyleEditLineColor', linkEditSave)

	// set up listeners
	listen('nodeStyleEditCancel', 'click', nodeEditCancel)
	listen('nodeStyleEditName', 'keyup', nodeEditSave)
	listen('nodeStyleEditShape', 'change', nodeEditSave)
	listen('nodeStyleEditBorder', 'change', nodeEditSave)
	listen('nodeStyleEditFontSize', 'change', nodeEditSave)
	listen('nodeStyleEditSubmit', 'click', nodeEditSubmit)

	listen('linkStyleEditCancel', 'click', linkEditCancel)
	listen('linkStyleEditName', 'keyup', linkEditSave)
	listen('linkStyleEditWidth', 'input', linkEditSave)
	listen('linkStyleEditDashes', 'input', linkEditSave)
	listen('linkStyleEditArrows', 'change', linkEditSave)
	listen('linkStyleEditSubmit', 'click', linkEditSubmit)
	listen('styleNodeContextMenuHide', 'contextmenu', (e) => e.preventDefault())
	listen('styleNodeContextMenuSelect', 'contextmenu', (e) => e.preventDefault())
	listen('styleEdgeContextMenuSelect', 'contextmenu', (e) => e.preventDefault())
}

var factorsHiddenByStyle = {}
var linksHiddenByStyle = {}
listen('nodesTab', 'contextmenu', (e) => {
	e.preventDefault()
})
listen('linksTab', 'contextmenu', (e) => {
	e.preventDefault()
})

function styleNodeContextMenu(event, sampleElement, groupId) {
	let menu = elem('styleNodeContextMenu')
	showMenu(event.pageX, event.pageY)
	document.addEventListener('click', onClick, false)

	function onClick(event) {
		// Safari emits a contextmenu and a click event on control-click; ignore the click
		if (event.ctrlKey && !event.target.id) return
		event.preventDefault()
		hideMenu()
		document.removeEventListener('click', onClick)
		switch (event.target.id) {
			case 'styleNodeContextMenuSelect': {
				selectFactorsWithStyle(groupId)
				break
			}
			case 'styleNodeContextMenuHide': {
				if (sampleElement.dataset.hide !== 'hidden') {
					hideFactorsWithStyle(groupId, true)
					sampleElement.dataset.hide = 'hidden'
					sampleElement.style.opacity = 0.6
				} else {
					hideFactorsWithStyle(groupId, false)
					sampleElement.dataset.hide = 'visible'
					sampleElement.style.opacity = 1.0
				}
				break
			}
			default: // clicked off menu
				break
		}
	}
	function showMenu(x, y) {
		elem('styleNodeContextMenuHide').innerText =
			sampleElement.dataset.hide === 'hidden' ? 'Unhide Factors' : 'Hide Factors'
		if (x + menu.offsetWidth > elem('container').offsetWidth) x = elem('container').offsetWidth - menu.offsetWidth
		if (y + menu.offsetHeight > elem('container').offsetHeight)
			y = elem('container').offsetHeighth - menu.offsetHeight
		menu.style.left = `${x}px`
		menu.style.top = `${y}px`
		menu.classList.add('show-menu')
	}
	function hideMenu() {
		menu.classList.remove('show-menu')
	}
	function selectFactorsWithStyle(groupId) {
		selectFactors(data.nodes.getIds({filter: (node) => node.grp === groupId}))
	}
	function hideFactorsWithStyle(groupId, toggle) {
		let nodes = data.nodes.get({filter: (node) => node.grp === groupId})
		nodes.forEach((node) => {
			setNodeHidden(node, toggle)
		})
		data.nodes.update(nodes)
		let edges = []
		nodes.forEach((node) => {
			let connectedEdges = network.getConnectedEdges(node.id)
			connectedEdges.forEach((edgeId) => {
				edges.push(data.edges.get(edgeId))
			})
		})
		edges.forEach((edge) => {
			setEdgeHidden(edge, toggle)
		})
		data.edges.update(edges)
		factorsHiddenByStyle[sampleElement.id] = toggle
		yNetMap.set('factorsHiddenByStyle', factorsHiddenByStyle)
	}
}

function styleEdgeContextMenu(event, sampleElement, groupId) {
	let menu = elem('styleEdgeContextMenu')
	showMenu(event.pageX, event.pageY)
	document.addEventListener('click', onClick, false)

	function onClick(event) {
		// Safari emits a contextmenu and a click event on control-click; ignore the click
		if (event.ctrlKey && !event.target.id) return
		event.preventDefault()
		hideMenu()
		document.removeEventListener('click', onClick)
		switch (event.target.id) {
			case 'styleEdgeContextMenuSelect': {
				selectLinksWithStyle(groupId)
				break
			}
			case 'styleEdgeContextMenuHide': {
				if (sampleElement.dataset.hide !== 'hidden') {
					hideLinksWithStyle(groupId, true)
					sampleElement.dataset.hide = 'hidden'
					sampleElement.style.opacity = 0.6
				} else {
					hideLinksWithStyle(groupId, false)
					sampleElement.dataset.hide = 'visible'
					sampleElement.style.opacity = 1.0
				}
				break
			}
			default: // clicked off menu
				break
		}
	}
	function showMenu(x, y) {
		elem('styleEdgeContextMenuHide').innerText =
			sampleElement.dataset.hide === 'hidden' ? 'Unhide Links' : 'Hide Links'
		if (x + menu.offsetWidth > elem('container').offsetWidth) x = elem('container').offsetWidth - menu.offsetWidth
		if (y + menu.offsetHeight > elem('container').offsetHeight)
			y = elem('container').offsetHeighth - menu.offsetHeight
		menu.style.left = `${x}px`
		menu.style.top = `${y}px`
		menu.classList.add('show-menu')
	}
	function hideMenu() {
		menu.classList.remove('show-menu')
	}
	function selectLinksWithStyle(groupId) {
		selectLinks(data.edges.getIds({filter: (edge) => edge.grp === groupId}))
	}
	function hideLinksWithStyle(groupId, toggle) {
		let edges = data.edges.get({filter: (edge) => edge.grp === groupId})
		edges.forEach((edge) => {
			setEdgeHidden(edge, toggle)
		})
		data.edges.update(edges)
		linksHiddenByStyle[sampleElement.id] = toggle
		yNetMap.set('linksHiddenByStyle', linksHiddenByStyle)
	}
}

/**
 * assemble configurations by merging the specifics into the default
 */
function configSamples() {
	let base = styles.nodes.base
	for (let prop in styles.nodes) {
		let grp = deepMerge(base, styles.nodes[prop])
		// make the hover and highlight colors the same as the basic ones
		grp.color.highlight = {}
		grp.color.highlight.border = grp.color.border
		grp.color.highlight.background = grp.color.background
		grp.color.hover = {}
		grp.color.hover.border = grp.color.border
		grp.color.hover.background = grp.color.background
		grp.font.size = base.font.size
		styles.nodes[prop] = grp
	}
	base = styles.edges.base
	for (let prop in styles.edges) {
		let grp = deepMerge(base, styles.edges[prop])
		grp.color.highlight = grp.color.color
		grp.color.hover = grp.color.color
		styles.edges[prop] = grp
	}
}
/**
 * create the sample network
 * @param {HTMLElement} wrapper
 * @param {object} sampleData
 */
function initSample(wrapper, sampleData) {
	let options = {
		interaction: {
			dragNodes: false,
			dragView: false,
			selectable: true,
			zoomView: false,
		},
		manipulation: {
			enabled: false,
		},
		layout: {
			hierarchical: {
				enabled: true,
				direction: 'LR',
			},
		},
		nodes: {
			widthConstraint: 50,
			heightConstraint: 50,
		},
		edges: {
			value: 10, // to make the links more visible at very small scale for the samples
		},
	}
	let net = new Network(wrapper, sampleData, options)
	net.fit()
	net.storePositions()
	wrapper.net = net
	return net
}

/**
 * open the dialog to edit a node style
 * @param {HTMLElement} styleElement
 * @param {number} groupId
 */
function editNodeStyle(styleElement, groupId) {
	styleElement.net.unselectAll()
	let container = elem('nodeStyleEditorContainer')
	container.styleElement = styleElement
	container.groupId = groupId
	// save the current style format (so that there can be a revert in case of cancelling the edit)
	container.origGroup = deepCopy(styles.nodes[groupId])
	// save the div in which the style is displayed
	container.styleElement = styleElement
	// set the style dialog widgets with the current values for the group style
	updateNodeEditor(groupId)
	// display the style dialog
	nodeEditorShow()
}
/**
 * ensure that the edit node style dialog shows the current state of the style
 * @param {string} groupId
 */
function updateNodeEditor(groupId) {
	let group = styles.nodes[groupId]
	elem('nodeStyleEditName').value = group.groupLabel !== 'Sample' ? group.groupLabel : ''
	elem('nodeStyleEditFillColor').style.backgroundColor = standardize_color(group.color.background)
	elem('nodeStyleEditBorderColor').style.backgroundColor = standardize_color(group.color.border)
	elem('nodeStyleEditFontColor').style.backgroundColor = standardize_color(group.font.color)
	elem('nodeStyleEditShape').value = group.shape
	elem('nodeStyleEditBorder').value = getDashes(group.shapeProperties.borderDashes, group.borderWidth)
	elem('nodeStyleEditFontSize').value = group.font.size
	if (group.fixed) {
		elem('nodeStyleEditFixed').style.display = 'inline'
		elem('nodeStyleEditUnfixed').style.display = 'none'
	} else {
		elem('nodeStyleEditFixed').style.display = 'none'
		elem('nodeStyleEditUnfixed').style.display = 'inline'
	}
	elem('nodeStyleEditFactorSize').value = factorSizeToPercent(group.size)
	progressBar(elem('nodeStyleEditFactorSize'))
}
listen('nodeStyleEditLock', 'click', toggleNodeStyleLock)

function toggleNodeStyleLock() {
	let group = styles.nodes[elem('nodeStyleEditorContainer').groupId]
	if (group.fixed) {
		elem('nodeStyleEditFixed').style.display = 'none'
		elem('nodeStyleEditUnfixed').style.display = 'inline'
	} else {
		elem('nodeStyleEditFixed').style.display = 'inline'
		elem('nodeStyleEditUnfixed').style.display = 'none'
	}
	group.fixed = !group.fixed
}
/**
 * save changes to the style made with the edit dialog to the style object
 */
function nodeEditSave() {
	let groupId = elem('nodeStyleEditorContainer').groupId
	let group = styles.nodes[groupId]
	group.groupLabel = elem('nodeStyleEditName').value
	if (group.groupLabel === '') group.groupLabel = 'Sample'
	group.color.background = elem('nodeStyleEditFillColor').style.backgroundColor
	group.color.border = elem('nodeStyleEditBorderColor').style.backgroundColor
	group.color.highlight.background = group.color.background
	group.color.highlight.border = group.color.border
	group.color.hover.background = group.color.background
	group.color.hover.border = group.color.border
	group.font.color = elem('nodeStyleEditFontColor').style.backgroundColor
	group.shape = elem('nodeStyleEditShape').value
	let border = elem('nodeStyleEditBorder').value
	group.shapeProperties.borderDashes = convertDashes(border)
	group.borderWidth = border === 'none' ? 0 : 4
	group.font.size = parseInt(elem('nodeStyleEditFontSize').value)
	setFactorSizeFromPercent(group, elem('nodeStyleEditFactorSize').value)
	nodeEditUpdateStyleSample(group)
}
/**
 * update the style sample to show changes to style
 * @param {Object} group
 */
function nodeEditUpdateStyleSample(group) {
	let groupId = elem('nodeStyleEditorContainer').groupId
	let styleElement = elem('nodeStyleEditorContainer').styleElement
	let node = styleElement.dataSet.get('1')
	node.label = group.groupLabel
	// the node in the style sample does not change size
	node = deepMerge(node, styles.nodes[groupId], {chosen: false, size: 25, widthConstraint: 25, heightConstraint: 25})
	styleElement.dataSet.update(node)
}
/**
 * cancel any editing of the style and revert to what it was previously
 */
function nodeEditCancel() {
	// restore saved group format
	let groupId = elem('nodeStyleEditorContainer').groupId
	styles.nodes[groupId] = elem('nodeStyleEditorContainer').origGroup
	nodeEditUpdateStyleSample(styles.nodes[groupId])
	nodeEditorHide()
}
/**
 * hide the node style editor dialog
 */
function nodeEditorHide() {
	elem('nodeStyleEditorContainer').classList.add('hideEditor')
}
/**
 * show the node style editor dialog
 */
function nodeEditorShow() {
	let panelRect = elem('panel').getBoundingClientRect()
	let container = elem('nodeStyleEditorContainer')
	container.style.top = `${panelRect.top}px`
	container.style.left = `${panelRect.left - 300}px`
	container.classList.remove('hideEditor')
}
/**
 * save the edited style and close the style editor.  Update the nodes
 * in the map and the legend to the current style
 */
function nodeEditSubmit() {
	nodeEditSave()
	nodeEditorHide()
	// apply updated style to all nodes having that style
	let groupId = elem('nodeStyleEditorContainer').groupId
	// somewhere - but I have no idea where or why, this is set to true, but it must be false
	styles.nodes[groupId].scaling.label.enabled = false
	reApplySampleToNodes([groupId], true)
	ySamplesMap.set(groupId, {
		node: styles.nodes[groupId],
	})
	updateLegend()
	network.redraw()
	logHistory('edited a Factor style')
}
/**
 * update all nodes in the map with this style to the current style features
 * @param {number[]} groupIds
 * @param {boolean} [force] override any existing individual node styling
 */
export function reApplySampleToNodes(groupIds, force) {
	let nodesToUpdate = data.nodes.get({
		filter: (item) => {
			return groupIds.includes(item.grp)
		},
	})
	data.nodes.update(
		nodesToUpdate.map((node) => {
			return force ? deepMerge(node, styles.nodes[node.grp]) : deepMerge(styles.nodes[node.grp], node)
		})
	)
}

/**
 * open the dialog to edit a link style
 * @param {HTMLElement} styleElement
 * @param {string} groupId
 */
function editLinkStyle(styleElement, groupId) {
	let container = elem('linkStyleEditorContainer')
	container.styleElement = styleElement
	container.groupId = groupId
	// save the current style format (so that there can be a revert in case of cancelling the edit)
	container.origGroup = deepCopy(styles.edges[groupId])
	// set the style dialog widgets with the current values for the group style
	updateLinkEditor(groupId)
	// display the style dialog
	linkEditorShow()
}
/**
 * ensure that the edit link style dialog shows the current state of the style
 * @param {string} groupId
 */
function updateLinkEditor(groupId) {
	let group = styles.edges[groupId]
	elem('linkStyleEditName').value = group.groupLabel !== 'Sample' ? group.groupLabel : ''
	elem('linkStyleEditLineColor').style.backgroundColor = standardize_color(group.color.color)
	elem('linkStyleEditWidth').value = group.width
	elem('linkStyleEditDashes').value = getDashes(group.dashes, 1)
	elem('linkStyleEditArrows').value = getArrows(group.arrows)
}
/**
 * save changes to the style made with the edit dialog to the style object
 */
function linkEditSave() {
	let groupId = elem('linkStyleEditorContainer').groupId
	let group = styles.edges[groupId]
	group.groupLabel = elem('linkStyleEditName').value
	if (group.groupLabel === '') group.groupLabel = 'Sample'
	group.color.color = elem('linkStyleEditLineColor').style.backgroundColor
	group.color.highlight = group.color.color
	group.color.hover = group.color.color
	group.width = parseInt(elem('linkStyleEditWidth').value)
	group.dashes = convertDashes(elem('linkStyleEditDashes').value)
	groupArrows(elem('linkStyleEditArrows').value)
	linkEditUpdateStyleSample(group)
	/**
	 * Set the link object properties to show various arrow types
	 * @param {string} val
	 */
	function groupArrows(val) {
		if (val !== '') {
			group.arrows.from.enabled = false
			group.arrows.middle.enabled = false
			group.arrows.to.enabled = true
			if (val === 'none') group.arrows.to.enabled = false
			else group.arrows.to.type = val
		}
	}
}
/**
 * update the style sample to show changes to style
 * @param {Object} group
 */
function linkEditUpdateStyleSample(group) {
	// update the style edge to show changes to style
	let groupId = elem('linkStyleEditorContainer').groupId
	let styleElement = elem('linkStyleEditorContainer').styleElement
	let edge = styleElement.dataSet.get('1')
	edge.label = group.groupLabel
	edge = deepMerge(edge, styles.edges[groupId], {chosen: false})
	let dataSet = styleElement.dataSet
	dataSet.update(edge)
}
/**
 * cancel any editing of the style and revert to what it was previously
 */
function linkEditCancel() {
	// restore saved group format
	let groupId = elem('linkStyleEditorContainer').groupId
	styles.edges[groupId] = elem('linkStyleEditorContainer').origGroup
	linkEditorHide()
}
/**
 * hide the link style editor dialog
 */
function linkEditorHide() {
	elem('linkStyleEditorContainer').classList.add('hideEditor')
}
/**
 * show the link style editor dialog
 */
function linkEditorShow() {
	let panelRect = elem('panel').getBoundingClientRect()
	let container = elem('linkStyleEditorContainer')
	container.style.top = `${panelRect.top}px`
	container.style.left = `${panelRect.left - 300}px`
	container.classList.remove('hideEditor')
}
/**
 * save the edited style and close the style editor.  Update the links
 * in the map and the legend to the current style
 */
function linkEditSubmit() {
	linkEditSave()
	linkEditorHide()
	// apply updated style to all edges having that style
	let groupId = elem('linkStyleEditorContainer').groupId
	reApplySampleToLinks([groupId], true)
	ySamplesMap.set(groupId, {
		edge: styles.edges[groupId],
	})
	updateLegend()
	network.redraw()
	logHistory('edited a Link style')
}
/**
 * update all links in the map with this style to the current style features
 * @param {number[]} groupIds
 * @param {boolean} [force] override any existing individual edge styling
 */
export function reApplySampleToLinks(groupIds, force) {
	let edgesToUpdate = data.edges.get({
		filter: (item) => {
			return groupIds.includes(item.grp)
		},
	})
	data.edges.update(
		edgesToUpdate.map((edge) => {
			return force ? deepMerge(edge, styles.edges[edge.grp]) : deepMerge(styles.edges[edge.grp], edge)
		})
	)
}
/**
 * Convert from style object properties to arrow menu selection
 * @param {Object} prop
 */
function getArrows(prop) {
	let val = 'none'
	if (prop.to?.enabled && prop.to.type) val = prop.to.type
	return val
}

/*  ------------display the map legend (includes all styles with a group label that is neither blank or 'Sample') */

var legendData = {nodes: new DataSet(), edges: new DataSet()}
var legendNetwork = null
const LEGENDSPACING = 60
const HALFLEGENDWIDTH = 60
/**
 * display a legend on the map (but only if the styles have been given names)
 * @param {Boolean} warn true if user is switching display legend on, but there is nothing to show
 */
export function legend(warn = false) {
	clearLegend()

	let sampleNodeDivs = document.getElementsByClassName('sampleNode')
	let nodes = Array.from(sampleNodeDivs).filter((elem) => elem.dataSet.get('1').groupLabel !== 'Sample')
	let sampleEdgeDivs = document.getElementsByClassName('sampleLink')
	let edges = Array.from(sampleEdgeDivs).filter(
		(elem) => !['Sample', '', ' '].includes(elem.dataSet.get('1').groupLabel)
	)
	let nItems = nodes.length + edges.length
	if (nItems === 0) {
		if (warn) statusMsg('Nothing to include in the Legend - rename some styles first', 'warn')
		elem('showLegendSwitch').checked = false
		return
	}
	let legendBox = document.createElement('div')
	legendBox.className = 'legend'
	legendBox.id = 'legendBox'
	elem('main').appendChild(legendBox)
	let title = document.createElement('p')
	title.id = 'Legend'
	title.className = 'legendTitle'
	title.appendChild(document.createTextNode('Legend'))
	legendBox.appendChild(title)
	legendBox.style.height = `${LEGENDSPACING * nItems + title.offsetHeight}px`
	legendBox.style.width = `${HALFLEGENDWIDTH * 2}px`
	let canvas = document.createElement('div')
	canvas.className = 'legendCanvas'
	canvas.style.height = `${LEGENDSPACING * nItems}px`
	legendBox.appendChild(canvas)

	dragElement(legendBox, title)

	legendNetwork = new Network(canvas, legendData, {
		physics: {enabled: false},
		interaction: {zoomView: false, dragView: false},
	})
	let height = legendNetwork.DOMtoCanvas({x: 0, y: 0}).y
	for (let i = 0; i < nodes.length; i++) {
		let node = deepMerge(styles.nodes[nodes[i].groupNode])
		node.id = i + 10000
		node.label = node.groupLabel
		node.fixed = true
		node.chosen = false
		node.margin = 10
		node.x = 0
		node.y = 0
		node.widthConstraint = 40
		node.heightConstraint = 40
		node.font.size = 10
		legendData.nodes.update(node)
		let bbox = legendNetwork.getBoundingBox(node.id)
		node.y = (bbox.bottom - bbox.top) / 2 + height
		height += bbox.bottom - bbox.top
		legendData.nodes.update(node)
	}
	height += 50
	for (let i = 0; i < edges.length; i++) {
		let edge = deepMerge(styles.edges[edges[i].groupLink])
		edge.label = edge.groupLabel
		edge.id = i + 10000
		edge.from = i + 20000
		edge.to = i + 30000
		edge.smooth = {type: 'straightCross'}
		edge.font = {size: 12, color: 'black', align: 'top', vadjust: -10}
		edge.widthConstraint = 80
		edge.chosen = false
		let nodes = [
			{
				id: edge.from,
				size: 5,
				shape: 'dot',
				x: -25,
				y: height,
				fixed: true,
				chosen: false,
			},
			{
				id: edge.to,
				shape: 'dot',
				size: 5,
				x: +25,
				y: height,
				fixed: true,
				chosen: false,
			},
		]
		legendData.nodes.update(nodes)
		legendData.edges.update(edge)
		height += 50
	}
	legendNetwork.fit({})
}
window.legendData = legendData
/**
 * remove the legend from the map
 */
export function clearLegend() {
	legendData.nodes.clear()
	legendData.edges.clear()
	if (legendNetwork) legendNetwork.destroy()
	let legendBox = elem('legendBox')
	if (legendBox) legendBox.remove()
}
/**
 * redraw the legend (to show updated styles)
 */
export function updateLegend() {
	if (elem('showLegendSwitch').checked) {
		legend(false)
		clearStatusBar()
	}
}