Source: Annotation.js

import { BoundingBox } from './BoundingBox.js'

/**
 * Represents an annotation that can be drawn as an overlay on a canvas.
 * An annotation is a decoration (text, graphics element, glyph) that provides
 * additional information for interpreting underlying drawings.
 * Each annotation has a unique identifier and can contain various properties
 * such as description, category, drawing style, labels, etc.
 * 
 */
class Annotation {
	/**
		* Creates a new Annotation instance.
		* @param {Object} [options] - Configuration options for the annotation.
		* @param {string} [options.id] - Unique identifier for the annotation. If not provided, a UUID will be generated.
		* @param {string} [options.code] - A code identifier for the annotation.
		* @param {string} [options.label=''] - Display label for the annotation.
		* @param {string} [options.description] - HTML text containing a comprehensive description.
		* @param {string} [options.class] - Category or classification of the annotation.
		* @param {string} [options.target] - Target element or area this annotation refers to.
		* @param {string} [options.svg] - SVG content for the annotation.
		* @param {Object} [options.image] - Image data associated with the annotation.
		* @param {Object} [options.region] - Region coordinates {x, y, w, h} for the annotation.
		* @param {Object} [options.data={}] - Additional custom data for the annotation.
		* @param {Object} [options.style] - Style configuration for rendering.
		* @param {BoundingBox} [options.bbox] - Bounding box of the annotation.
		* @param {boolean} [options.visible=true] - Visibility state of the annotation.
		* @param {Object} [options.state] - State variables for the annotation.
		* @param {boolean} [options.ready=false] - Indicates if SVG conversion is complete.
		* @param {boolean} [options.needsUpdate=true] - Indicates if annotation needs updating.
		* @param {boolean} [options.editing=false] - Indicates if annotation is being edited.
		* @class
		*/
	constructor(options) {
		Object.assign(
			this,
			{
				id: Annotation.UUID(),
				code: null,
				label: null,
				description: null,
				class: null,
				target: null,
				svg: null,
				image: null,
				region: null,
				data: {},
				style: null,
				bbox: null,
				visible: true,
				state: null,
				ready: false, //already: converted to svg
				needsUpdate: true,
				editing: false,
			},
			options);
		//TODO label as null is problematic, sort this issue.
		if (!this.label) this.label = '';
		this.elements = []; //assign options is not recursive!!!
	}

	/**
		* Generates a UUID (Universally Unique Identifier) for annotation instances.
		* @returns {string} A newly generated UUID.
		* @private
		*/
	static UUID() {
		return 'axxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
			var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
			return v.toString(16);
		});
	}

	/**
	 * Calculates and returns the bounding box of the annotation based on its elements or region.
	 * The coordinates are always relative to the top-left corner of the canvas.
	 * @returns {BoundingBox} The calculated bounding box of the annotation.
	 * If the annotation has no elements and no region, returns an empty bounding box.
	 * If the annotation has a region but no elements, returns a bounding box based on the region.
	 * If the annotation has elements, calculates the bounding box that encompasses all elements.
	 */
	getBBoxFromElements() {
		let box = null;
		if (!this.elements.length) {
			if (this.region == null) {
				box = new BoundingBox();
			} else {
				const r = this.region;
				box = new BoundingBox({ xLow: r.x, yLow: r.y, xHigh: r.x + r.w, yHigh: r.y + r.h });
			}
		} else {
			let { x, y, width, height } = this.elements[0].getBBox();
			for (let shape of this.elements) {
				const { sx, sy, swidth, sheight } = shape.getBBox();
				x = Math.min(x, sx);
				y = Math.min(x, sy);
				width = Math.max(width + x, sx + swidth) - x;
				height = Math.max(height + y, sy + sheight) - y;
			}
			box = new BoundingBox({ xLow: x, yLow: y, xHigh: x + width, yHigh: y + width });
		}
		return box;
	}

	/////////////////////////////////
	/**
	 * Creates an Annotation instance from a JSON-LD format string.
	 * @param {Object} entry - The JSON-LD object representing an annotation.
	 * @returns {Annotation} A new Annotation instance.
	 * @throws {Error} If the entry is not a valid JSON-LD annotation or contains unsupported selectors.
	 */
	static fromJsonLd(entry) {
		if (entry.type != 'Annotation')
			throw "Not a jsonld annotation.";
		let options = { id: entry.id };

		let rename = { 'identifying': 'code', 'identifying': 'label', 'describing': 'description', 'classifying': 'class' };
		for (let item of entry.body) {
			let field = rename[item.purpose];
			if (field)
				options[field] = item.value;
		}
		let selector = entry.target && entry.target.selector;
		if (selector) {
			switch (selector.type) {
				case 'SvgSelector':
					options.svg = selector.value;
					options.elements = [];
					break;
				default:
					throw "Unsupported selector: " + selector.type;
			}
		}
		return new Annotation(options);
	}

	/**
	* Converts the annotation to a JSON-LD format object.
 	* @returns {Object} A JSON-LD representation of the annotation including context, 
	* id, type, body (with code, class, and description), and target selector information.
 	*/
	toJsonLd() {
		let body = [];
		if (this.code !== null)
			body.push({ type: 'TextualBody', value: this.code, purpose: 'indentifying' });
		if (this.class !== null)
			body.push({ type: 'TextualBody', value: this.class, purpose: 'classifying' });
		if (this.description !== null)
			body.push({ type: 'TextualBody', value: this.description, purpose: 'describing' });

		let obj = {
			"@context": "http://www.w3.org/ns/anno.jsonld",
			id: this.id,
			type: "Annotation",
			body: body,
			target: { selector: {} }
		}
		if (this.target)
			target.selector.source = this.target;


		if (this.element) {
			var s = new XMLSerializer();
			obj.target.selector.type = 'SvgSelector';
			obj.target.selector.value = s.serializeToString(this.element);
		}
	}
}

export { Annotation }