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 === "px") {
return Math.floor(d) + unit;
}
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 }