UNPKG

@openhps/core

Version:

Open Hybrid Positioning System - Core component

418 lines (376 loc) 15.1 kB
import 'reflect-metadata'; import { SerializableObject, SerializableMember } from '../../data/decorators'; import { UnitOptions } from './UnitOptions'; import { UnitBasicDefinition, UnitDefinition, UnitFunctionDefinition, UnitValueType } from './UnitDefinition'; import { UnitPrefix, UnitPrefixType } from './UnitPrefix'; import { Vector3 } from '../math/Vector3'; /** * Unit * * ## Usage * ### Creation * ```typescript * const myUnit = new Unit("meter", { * baseName: "length", * aliases: ["m", "meters"], * prefixes: 'decimal' * }) * ``` * * ### Specifiers * You can specify the prefix using the ```specifier(...)``` function. * ```typescript * const nanoUnit = myUnit.specifier(UnitPrefix.NANO); * ``` * @category Unit */ @SerializableObject({ initializer: Unit.fromJSON, }) export class Unit { private _name: string; private _baseName: string; private _definitions: Map<string, UnitFunctionDefinition<any, any>> = new Map(); private _prefixType: UnitPrefixType = 'none'; private _aliases: string[] = []; // Unit bases (e.g. length, time, velocity, ...) protected static readonly UNIT_BASES: Map<string, string> = new Map(); // Units (e.g. second, meter, ...) protected static readonly UNITS: Map<string, Unit> = new Map(); static readonly UNKNOWN = new Unit('unknown'); /** * Create a new unit * @param {string} name Unit name * @param {UnitOptions} options Unit options */ constructor(name?: string, options?: UnitOptions) { const config: UnitOptions = options || { baseName: undefined }; config.aliases = config.aliases || []; config.prefixes = config.prefixes || 'none'; config.definitions = config.definitions || []; // Unit config this._name = name || config.name; this._baseName = config.baseName; this._aliases = config.aliases; this._prefixType = config.prefixes; // Unit definitions config.definitions.forEach(this._initDefinition.bind(this)); if (this.name) { Unit.registerUnit(this, config.override); } } /** * Get a unit from JSON * @param {any} json JSON object * @returns {Unit} Unit if found */ static fromJSON<T extends Unit | Unit>(json: any): T { if (json.name !== undefined) { const unit = Unit.findByName(json.name); if (!unit) { throw new Error(`Unit with name '${json.name}' not found! Unable to deserialize!`); } return unit as T; } else { throw new Error(`Unit does not define a serialization name! Unable to deserialize!`); } } private _initDefinition(definition: UnitDefinition): void { const referenceUnit = Unit.findByName(definition.unit, this.baseName); const unitName = referenceUnit ? referenceUnit.name : definition.unit; if ('toUnit' in definition) { // UnitFunctionDefinition this._initFunctionDefinition(definition, unitName); } else { // UnitBasicDefinition this._initBasicDefinition(definition, unitName); } } private _initFunctionDefinition(definition: UnitDefinition, unitName: string): void { const functionDefinition = definition as UnitFunctionDefinition<any, any>; this._definitions.set(unitName, functionDefinition); } private _initBasicDefinition(definition: UnitDefinition, unitName: string): void { const definitionKeys = Object.keys(definition); const basicDefinition: UnitBasicDefinition = definition; const magnitudeOrder = definitionKeys.indexOf('magnitude'); const offsetOrder = definitionKeys.indexOf('offset'); const magnitude = basicDefinition.magnitude || 1; const offset = basicDefinition.offset !== undefined ? basicDefinition.offset : 0; const offsetPriority = magnitudeOrder === -1 ? true : offsetOrder < magnitudeOrder; let toUnitFn: (value: number) => number; let fromUnitFn: (value: number) => number; if (offsetPriority) { toUnitFn = (value: number) => (value + offset) * magnitude; fromUnitFn = (value: number) => value / magnitude - offset; } else { toUnitFn = (value: number) => value * magnitude + offset; fromUnitFn = (value: number) => (value - offset) / magnitude; } this._definitions.set(unitName, { unit: basicDefinition.unit, toUnit: toUnitFn, fromUnit: fromUnitFn, }); } /** * Unit name * @returns {string} Name */ @SerializableMember() get name(): string { return this._name; } set name(name: string) { this._name = name; const existingUnit = Unit.findByName(name); if (existingUnit) { this._baseName = existingUnit.baseName; this._definitions = existingUnit._definitions; this._prefixType = existingUnit._prefixType; this._aliases = existingUnit._aliases; } } /** * Unit aliases * @returns {string[]} Alias names as array */ get aliases(): string[] { return this._aliases; } get baseName(): string { return this._baseName; } get prefixType(): UnitPrefixType { return this._prefixType; } get definitions(): UnitDefinition[] { return Array.from(this._definitions.values()); } protected get prefixes(): UnitPrefix[] { switch (this._prefixType) { case 'decimal': return UnitPrefix.DECIMAL; case 'none': return []; } } /** * Get or create a definition from this unit to the base * @returns {UnitFunctionDefinition} Definition to base */ createBaseDefinition(): UnitFunctionDefinition<any, any> { let newDefinition: UnitFunctionDefinition<any, any>; // Get base unit const baseUnitName = Unit.UNIT_BASES.get(this.baseName); if (this._definitions.has(baseUnitName)) { const definition = this._definitions.get(baseUnitName); newDefinition = definition; } else { this._definitions.forEach((definition) => { const unit = Unit.findByName(definition.unit, this.baseName); const baseDefinition = unit.createBaseDefinition(); if (baseDefinition) { newDefinition = { unit: baseDefinition.unit, toUnit: (value: any) => baseDefinition.toUnit(definition.toUnit(value)), fromUnit: (value: any) => definition.fromUnit(baseDefinition.fromUnit(value)), }; return; } }); } return newDefinition; } createDefinition(targetUnit: Unit): UnitFunctionDefinition<any, any> { let newDefinition: UnitFunctionDefinition<any, any>; // Get base unit const baseUnitName = Unit.UNIT_BASES.get(this.baseName); const baseUnit = Unit.findByName(baseUnitName); if (this._definitions.has(targetUnit.name)) { // Direct conversion const definition = this._definitions.get(targetUnit.name); newDefinition = definition; } else if (targetUnit._definitions.has(this.name)) { // Reverse conversion const definition = targetUnit._definitions.get(this.name); newDefinition = { inputType: definition.inputType, outputType: definition.outputType, unit: targetUnit.name, toUnit: definition.fromUnit, fromUnit: definition.toUnit, }; this._definitions.set(targetUnit.name, newDefinition); } else if (baseUnit.name !== this.name) { // No direct conversion found, convert to base unit const currentToBase = this._definitions.get(baseUnitName); const baseToTarget = baseUnit.createDefinition(targetUnit); // Convert unit if definitions are found if (currentToBase && baseToTarget) { newDefinition = { inputType: currentToBase.inputType, outputType: currentToBase.outputType, unit: targetUnit.name, toUnit: (value: UnitValueType) => baseToTarget.toUnit(currentToBase.toUnit(value)), fromUnit: (value: UnitValueType) => currentToBase.fromUnit(baseToTarget.fromUnit(value)), }; this._definitions.set(targetUnit.name, newDefinition); } } return newDefinition; } /** * Get the unit specifier * @param {UnitPrefix} prefix Unit prefix * @returns {Unit} Unit with specifier */ specifier(prefix: UnitPrefix): this { // Check if the unit already exists const unitName = `${prefix.name}${this.name}`; if (Unit.UNITS.has(unitName)) { return Unit.UNITS.get(unitName) as this; } // Confirm that the prefix is allowed if (!this.prefixes.includes(prefix)) throw new Error(`Prefix '${prefix.name}' is not allowed for this unit!`); // Get the unit constructor of the extended class. This allows // serializing of units that are extended (e.g. LengthUnit) const UnitConstructor = Object.getPrototypeOf(this).constructor; const unit: Unit = new UnitConstructor(); unit._name = unitName; unit._baseName = this.baseName; const aliases: Array<string> = []; this.aliases.forEach((alias) => { aliases.push(`${prefix.name}${alias}`); aliases.push(`${prefix.abbrevation}${alias}`); }); unit._aliases = aliases; unit._definitions.set(this.name, { unit: this.name, toUnit: (value: number) => value * prefix.magnitude, fromUnit: (value: number) => value / prefix.magnitude, }); return Unit.registerUnit(unit) as this; } /** * Find unit specifier by name or alias * @param {string} name Unit name * @returns {Unit | undefined} Unit if found */ protected findByName(name: string): Unit | undefined { // Check all aliases in those units for (const alias of this.aliases.concat(this.name)) { if (name === alias) { // Exact match with alias return this; } else if (name.endsWith(alias)) { // Unit that we are looking for ends with the alias // confirm that there is a prefix match for (const prefix of this.prefixes) { if (name.match(prefix.abbrevationPattern) || name.match(prefix.namePattern)) { return this.specifier(prefix); } } } } return undefined; } /** * Find a unit by its name * @param {string} name Unit name * @param {string} baseName Optional base name to specific result * @returns {Unit | undefined} Unit if found */ static findByName(name: string, baseName?: string): Unit | undefined { if (name === undefined) { return undefined; } else if (Unit.UNITS.has(name)) { return Unit.UNITS.get(name); } else { // Check all units for (const [, unit] of Unit.UNITS) { if (baseName ? baseName !== unit.baseName : false) { continue; } // Check all aliases in those units const result = unit.findByName(name); if (result) { return result; } } return undefined; } } /** * Convert a value in the current unit to a target unit * @param {UnitValueType} value Value to convert * @param {string | Unit} target Target unit * @returns {number} Converted unit */ convert<T extends UnitValueType>(value: T, target: string | Unit): T { const targetUnit: Unit = target instanceof Unit ? target : Unit.findByName(target, this.baseName); // Do not convert if target unit is the same or undefined if (!targetUnit || targetUnit.name === this.name) { return value; } const definition = this.createDefinition(targetUnit); if (!definition) { throw new Error(`No conversion definition found from '${this.name}' to '${targetUnit.name}'!`); } else { if (value instanceof Vector3 && definition.inputType !== Vector3) { // Convert vector individually when definition only supports atomic data return value .clone() .fromArray([ definition.toUnit(value.x), definition.toUnit(value.y), definition.toUnit(value.z), ]) as T; } else { return definition.toUnit(value) as T; } } } /** * Convert a value from a specific unit to a target unit * @param {UnitValueType} value Value to convert * @param {string | Unit} from Source unit * @param {string | Unit} to Target unit * @returns {UnitValueType} Converted unit */ static convert<T extends UnitValueType>(value: T, from: string | Unit, to: string | Unit): T { const fromUnit: Unit = typeof from === 'string' ? Unit.findByName(from) : from; return fromUnit.convert(value, to); } /** * Register a new unit * @param {Unit} unit Unit to register * @param {boolean} override Override an existing unit with the same name * @returns {Unit} Registered unit */ static registerUnit(unit: Unit, override: boolean = false): Unit { if (!unit.name) { return unit; } // Register unit if it does not exist yet if (!Unit.UNITS.has(unit.name) || override) { Unit.UNITS.set(unit.name, unit); } // Check if the unit is a new base unit const baseName = unit.baseName ? unit.baseName : unit.name; const baseUnitName = Unit.UNIT_BASES.get(baseName); if (!baseUnitName) { Unit.UNIT_BASES.set(baseName, unit.name); } else { // Confirm that the unit can be converted to a base unit const baseUnit = Unit.findByName(baseUnitName, baseName); const fromBase = baseUnit.createDefinition(unit); const toBase = unit.createBaseDefinition(); if (!fromBase) { // No conversion definition unit._definitions.set(baseUnitName, toBase); } } return unit; } }