Source: EditorSvgAnnotation.js

import { Skin } from './Skin.js';
import { Util } from './Util.js';
import { simplify, smooth, smoothToPath } from './Simplify.js'
import { LayerSvgAnnotation } from './LayerSvgAnnotation.js'
import { CoordinateSystem } from './CoordinateSystem.js'

/**
 * @typedef {Object} AnnotationObj
 * @property {string} id - Unique identifier
 * @property {string} label - Annotation title
 * @property {string} description - Detailed description
 * @property {string} class - Class name for styling/categorization
 * @property {number} publish - Publication status (0 or 1)
 * @property {Object} data - Custom data object
 * @property {Array<SVGElement>} elements - SVG elements composing the annotation
 * @property {Object} [state] - Camera and viewer state
 */

/**
 * @callback crudCallback
 * @param {AnnotationObj} annotation - The annotation being operated on
 * @returns {boolean} Success status of the operation
 * @description Callback for create/update/delete operations on annotations
 */

/**
 * @callback customStateCallback
 * @param {AnnotationObj} annotation - The annotation being modified
 * @description Callback to customize state information saved with annotations
 */

/**
 * @callback customDataCallback
 * @param {AnnotationObj} annotation - The annotation being modified
 * @description Callback to customize the annotation data object
 */

/**
 * @callback selectedCallback
 * @param {AnnotationObj} annotation - The selected annotation
 * @description Callback executed when an annotation is selected in the UI
 */

/**
 * EditorSvgAnnotation enables creation and editing of SVG annotations in OpenLIME.
 * It provides tools for drawing various shapes and managing annotations through a user interface.
 * 
 * Features:
 * - Drawing tools: point, pin, line, box, circle
 * - Annotation editing and management
 * - Custom state and data storage
 * - Integration with annotation databases through callbacks
 * - Undo/redo functionality
 * - SVG export capabilities
 * 
 * @example
 * ```javascript
 * // Create annotation layer
 * const anno = new OpenLIME.Layer(options);
 * viewer.addLayer('annotations', anno);
 * 
 * // Initialize editor
 * const editor = new OpenLIME.EditorSvgAnnotation(viewer, anno, {
 *   classes: {
 *     'default': { stroke: '#000', label: 'Default' },
 *     'highlight': { stroke: '#ff0', label: 'Highlight' }
 *   }
 * });
 * 
 * // Setup callbacks
 * editor.createCallback = (anno) => { 
 *   console.log("Created:", anno);
 *   return saveToDatabase(anno);
 * };
 * ```
 */
class EditorSvgAnnotation {
	/**
	 * Creates an EditorSvgAnnotation instance
	 * @param {Viewer} viewer - The OpenLIME viewer instance
	 * @param {LayerSvgAnnotation} layer - The annotation layer to edit
	 * @param {Object} [options] - Configuration options
	 * @param {Object.<string, {stroke: string, label: string}>} options.classes - Annotation classes with colors and labels
	 * @param {crudCallback} [options.createCallback] - Called when creating annotations
	 * @param {crudCallback} [options.updateCallback] - Called when updating annotations
	 * @param {crudCallback} [options.deleteCallback] - Called when deleting annotations
	 * @param {boolean} [options.enableState=false] - Whether to save viewer state with annotations
	 * @param {customStateCallback} [options.customState] - Customize saved state data
	 * @param {customDataCallback} [options.customData] - Customize annotation data
	 * @param {selectedCallback} [options.selectedCallback] - Called when annotation is selected
	 * @param {Object} [options.tools] - Custom tool configurations
	 * @param {number} [options.priority=20000] - Event handling priority
	 */
	constructor(viewer, layer, options) {
		this.layer = layer;
		Object.assign(this, {
			viewer: viewer,
			panning: false,
			tool: null, //doing nothing, could: ['line', 'polygon', 'point', 'box', 'circle']
			startPoint: null, //starting point for box and  circle
			currentLine: [],
			annotation: null,
			priority: 20000,
			classes: {
				'': { stroke: '#000', label: '' },
				'class1': { stroke: '#770', label: '' },
				'class2': { stroke: '#707', label: '' },
				'class3': { stroke: '#777', label: '' },
				'class4': { stroke: '#070', label: '' },
				'class5': { stroke: '#007', label: '' },
				'class6': { stroke: '#077', label: '' },
			},
			tools: {
				point: {
					img: '<svg width=24 height=24><circle cx=12 cy=12 r=3 fill="red" stroke="gray"/></svg>',
					tooltip: 'New point',
					tool: Point,
				},
				pin: {
					template: (x, y) => {
						return `<svg xmlns='http://www.w3.org/2000/svg' x='${x}' y='${y}' width='4%' height='4%' class='pin'
						viewBox='0 0 18 18'><path d='M 0,0 C 0,0 4,0 8,0 12,0 16,4 16,8 16,12 12,16 8,16 4,16 0,12 0,8 0,4 0,0 0,0 Z'/><text class='pin-text' x='7' y='8'>${this.annotation.idx}</text></svg>`;
					}, //pin di alcazar  1. url a svg 2. txt (stringa con svg) 3. funzione(x,y) ritorna svg 4. dom (da skin).
					tooltip: 'New pin',
					tool: Pin
				},
				pen: {
					img: '<svg width=24 height=24><circle cx=12 cy=12 r=3 fill="red" stroke="gray"/></svg>',
					tooltip: 'New polyline',
					tool: Pen,
				},
				line: {
					img: `<svg width=24 height=24>
						<path d="m 4.7,4.5 c 0.5,4.8 0.8,8.5 3.1,11 2.4,2.6 4.2,-4.8 6.3,-5 2.7,-0.3 5.1,9.3 5.1,9.3" stroke-width="3" fill="none" stroke="grey"/>
						<path d="m 4.7,4.5 c 0.5,4.8 0.8,8.5 3.1,11 2.4,2.6 4.2,-4.8 6.3,-5 2.7,-0.3 5.1,9.3 5.1,9.3" stroke-width="1" fill="none" stroke="red"/></svg>`,
					tooltip: 'New line',
					tool: Line,
				},
				erase: {
					img: '',
					tooltip: 'Erase lines',
					tool: Erase,
				},
				box: {
					img: '<svg width=24 height=24><rect x=5 y=5 width=14 height=14 fill="red" stroke="gray"/></svg>',
					tooltip: 'New box',
					tool: Box,
				},
				circle: {
					img: '<svg width=24 height=24><circle cx=12 cy=12 r=7 fill="red" stroke="gray"/></svg>',
					tooltip: 'New circle',
					tool: Circle,
				},
				/*				colorpick: {
									img: '',
									tooltip: 'Pick a color',
									tool: Colorpick,
								} */
			},
			annotation: null, //not null only when editWidget is shown.
			enableState: false,
			customState: null,
			customData: null,
			editWidget: null,
			selectedCallback: null,
			createCallback: null, //callbacks for backend
			updateCallback: null,
			deleteCallback: null
		}, options);

		layer.style += Object.entries(this.classes).map((g) => {
			console.assert(g[1].hasOwnProperty('stroke'), "Classes needs a stroke property");
			return `[data-class=${g[0]}] { stroke:${g[1].stroke}; }`;
		}).join('\n');
		//at the moment is not really possible to unregister the events registered here.
		viewer.pointerManager.onEvent(this);
		document.addEventListener('keyup', (e) => this.keyUp(e), false);
		layer.addEvent('selected', (anno) => {
			if (!anno || anno == this.annotation)
				return;
			if (this.selectedCallback) this.selectedCallback(anno);
			this.showEditWidget(anno);
		});

		layer.annotationsEntry = () => {

			let entry = {
				html: `<div class="openlime-tools"></div>`,
				list: [], //will be filled later.
				classes: 'openlime-annotations',
				status: () => 'active',
				oncreate: () => {
					if (Array.isArray(layer.annotations))
						layer.createAnnotationsList();

					let tools = {
						'add': { action: () => { this.createAnnotation(); }, title: "New annotation" },
						'edit': { action: () => { this.toggleEditWidget(); }, title: "Edit annotations" },
						'export': { action: () => { this.exportAnnotations(); }, title: "Export annotations" },
						'trash': { action: () => { this.deleteSelected(); }, title: "Delete selected annotations" },
					};
					(async () => {

						for (const [label, tool] of Object.entries(tools)) {
							let icon = await Skin.appendIcon(entry.element.firstChild, '.openlime-' + label); // TODO pass entry.element.firstChild as parameter in onCreate
							icon.setAttribute('title', tool.title);
							icon.addEventListener('click', tool.action);
						}
					})();
				}
			}
			layer.annotationsListEntry = entry;
			return entry;
		}
	}

	/**
	 * Creates a new annotation
	 * @returns {void}
	 */
	createAnnotation() {
		let anno = this.layer.newAnnotation();
		if (this.customData) this.customData(anno);
		if (this.enableState) this.setAnnotationCurrentState(anno);
		anno.idx = this.layer.annotations.length;
		anno.publish = 1;
		anno.label = anno.description = anno.class = '';
		let post = {
			id: anno.id, idx: anno.idx, label: anno.label, description: anno.description, 'class': anno.class, svg: null,
			publish: anno.publish, data: anno.data
		};
		if (this.enableState) post = { ...post, state: anno.state };
		if (this.createCallback) {
			let result = this.createCallback(post);
			if (!result)
				alert("Failed to create annotation!");
		}
		this.layer.setSelected(anno);
	}

	/** @ignore */
	toggleEditWidget() {
		if (this.annotation)
			return this.hideEditWidget();

		let id = this.layer.selected.values().next().value;
		if (!id)
			return;

		let anno = this.layer.getAnnotationById(id);
		this.showEditWidget(anno);
	}

	/** @ignore */
	updateEditWidget() {
		let anno = this.annotation;
		let edit = this.editWidget;
		if (!anno.class)
			anno.class = '';
		edit.querySelector('[name=label]').value = anno.label || '';
		edit.querySelector('[name=description]').value = anno.description || '';
		edit.querySelector('[name=idx]').value = anno.idx || '';
		Object.entries(anno.data).map(k => {
			edit.querySelector(`[name=data-data-${k[0]}]`).value = k[1] || '';
		});

		edit.querySelector('[name=classes]').value = anno.class;
		edit.querySelector('[name=publish]').checked = anno.publish == 1;
		edit.classList.remove('hidden');
		let button = edit.querySelector('.openlime-select-button');
		button.textContent = this.classes[anno.class].label;
		button.style.background = this.classes[anno.class].stroke;
	}

	/**
 * Shows the annotation editor widget
 * @param {AnnotationObj} annotation - Annotation to edit
 * @private
 */
	showEditWidget(anno) {
		this.annotation = anno;
		this.setTool(null);
		this.setActiveTool();
		this.layer.annotationsListEntry.element.querySelector('.openlime-edit').classList.add('active');
		(async () => {
			await this.createEditWidget();
			this.updateEditWidget();
		})();
	}

	/** @ignore */
	hideEditWidget() {
		this.annotation = null;
		this.setTool(null);
		this.editWidget.classList.add('hidden');
		this.layer.annotationsListEntry.element.querySelector('.openlime-edit').classList.remove('active');
	}

	//TODO this should actually be in the html.
	/** @ignore */
	async createEditWidget() {
		if (this.editWidget)
			return;
		let html = `
				<div class="openlime-annotation-edit">
					<label for="label">Title:</label> <input name="label" type="text"><br>
					<label for="description">Description:</label><br>
					<textarea name="description" cols="30" rows="5"></textarea><br>
					<span>Class:</span> 
					<div class="openlime-select">
						<input type="hidden" name="classes" value=""/>
						<div class="openlime-select-button"></div>
						<ul class="openlime-select-menu">
						${Object.entries(this.classes).map((c) =>
			`<li data-class="${c[0]}" style="background:${c[1].stroke};">${c[1].label}</li>`).join('\n')}
						</ul>
					</div>
					<label for="idx">Index:</label> <input name="idx" type="text"><br>	
					${Object.entries(this.annotation.data).map(k => {
				let label = k[0];
				let str = `<label for="data-data-${k[0]}">${label}:</label> <input name="data-data-${k[0]}" type="text"><br>`
				return str;
			}).join('\n')}
					<br>
					<span><button class="openlime-state">SAVE</button></span>
					<span><input type="checkbox" name="publish" value=""> Publish</span><br>
					<div class="openlime-annotation-edit-tools"></div>
				</div>`;
		let template = document.createElement('template');
		template.innerHTML = html.trim();
		let edit = template.content.firstChild;

		let select = edit.querySelector('.openlime-select');
		let button = edit.querySelector('.openlime-select-button');
		let ul = edit.querySelector('ul');
		let options = edit.querySelectorAll('li');
		let input = edit.querySelector('[name=classes]');

		let state = edit.querySelector('.openlime-state');

		state.addEventListener('click', (e) => {
			if (this.enableState) this.setAnnotationCurrentState(this.annotation);
			this.saveCurrent();
			this.saveAnnotation();
		});

		button.addEventListener('click', (e) => {
			e.stopPropagation();
			for (let o of options)
				o.classList.remove('selected');
			select.classList.toggle('active');

		});

		ul.addEventListener('click', (e) => {
			e.stopPropagation();

			input.value = e.srcElement.getAttribute('data-class');
			input.dispatchEvent(new Event('change'));
			button.style.background = this.classes[input.value].stroke;
			button.textContent = e.srcElement.textContent;

			select.classList.toggle('active');
		});

		document.addEventListener('click', (e) => {
			select.classList.remove('active');
		});

		document.querySelector('.openlime-layers-menu').appendChild(edit);

		let tools = edit.querySelector('.openlime-annotation-edit-tools');

		let pin = await Skin.appendIcon(tools, '.openlime-pin');
		pin.addEventListener('click', (e) => { this.setTool('pin'); this.setActiveTool(pin); });

		let draw = await Skin.appendIcon(tools, '.openlime-draw');
		draw.addEventListener('click', (e) => { this.setTool('line'); this.setActiveTool(draw); });


		//		let pen = await Skin.appendIcon(tools, '.openlime-pen'); 
		//		pen.addEventListener('click', (e) => { this.setTool('pen'); setActive(pen); });

		let erase = await Skin.appendIcon(tools, '.openlime-erase');
		erase.addEventListener('click', (e) => { this.setTool('erase'); this.setActiveTool(erase); });

		let undo = await Skin.appendIcon(tools, '.openlime-undo');
		undo.addEventListener('click', (e) => { this.undo(); });

		let redo = await Skin.appendIcon(tools, '.openlime-redo');
		redo.addEventListener('click', (e) => { this.redo(); });

		/*		let colorpick = await Skin.appendIcon(tools, '.openlime-colorpick'); 
				undo.addEventListener('click', (e) => { this.pickColor(); }); */

		let label = edit.querySelector('[name=label]');
		label.addEventListener('blur', (e) => { if (this.annotation.label != label.value) this.saveCurrent(); this.saveAnnotation(); });

		let descr = edit.querySelector('[name=description]');
		descr.addEventListener('blur', (e) => { if (this.annotation.description != descr.value) this.saveCurrent(); this.saveAnnotation(); });

		let idx = edit.querySelector('[name=idx]');
		idx.addEventListener('blur', (e) => {
			if (this.annotation.idx != idx.value) {
				const svgPinIdx = this.annotation.elements[0];
				if (svgPinIdx) {
					const txt = svgPinIdx.querySelector(".pin-text");
					if (txt) {
						txt.textContent = idx.value;
					}
				}
				this.saveCurrent();
			}
			this.saveAnnotation();
		});

		Object.entries(this.annotation.data).map(k => {
			let dataElm = edit.querySelector(`[name=data-data-${k[0]}]`);
			dataElm.addEventListener('blur', (e) => { if (this.annotation.data[k[0]] != dataElm.value) this.saveCurrent(); this.saveAnnotation(); });
		});

		let classes = edit.querySelector('[name=classes]');
		classes.addEventListener('change', (e) => { if (this.annotation.class != classes.value) this.saveCurrent(); this.saveAnnotation(); });

		let publish = edit.querySelector('[name=publish]');
		publish.addEventListener('change', (e) => { if (this.annotation.publish != publish.value) this.saveCurrent(); this.saveAnnotation(); });

		edit.classList.add('hidden');
		this.editWidget = edit;
	}

	/** @ignore */
	setAnnotationCurrentState(anno) {
		anno.state = window.structuredClone(this.viewer.canvas.getState());
		// Callback to add  light/lens params or other data
		if (this.customState) this.customState(anno);
	}

	/**
	 * Saves annotation changes and triggers update callback
	 * @private
	 */
	saveAnnotation() {
		let edit = this.editWidget;
		let anno = this.annotation;

		anno.label = edit.querySelector('[name=label]').value || '';
		anno.description = edit.querySelector('[name=description]').value || '';
		anno.idx = edit.querySelector('[name=idx]').value || '0';
		Object.entries(anno.data).map(k => {
			anno.data[k[0]] = edit.querySelector(`[name=data-data-${k[0]}]`).value || '';
		});
		anno.publish = edit.querySelector('[name=publish]').checked ? 1 : 0;
		let select = edit.querySelector('[name=classes]');
		anno.class = select.value || '';

		let button = edit.querySelector('.openlime-select-button');
		button.style.background = this.classes[anno.class].stroke;

		for (let e of this.annotation.elements)
			e.setAttribute('data-class', anno.class);

		let post = {
			id: anno.id, idx: anno.idx, label: anno.label, description: anno.description, class: anno.class,
			publish: anno.publish, data: anno.data
		};
		if (this.enableState) post = { ...post, state: anno.state };
		// if (anno.light) post = { ...post, light: anno.light }; FIXME
		// if (anno.lens) post = { ...post, lens: anno.lens };

		//anno.bbox = anno.getBBoxFromElements();
		let serializer = new XMLSerializer();
		post.svg = `<svg xmlns="http://www.w3.org/2000/svg">
				${anno.elements.map((s) => { s.classList.remove('selected'); return serializer.serializeToString(s) }).join("\n")}  
				</svg>`;

		if (this.updateCallback) {
			let result = this.updateCallback(post);
			if (!result) {
				alert("Failed to update annotation");
				return;
			}
		}				//for (let c of element.children)
		//		a.elements.push(c);

		//update the entry
		let template = document.createElement('template');
		template.innerHTML = this.layer.createAnnotationEntry(anno);
		let entry = template.content.firstChild;
		//TODO find a better way to locate the entry!
		this.layer.annotationsListEntry.element.parentElement.querySelector(`[data-annotation="${anno.id}"]`).replaceWith(entry);
		this.layer.setSelected(anno);
	}

	/**
	 * Deletes the selected annotation
	 * @returns {void}
	 */
	deleteSelected() {
		let id = this.layer.selected.values().next().value;
		if (id)
			this.deleteAnnotation(id);
	}

	/** @ignore */
	deleteAnnotation(id) {
		let anno = this.layer.getAnnotationById(id);
		if (this.deleteCallback) {
			if (!confirm(`Deleting annotation ${anno.label}, are you sure?`))
				return;
			let result = this.deleteCallback(anno);
			if (!result) {
				alert("Failed to delete this annotation.");
				return;
			}
		}
		//remove svg elements from the canvas
		this.layer.svgGroup.querySelectorAll(`[data-annotation="${anno.id}"]`).forEach(e => e.remove());

		//remove entry from the list
		let list = this.layer.annotationsListEntry.element.parentElement.querySelector('.openlime-list');
		list.querySelectorAll(`[data-annotation="${anno.id}"]`).forEach(e => e.remove());

		this.layer.annotations = this.layer.annotations.filter(a => a !== anno);
		this.layer.clearSelected();
		this.hideEditWidget();
	}

	/**
	 * Exports all annotations as SVG
	 * @returns {void}
	 */
	exportAnnotations() {
		let svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
		const bBox = this.layer.boundingBox();
		svgElement.setAttribute('viewBox', `0 0 ${bBox.xHigh - bBox.xLow} ${bBox.yHigh - bBox.yLow}`);
		let style = Util.createSVGElement('style');
		style.textContent = this.layer.style;
		svgElement.appendChild(style);
		let serializer = new XMLSerializer();
		//let svg = `<svg xmlns="http://www.w3.org/2000/svg">
		for (let anno of this.layer.annotations) {
			for (let e of anno.elements) {
				if (e.tagName == 'path') {
					//Inkscape nitpicks on the commas in svg path.
					let d = e.getAttribute('d');
					e.setAttribute('d', d.replaceAll(',', ' '));
				}
				svgElement.appendChild(e.cloneNode());
			}
		}
		let svg = serializer.serializeToString(svgElement);
		/*(${this.layer.annotations.map(anno => {
			return `<group id="${anno.id}" title="${anno.label}" data-description="${anno.description}">
				${anno.elements.map((s) => { 
					s.classList.remove('selected'); 
					return serializer.serializeToString(s) 
				}).join("\n")}
				</group>`;
		})}
		</svg>`; */

		///console.log(svg);

		var e = document.createElement('a');
		e.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(svg));
		e.setAttribute('download', 'annotations.svg');
		e.style.display = 'none';
		document.body.appendChild(e);
		e.click();
		document.body.removeChild(e);
	}

	/** @ignore */
	setActiveTool(e) {
		if (!this.editWidget) return;
		let tools = this.editWidget.querySelector('.openlime-annotation-edit-tools');
		tools.querySelectorAll('svg').forEach(a =>
			a.classList.remove('active'));
		if (e)
			e.classList.add('active');
	}

	/**
	 * Sets the active drawing tool
	 * @param {string} tool - Tool name ('point', 'pin', 'line', 'box', 'circle', 'erase', or null)
	 * @private
	 */
	setTool(tool) {
		this.tool = tool;
		if (this.factory && this.factory.quit)
			this.factory.quit();
		if (tool) {
			if (!tool in this.tools)
				throw "Unknown editor tool: " + tool;

			this.factory = new this.tools[tool].tool(this.tools[tool]);
			this.factory.annotation = this.annotation;
			this.factory.layer = this.layer;
		}
		document.querySelector('.openlime-overlay').classList.toggle('erase', tool == 'erase');
		document.querySelector('.openlime-overlay').classList.toggle('crosshair', tool && tool != 'erase');
	}


	// UNDO STUFF	

	/**
	 * Performs an undo operation
	 * @returns {void}
	 */
	undo() {
		let anno = this.annotation; //current annotation.
		if (!anno)
			return;
		if (this.factory && this.factory.undo && this.factory.undo()) {
			anno.needsUpdate = true;
			this.viewer.redraw();
			return;
		}

		if (anno.history && anno.history.length) {
			//FIXME TODO history will be more complicated if it has to manage multiple tools.
			anno.future.push(this.annoToData(anno));

			let data = anno.history.pop();
			this.dataToAnno(data, anno);

			anno.needsUpdate = true;
			this.viewer.redraw();
			this.updateEditWidget();
		}
	}

	/**
	 * Performs a redo operation
	 * @returns {void}
	 */
	redo() {
		let anno = this.annotation; //current annotation.
		if (!anno)
			return;
		if (this.factory && this.factory.redo && this.factory.redo()) {
			anno.needsUpdate = true;
			this.viewer.redraw();
			return;
		}
		if (anno.future && anno.future.length) {
			anno.history.push(this.annoToData(anno));

			let data = anno.future.pop();
			this.dataToAnno(data, anno);

			anno.needsUpdate = true;
			this.viewer.redraw();
			this.updateEditWidget();
		}
	}

	/**
	 * Saves current annotation state to history
	 * @private
	 */
	saveCurrent() {
		let anno = this.annotation; //current annotation.
		if (!anno.history)
			anno.history = [];

		anno.history.push(this.annoToData(anno));
		anno.future = [];
	}

	/** @ignore */
	annoToData(anno) {
		let data = {};
		for (let i of ['id', 'label', 'description', 'class', 'publish', 'data'])
			data[i] = `${anno[i] || ''}`;
		data.elements = anno.elements.map(e => { let n = e.cloneNode(); n.points = e.points; return n; });
		return data;
	}

	/** @ignore */
	dataToAnno(data, anno) {
		for (let i of ['id', 'label', 'description', 'class', 'publish', 'data'])
			anno[i] = `${data[i]}`;
		anno.elements = data.elements.map(e => { let n = e.cloneNode(); n.points = e.points; return n; });
	}


	// TOOLS STUFF

	/** @ignore */
	keyUp(e) {
		if (e.defaultPrevented) return;
		switch (e.key) {
			case 'Escape':
				if (this.tool) {
					this.setActiveTool();
					this.setTool(null);
					e.preventDefault();
				}
				break;
			case 'Delete':
				this.deleteSelected();
				break;
			case 'Backspace':
				break;
			case 'z':
				if (e.ctrlKey)
					this.undo();
				break;
			case 'Z':
				if (e.ctrlKey)
					this.redo();
				break;
		}
	}

	/** @ignore */
	panStart(e) {
		if (e.buttons != 1 || e.ctrlKey || e.altKey || e.shiftKey || e.metaKey)
			return;
		if (!['line', 'erase', 'box', 'circle'].includes(this.tool))
			return;
		this.panning = true;
		e.preventDefault();

		this.saveCurrent();

		const pos = this.mapToSvg(e);
		let shape = this.factory.create(pos, e);

		this.annotation.needsUpdate = true;

		this.viewer.redraw();
	}

	/** @ignore */
	panMove(e) {
		if (!this.panning)
			return false;

		const pos = this.mapToSvg(e);
		this.factory.adjust(pos, e);
	}

	/** @ignore */
	panEnd(e) {
		if (!this.panning)
			return false;
		this.panning = false;

		const pos = this.mapToSvg(e);
		let changed = this.factory.finish(pos, e);
		if (!changed) //nothing changed no need to keep current situation in history.
			this.annotation.history.pop();
		else
			this.saveAnnotation();
		this.annotation.needsUpdate = true;
		this.viewer.redraw();
	}

	/** @ignore */
	fingerHover(e) {
		if (this.tool != 'line')
			return;
		e.preventDefault();
		const pos = this.mapToSvg(e);
		let changed = this.factory.hover(pos, e);
		this.annotation.needsUpdate = true;
		this.viewer.redraw();
	}

	/** @ignore */
	fingerSingleTap(e) {
		if (!['point', 'pin', 'line', 'erase'].includes(this.tool))
			return;
		e.preventDefault();

		this.saveCurrent();

		const pos = this.mapToSvg(e);
		let changed = this.factory.tap(pos, e)
		if (!changed) //nothing changed no need to keep current situation in history.
			this.annotation.history.pop();
		else
			this.saveAnnotation();
		this.annotation.needsUpdate = true;

		this.viewer.redraw();
	}

	/** @ignore */
	fingerDoubleTap(e) {
		if (!['line'].includes(this.tool))
			return;
		e.preventDefault();

		this.saveCurrent();

		const pos = this.mapToSvg(e);
		let changed = this.factory.doubleTap(pos, e)
		if (!changed) //nothing changed no need to keep current situation in history.
			this.annotation.history.pop();
		else
			this.saveAnnotation();
		this.annotation.needsUpdate = true;

		this.viewer.redraw();
	}

	/**
	 * Converts viewer coordinates to SVG coordinates
	 * @param {PointerEvent} event - Pointer event
	 * @returns {Object} Position in SVG coordinates with pixel size information
	 * @private
	 */
	mapToSvg(e) {
		const p = { x: e.offsetX, y: e.offsetY };
		const layerT = this.layer.transform;
		const useGL = false;
		const layerbb = this.layer.boundingBox();
		const layerSize = { w: layerbb.width(), h: layerbb.height() };
		//compute also size of an image pixel on screen and store in pixelSize.
		let pos = CoordinateSystem.fromCanvasHtmlToImage(p, this.viewer.camera, layerT, layerSize, useGL);
		p.x += 1;
		let pos1 = CoordinateSystem.fromCanvasHtmlToImage(p, this.viewer.camera, layerT, layerSize, useGL);
		pos.pixelSize = Math.abs(pos1.x - pos.x);
		return pos;
	}
}


/** @ignore */
class Point {
	tap(pos) {
		let point = Util.createSVGElement('circle', { cx: pos.x, cy: pos.y, r: 10, class: 'point' });
		this.annotation.elements.push(point);
		return true;
	}
}

/** @ignore */
class Pin {
	constructor(options) {
		Object.assign(this, options);
	}
	tap(pos) {
		const str = this.template(pos.x, pos.y);
		let parser = new DOMParser();
		let point = parser.parseFromString(str, "image/svg+xml").documentElement;
		//		this.annotation.elements.push(point);
		this.annotation.elements[0] = point;
		return true;
	}
}

/** @ignore */
class Pen {
	constructor() {
		//TODO Use this.path.points as in line, instead.
		this.points = [];
	}
	create(pos) {
		this.points.push(pos);
		if (this.points.length == 1) {
			saveCurrent

			this.path = Util.createSVGElement('path', { d: `M${pos.x} ${pos.y}`, class: 'line' });
			return this.path;
		}
		let p = this.path.getAttribute('d');
		this.path.setAttribute('d', p + ` L${pos.x} ${pos.y}`);
		this.path.points = this.points;
	}
	undo() {
		if (!this.points.length)
			return;
		this.points.pop();
		let d = this.points.map((p, i) => `${i == 0 ? 'M' : 'L'}${p.x} ${p.y}`).join(' ');
		this.path.setAttribute('d', d);

		if (this.points.length < 2) {
			this.points = [];
			this.annotation.elements = this.annotation.elements.filter((e) => e != this.path);
		}
	}
}

/** @ignore */
class Box {
	constructor() {
		this.origin = null;
		this.box = null;
	}

	create(pos) {
		this.origin = pos;
		this.box = Util.createSVGElement('rect', { x: pos.x, y: pos.y, width: 0, height: 0, class: 'rect' });
		return this.box;
	}

	adjust(pos) {
		let p = this.origin;

		this.box.setAttribute('x', Math.min(p.x, pos.x));
		this.box.setAttribute('width', Math.abs(pos.x - p.x));
		this.box.setAttribute('y', Math.min(p.y, pos.y));
		this.box.setAttribute('height', Math.abs(pos.y - p.y));
	}

	finish(pos) {
		return this.box;
	}
}

/** @ignore */
class Circle {
	constructor() {
		this.origin = null;
		this.circle = null;
	}
	create(pos) {
		this.origin = pos;
		this.circle = Util.createSVGElement('circle', { cx: pos.x, cy: pos.y, r: 0, class: 'circle' });
		return this.circle;
	}
	adjust(pos) {
		let p = this.origin;
		let r = Math.hypot(pos.x - p.x, pos.y - p.y);
		this.circle.setAttribute('r', r);
	}
	finish() {
		return this.circle;
	}
}

/** @ignore */
class Line {
	constructor() {
		this.history = []
	}
	create(pos) {
		/*if(this.segment) {
			this.layer.svgGroup.removeChild(this.segment);
			this.segment = null;
		}*/
		for (let e of this.annotation.elements) {
			if (!e.points || e.points.length < 2)
				continue;
			if (Line.distance(e.points[0], pos) / pos.pixelSize < 5) {
				e.points.reverse();
				this.path = e;
				this.path.setAttribute('d', Line.svgPath(e.points));
				//reverse points!
				this.history = [this.path.points.length];
				return;
			}
			if (Line.distanceToLast(e.points, pos) < 5) {
				this.path = e;
				this.adjust(pos);
				this.history = [this.path.points.length];
				return;
			}
		}
		this.path = Util.createSVGElement('path', { d: `M${pos.x} ${pos.y}`, class: 'line' });
		this.path.points = [pos];
		this.history = [this.path.points.length];
		this.annotation.elements.push(this.path);
	}

	tap(pos) {
		if (!this.path) {
			this.create(pos);
			return false;
		} else {
			if (this.adjust(pos))
				this.history = [this.path.points.length - 1];
			return true;
		}
	}
	doubleTap(pos) {
		if (!this.path)
			return false;
		if (this.adjust(pos)) {
			this.history = [this.path.points.length - 1];
			this.path = null;
		}
		return false;
	}

	hover(pos, event) {
		return;
		if (!this.path)
			return false;
		let s = this.path.points[this.path.points.length - 1];
		if (!this.segment) {
			this.segment = Util.createSVGElement('path', { class: 'line' });
			this.layer.svgGroup.appendChild(this.segment);
		}
		pos.x = pos.x - s.x;
		pos.y = pos.y - s.y;
		let len = Math.sqrt(pos.x * pos.x + pos.y * pos.y);
		if (len > 30) {
			pos.x *= 30 / len;
			pos.y *= 30 / len;
		}
		this.segment.setAttribute('d', `M${s.x} ${s.y} l${pos.x} ${pos.y}`);
		return true;
	}
	quit() {
		return;
		if (this.segment) {
			this.layer.svgGroup.removeChild(this.segment);
			this.segment = null;
		}
	}

	adjust(pos) {
		let gap = Line.distanceToLast(this.path.points, pos);
		if (gap / pos.pixelSize < 4) return false;

		this.path.points.push(pos);

		let d = this.path.getAttribute('d');
		this.path.setAttribute('d', Line.svgPath(this.path.points));//d + `L${pos.x} ${pos.y}`);
		return true;
	}

	finish() {
		this.path.setAttribute('d', Line.svgPath(this.path.points));
		return true; //some changes where made!
	}

	undo() {
		if (!this.path || !this.history.length)
			return false;
		this.path.points = this.path.points.slice(0, this.history.pop());
		this.path.setAttribute('d', Line.svgPath(this.path.points));
		return true;
	}
	redo() {
		return false;
	}
	//TODO: smooth should be STABLE, if possible.
	static svgPath(points) {
		//return points.map((p, i) =>  `${(i == 0? "M" : "L")}${p.x} ${p.y}`).join(' '); 

		let tolerance = 1.5 * points[0].pixelSize;
		let tmp = simplify(points, tolerance);

		let smoothed = smooth(tmp, 90, true);
		return smoothToPath(smoothed);

	}
	static distanceToLast(line, point) {
		let last = line[line.length - 1];
		return Line.distance(last, point);
	}
	static distance(a, b) {
		let dx = a.x - b.x;
		let dy = a.y - b.y;
		return Math.sqrt(dx * dx + dy * dy);
	}
}

/** @ignore */
class Erase {
	create(pos, event) { this.erased = false; this.erase(pos, event); }
	adjust(pos, event) { this.erase(pos, event); }
	finish(pos, event) { return this.erase(pos, event); } //true if some points where removed.
	tap(pos, event) { return this.erase(pos, event); }
	erase(pos, event) {
		for (let e of this.annotation.elements) {
			if (e == event.originSrc) {
				e.points = [];
				this.erased = true;
				continue;
			}

			let points = e.points;
			if (!points || !points.length)
				continue;

			if (Line.distanceToLast(points, pos) < 10)
				this.erased = true, points.pop();
			else if (Line.distance(points[0], pos) < 10)
				this.erased = true, points.shift();
			else
				continue;

			if (points.length <= 2) {
				e.points = [];
				e.setAttribute('d', '');
				this.annotation.needsUpdate = true;
				this.erased = true;
				continue;
			}

			e.setAttribute('d', Line.svgPath(points));
		}
		this.annotation.elements = this.annotation.elements.filter(e => { return !e.points || e.points.length > 2; });
		return this.erased;
	}
}

export { EditorSvgAnnotation }