Source: ScaleBar.js

import { Util } from './Util'

/**
 * ScaleBar module provides measurement scale visualization and unit conversion functionality.
 * Includes both a base Units class for unit management and a ScaleBar class for visual representation.
 *
 * Units class provides unit conversion and formatting capabilities.
 * Supports various measurement units and automatic unit selection based on scale.
 */
class Units {
	/**
	 * Creates a new Units instance.
	 * @param {Object} [options] - Configuration options
	 * @param {string[]} [options.units=['km', 'm', 'cm', 'mm', 'µm']] - Available units in order of preference
	 * @param {Object.<string, number>} [options.allUnits] - All supported units and their conversion factors to millimeters
	 * @param {number} [options.precision=2] - Number of decimal places for formatted values
	 */
	constructor(options) {
		this.units = ["km", "m", "cm", "mm", "µm"],
			this.allUnits = { "µm": 0.001, "mm": 1, "cm": 10, "m": 1000, "km": 1e6, "in": 254, "ft": 254 * 12 }
		this.precision = 2;
		if (options)
			Object.assign(options, this);
	}

	/**
	 * Formats a measurement value with appropriate units.
	 * Automatically selects the best unit if none specified.
	 * @param {number} d - Value to format (in millimeters)
	 * @param {string} [unit] - Specific unit to use for formatting
	 * @returns {string} Formatted measurement with units (e.g., "5.00 mm" or "1.00 m")
	 * 
	 * @example
	 * const units = new Units();
	 * units.format(1500);       // Returns "1.50 m"
	 * units.format(1500, 'mm'); // Returns "1500.00 mm"
	 */
	format(d, unit) {
		if (d == 0)
			return '';
		if (unit)
			return (d / this.allUnits[unit]).toFixed(this.precision) + unit;

		let best_u = null;
		let best_penalty = 100;
		for (let u of this.units) {
			let size = this.allUnits[u];
			let penalty = d <= 0 ? 0 : Math.abs(Math.log10(d / size) - 1);
			if (penalty < best_penalty) {
				best_u = u;
				best_penalty = penalty;
			}
		}
		return this.format(d, best_u);
	}
}

/**
 * ScaleBar class creates a visual scale bar that updates with viewer zoom level.
 * Features:
 * - Automatic scale adjustment based on zoom
 * - Smart unit selection
 * - SVG-based visualization
 * - Configurable size and appearance
 * @extends Units
 */
class ScaleBar extends Units {
	/**
	 * Creates a new ScaleBar instance.
	 * @param {number} pixelSize - Size of a pixel in real-world units (in mm)
	 * @param {Viewer} viewer - The OpenLIME viewer instance
	 * @param {Object} [options] - Configuration options
	 * @param {number} [options.width=200] - Width of the scale bar in pixels
	 * @param {number} [options.fontSize=24] - Font size for scale text in pixels
	 * @param {number} [options.precision=0] - Number of decimal places for scale values
	 * 
	 * @property {SVGElement} svg - Main SVG container element
	 * @property {SVGElement} line - Scale bar line element
	 * @property {SVGElement} text - Scale text element
	 * @property {number} lastScaleZoom - Last zoom level where scale was updated
	 */
	constructor(pixelSize, viewer, options) {
		super(options)
		options = Object.assign(this, {
			pixelSize: pixelSize,
			viewer: viewer,
			width: 200,
			fontSize: 24,
			precision: 0
		}, options);
		Object.assign(this, options);

		this.svg = Util.createSVGElement('svg', { viewBox: `0 0 ${this.width} 30` });
		this.svg.classList.add('openlime-scale');

		this.line = Util.createSVGElement('line', { x1: 5, y1: 26.5, x2: this.width - 5, y2: 26.5 });

		this.text = Util.createSVGElement('text', { x: '50%', y: '16px', 'dominant-basiline': 'middle', 'text-anchor': 'middle' });
		this.text.textContent = "";

		this.svg.appendChild(this.line);
		this.svg.appendChild(this.text);
		this.viewer.containerElement.appendChild(this.svg);
		this.viewer.addEvent('draw', () => { this.updateScale(); });
	}

	/**
	 * Updates the scale bar based on current zoom level.
	 * Called automatically on viewer draw events.
	 * @private
	 */
	updateScale() {
		//let zoom = this.viewer.camera.getCurrentTransform(performance.now()).z;
		let zoom = this.viewer.camera.target.z;
		if (zoom == this.lastScaleZoom)
			return;
		this.lastScaleZoom = zoom;
		let s = this.bestLength(this.width / 2, this.width, this.pixelSize, zoom);

		let margin = this.width - s.length;
		this.line.setAttribute('x1', margin / 2);
		this.line.setAttribute('x2', this.width - margin / 2);
		this.text.textContent = this.format(s.label);
	}

	/**
	 * Calculates the best scale length and label value for current zoom.
	 * Tries to find a "nice" round number that fits within the given constraints.
	 * @private
	 * @param {number} min - Minimum desired length in pixels
	 * @param {number} max - Maximum desired length in pixels
	 * @param {number} pixelSize - Size of a pixel in real-world units
	 * @param {number} zoom - Current zoom level
	 * @returns {Object} Scale information
	 * @returns {number} .length - Length of scale bar in pixels
	 * @returns {number} .label - Value to display (in real-world units)
	 */
	bestLength(min, max, pixelSize, zoom) {
		pixelSize /= zoom;
		//closest power of 10:
		let label10 = Math.pow(10, Math.floor(Math.log(max * pixelSize) / Math.log(10)));
		let length10 = label10 / pixelSize;
		if (length10 > min) return { length: length10, label: label10 };

		let label20 = label10 * 2;
		let length20 = length10 * 2;
		if (length20 > min) return { length: length20, label: label20 };

		let label50 = label10 * 5;
		let length50 = length10 * 5;

		if (length50 > min) return { length: length50, label: label50 };
		return { length: 0, label: 0 }
	}
}
/**
 * Example usage:
 * ```javascript
 * // Create scale bar with 0.1mm per pixel
 * const scaleBar = new ScaleBar(0.1, viewer, {
 *     width: 300,
 *     fontSize: 20,
 *     precision: 1,
 *     units: ['m', 'cm', 'mm']  // Only use these units
 * });
 * 
 * // The scale bar will automatically update with viewer zoom
 * 
 * // Format a specific measurement
 * scaleBar.format(1234);  // Returns "1.23 m"
 * ```
 */
export { Units, ScaleBar }