@itwin/core-frontend
Version:
iTwin.js frontend components
911 lines • 58.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module QuantityFormatting
*/
import { BeEvent, BentleyError, BeUiEvent, Logger } from "@itwin/core-bentley";
import { Format, FormatterSpec, ParseError, ParserSpec, } from "@itwin/core-quantity";
import { FrontendLoggerCategory } from "../common/FrontendLoggerCategory";
import { IModelApp } from "../IModelApp";
import { BasicUnitsProvider, getDefaultAlternateUnitLabels } from "./BasicUnitsProvider";
// cSpell:ignore FORMATPROPS FORMATKEY ussurvey uscustomary USCUSTOM
/**
* Defines standard format types for tools that need to display measurements to user.
* @public
*/
export var QuantityType;
(function (QuantityType) {
/** Length which is stored in meters. Typically formatted to display in meters or feet-inches based on active unit system. */
QuantityType[QuantityType["Length"] = 1] = "Length";
/** Angular value which is stored in radians. Typically formatted to display degrees or Degrees-Minute-Seconds based on active unit system. */
QuantityType[QuantityType["Angle"] = 2] = "Angle";
/** Area value store in meters squared. Typically formatted to display in meters squared or feet squared based on active unit system. */
QuantityType[QuantityType["Area"] = 3] = "Area";
/** Volume value which is stored in meters cubed. Typically formatted to display in meters cubed or feet cubed based on active unit system. */
QuantityType[QuantityType["Volume"] = 4] = "Volume";
/** LatLong is an angular value which is stored in radians. Typically formatted to display degrees or Degrees-Minute-Seconds based on active unit system. */
QuantityType[QuantityType["LatLong"] = 5] = "LatLong";
/** Coordinate/Location value which is stored in meters. Typically formatted to display in meters or feet based on active unit system. */
QuantityType[QuantityType["Coordinate"] = 6] = "Coordinate";
/** Stationing is a distance value stored in meters. Typically formatted to display `xxx+xx` or `xx+xxx` based on active unit system. */
QuantityType[QuantityType["Stationing"] = 7] = "Stationing";
/** LengthSurvey is a distance value stored in meters. Typically formatted to display in meters or US Survey Feet based on active unit system.. */
QuantityType[QuantityType["LengthSurvey"] = 8] = "LengthSurvey";
/** LengthEngineering is a distance value stored in meters. Typically formatted to display either meters or feet based on active unit system. */
QuantityType[QuantityType["LengthEngineering"] = 9] = "LengthEngineering";
})(QuantityType || (QuantityType = {}));
/**
* Class that contains alternate Unit Labels. These labels are used when parsing strings to quantities.
* One use case is to allow a "^", which is easily input, to be used to specify "°".
* @internal
*/
export class AlternateUnitLabelsRegistry {
_alternateLabelRegistry = new Map();
addAlternateLabels(key, ...labels) {
[...labels].forEach((value) => this._alternateLabelRegistry.get(key)?.add(value));
}
constructor(defaultAlternates) {
if (defaultAlternates) {
this._alternateLabelRegistry = defaultAlternates;
}
}
getAlternateUnitLabels(unit) {
const key = unit.name;
const labels = this._alternateLabelRegistry.get(key);
if (labels)
return [...labels.values()];
return undefined;
}
}
/**
* Function to return a QuantityTypeKey given either a QuantityType enum value or a string. This allows caching and
* retrieving standard and custom quantity types.
* @public
*/
export function getQuantityTypeKey(type) {
// For QuantityType enum values, build a string that shouldn't collide with anything a user may come up with
if (typeof type === "number")
return `QuantityTypeEnumValue-${type.toString()}`;
return type;
}
/** CustomQuantityTypeDefinition type guard.
* @public
*/
export function isCustomQuantityTypeDefinition(item) {
return !!item.isCompatibleFormatProps;
}
/** private class to hold standard quantity definitions as defined by QuantityType enum and implement QuantityTypeDefinition interface */
class StandardQuantityTypeDefinition {
type;
persistenceUnit;
_labelKey;
_descriptionKey;
_label;
_description;
_key;
constructor(type, persistenceUnit, _labelKey, _descriptionKey) {
this.type = type;
this.persistenceUnit = persistenceUnit;
this._labelKey = _labelKey;
this._descriptionKey = _descriptionKey;
this._key = getQuantityTypeKey(type);
}
get key() { return this._key; }
get label() {
if (!this._label) {
this._label = IModelApp.localization.getLocalizedString(this._labelKey);
}
return this._label ?? "";
}
get description() {
if (!this._description) {
this._description = IModelApp.localization.getLocalizedString(this._descriptionKey);
}
return this._description ?? this.label;
}
/** Get a default format to show quantity in persistence unit with precision or 6 decimal places. */
getDefaultFormatPropsBySystem(requestedSystem) {
// Fallback same as Format "DefaultRealU" in Formats ecschema
const fallbackProps = {
formatTraits: ["keepSingleZero", "keepDecimalPoint", "showUnitLabel"],
precision: 6,
type: "Decimal",
uomSeparator: " ",
decimalSeparator: ".",
};
const defaultUnitSystemData = DEFAULT_FORMATKEY_BY_UNIT_SYSTEM.find((value) => value.system === requestedSystem);
if (defaultUnitSystemData) {
const defaultFormatEntry = defaultUnitSystemData.entries.find((value) => value.type === this.key);
if (defaultFormatEntry) {
const defaultFormatPropsEntry = DEFAULT_FORMATPROPS.find((props) => props.key === defaultFormatEntry.formatKey);
if (defaultFormatPropsEntry)
return defaultFormatPropsEntry.format;
}
}
return fallbackProps;
}
async generateFormatterSpec(formatProps, unitsProvider) {
const format = await Format.createFromJSON(this.key, unitsProvider, formatProps);
return FormatterSpec.create(format.name, format, unitsProvider, this.persistenceUnit);
}
async generateParserSpec(formatProps, unitsProvider, alternateUnitLabelsProvider) {
const format = await Format.createFromJSON(this.key, unitsProvider, formatProps);
return ParserSpec.create(format, unitsProvider, this.persistenceUnit, alternateUnitLabelsProvider);
}
}
/**
* A default formatsProvider, that provides a limited set of [[FormatDefinition]], associated to a few [[KindOfQuantity]].
* Maps each KindOfQuantity to a [[QuantityType]].
* When retrieving a valid [[KindOfQuantity]], returns the [[FormatProps]] for the associated [[QuantityType]].
* @internal
*/
export class QuantityTypeFormatsProvider {
onFormatsChanged = new BeEvent();
constructor() {
IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener(() => {
this.onFormatsChanged.raiseEvent({ formatsChanged: "all" });
});
}
_kindOfQuantityMap = new Map([
["AecUnits.LENGTH", QuantityType.Length],
["AecUnits.ANGLE", QuantityType.Angle],
["AecUnits.AREA", QuantityType.Area],
["AecUnits.VOLUME", QuantityType.Volume],
["AecUnits.LENGTH_COORDINATE", QuantityType.Coordinate],
["RoadRailUnits.STATION", QuantityType.Stationing],
["RoadRailUnits.LENGTH", QuantityType.LengthSurvey],
]);
async getFormat(name) {
const quantityType = this._kindOfQuantityMap.get(name);
if (!quantityType)
return undefined;
return IModelApp.quantityFormatter.getFormatPropsByQuantityType(quantityType);
}
}
/**
* An implementation of the [[FormatsProvider]] interface that forwards calls to getFormats to the underlying FormatsProvider.
* Also fires the onFormatsChanged event when the underlying FormatsProvider fires its own onFormatsChanged event.
* @internal
*/
export class FormatsProviderManager {
_formatsProvider;
onFormatsChanged = new BeEvent();
constructor(_formatsProvider) {
this._formatsProvider = _formatsProvider;
this._formatsProvider.onFormatsChanged.addListener((args) => {
this.onFormatsChanged.raiseEvent(args);
});
}
async getFormat(name) {
return this._formatsProvider.getFormat(name);
}
get formatsProvider() { return this; }
set formatsProvider(formatsProvider) {
this._formatsProvider = formatsProvider;
this._formatsProvider.onFormatsChanged.addListener((args) => {
this.onFormatsChanged.raiseEvent(args);
});
this.onFormatsChanged.raiseEvent({ formatsChanged: "all" });
}
}
/** Class that supports formatting quantity values into strings and parsing strings into quantity values. This class also maintains
* the "active" unit system and caches FormatterSpecs and ParserSpecs for the "active" unit system to allow synchronous access to
* parsing and formatting values. The support unit systems are defined by [[UnitSystemKey]] and is kept in synch with the unit systems
* provided by the Presentation Manager on the backend. The QuantityFormatter contains a registry of quantity type definitions. These definitions implement
* the [[QuantityTypeDefinition]] interface, which among other things, provide default [[FormatProps]], and provide methods
* to generate both a [[FormatterSpec]] and a [[ParserSpec]]. There are built-in quantity types that are
* identified by the [[QuantityType]] enum. [[CustomQuantityTypeDefinition]] can be registered to extend the available quantity types available
* by frontend tools. The QuantityFormatter also allows the default formats to be overriden.
*
* @public
*/
export class QuantityFormatter {
_unitsProvider = new BasicUnitsProvider();
_alternateUnitLabelsRegistry = new AlternateUnitLabelsRegistry(getDefaultAlternateUnitLabels());
/** Registry containing available quantity type definitions. */
_quantityTypeRegistry = new Map();
/** Registry containing available FormatterSpec and ParserSpec, mapped by keys.
* @beta
*/
_formatSpecsRegistry = new Map();
/** Active UnitSystem key - must be one of "imperial", "metric", "usCustomary", or "usSurvey". */
_activeUnitSystem = "imperial";
/** Map of FormatSpecs for all available QuantityTypes and the active Unit System */
_activeFormatSpecsByType = new Map();
/** Map of ParserSpecs for all available QuantityTypes and the active Unit System */
_activeParserSpecsByType = new Map();
/** Map of FormatSpecs that have been overriden from the default. */
_overrideFormatPropsByUnitSystem = new Map();
/** Optional object that gets called to store and retrieve format overrides. */
_unitFormattingSettingsProvider;
/** Set the settings provider and if not iModel specific initialize setting for user. */
async setUnitFormattingSettingsProvider(provider) {
this._unitFormattingSettingsProvider = provider;
if (!provider.maintainOverridesPerIModel)
await provider.loadOverrides(undefined);
}
/** Called after the active unit system is changed.
* The system will report the UnitSystemKey/name of the the system that was activated.
*/
onActiveFormattingUnitSystemChanged = new BeUiEvent();
/** Called when the format of a QuantityType is overriden or the override is cleared. The string returned will
* be a QuantityTypeKey generated by method `getQuantityTypeKey`.
*/
onQuantityFormatsChanged = new BeUiEvent();
/** Fired when the active UnitsProvider is updated. This will allow cached Formatter and Parser specs to be updated if necessary. */
onUnitsProviderChanged = new BeUiEvent();
_removeFormatsProviderListener;
/**
* constructor
* @param showMetricOrUnitSystem - Pass in `true` to show Metric formatted quantity values. Defaults to Imperial. To explicitly
* set it to a specific unit system pass a UnitSystemKey.
*/
constructor(showMetricOrUnitSystem) {
if (undefined !== showMetricOrUnitSystem) {
if (typeof showMetricOrUnitSystem === "boolean")
this._activeUnitSystem = showMetricOrUnitSystem ? "metric" : "imperial";
else
this._activeUnitSystem = showMetricOrUnitSystem;
}
}
[Symbol.dispose]() {
if (this._removeFormatsProviderListener) {
this._removeFormatsProviderListener();
this._removeFormatsProviderListener = undefined;
}
}
getOverrideFormatPropsByQuantityType(quantityTypeKey, unitSystem) {
const requestedUnitSystem = unitSystem ?? this.activeUnitSystem;
const overrideMap = this._overrideFormatPropsByUnitSystem.get(requestedUnitSystem);
if (!overrideMap)
return undefined;
return overrideMap.get(quantityTypeKey);
}
/** Method used to register all QuantityTypes defined in QuantityType enum. */
async initializeQuantityTypesRegistry() {
// QuantityType.Length
const lengthUnit = await this.findUnitByName("Units.M");
const lengthDefinition = new StandardQuantityTypeDefinition(QuantityType.Length, lengthUnit, "iModelJs:QuantityType.Length.label", "iModelJs:QuantityType.Length.description");
this._quantityTypeRegistry.set(lengthDefinition.key, lengthDefinition);
// QuantityType.LengthEngineering
const lengthEngineeringDefinition = new StandardQuantityTypeDefinition(QuantityType.LengthEngineering, lengthUnit, "iModelJs:QuantityType.LengthEngineering.label", "iModelJs:QuantityType.LengthEngineering.description");
this._quantityTypeRegistry.set(lengthEngineeringDefinition.key, lengthEngineeringDefinition);
// QuantityType.Coordinate
const coordinateDefinition = new StandardQuantityTypeDefinition(QuantityType.Coordinate, lengthUnit, "iModelJs:QuantityType.Coordinate.label", "iModelJs:QuantityType.Coordinate.description");
this._quantityTypeRegistry.set(coordinateDefinition.key, coordinateDefinition);
// QuantityType.Stationing
const stationingDefinition = new StandardQuantityTypeDefinition(QuantityType.Stationing, lengthUnit, "iModelJs:QuantityType.Stationing.label", "iModelJs:QuantityType.Stationing.description");
this._quantityTypeRegistry.set(stationingDefinition.key, stationingDefinition);
// QuantityType.LengthSurvey
const lengthSurveyDefinition = new StandardQuantityTypeDefinition(QuantityType.LengthSurvey, lengthUnit, "iModelJs:QuantityType.LengthSurvey.label", "iModelJs:QuantityType.LengthSurvey.description");
this._quantityTypeRegistry.set(lengthSurveyDefinition.key, lengthSurveyDefinition);
// QuantityType.Angle
const radUnit = await this.findUnitByName("Units.RAD");
const angleDefinition = new StandardQuantityTypeDefinition(QuantityType.Angle, radUnit, "iModelJs:QuantityType.Angle.label", "iModelJs:QuantityType.Angle.description");
this._quantityTypeRegistry.set(angleDefinition.key, angleDefinition);
// QuantityType.LatLong
const latLongDefinition = new StandardQuantityTypeDefinition(QuantityType.LatLong, radUnit, "iModelJs:QuantityType.LatLong.label", "iModelJs:QuantityType.LatLong.description");
this._quantityTypeRegistry.set(latLongDefinition.key, latLongDefinition);
// QuantityType.Area
const sqMetersUnit = await this.findUnitByName("Units.SQ_M");
const areaDefinition = new StandardQuantityTypeDefinition(QuantityType.Area, sqMetersUnit, "iModelJs:QuantityType.Area.label", "iModelJs:QuantityType.Area.description");
this._quantityTypeRegistry.set(areaDefinition.key, areaDefinition);
// QuantityType.Volume
const cubicMetersUnit = await this.findUnitByName("Units.CUB_M");
const volumeDefinition = new StandardQuantityTypeDefinition(QuantityType.Volume, cubicMetersUnit, "iModelJs:QuantityType.Volume.label", "iModelJs:QuantityType.Volume.description");
this._quantityTypeRegistry.set(volumeDefinition.key, volumeDefinition);
}
/** Asynchronous call to load Formatting and ParsingSpecs for a unit system. This method ends up caching FormatterSpecs and ParserSpecs
* so they can be quickly accessed.
* @internal public for unit test usage
*/
async loadFormatAndParsingMapsForSystem(systemType) {
const systemKey = (undefined !== systemType) ? systemType : this._activeUnitSystem;
const formatPropsByType = new Map();
// load cache for every registered QuantityType
[...this.quantityTypesRegistry.keys()].forEach((key) => {
const entry = this.quantityTypesRegistry.get(key);
formatPropsByType.set(entry, this.getFormatPropsByQuantityTypeEntryAndSystem(entry, systemKey));
});
for (const [entry, formatProps] of formatPropsByType) {
await this.loadFormatAndParserSpec(entry, formatProps);
}
}
getFormatPropsByQuantityTypeEntryAndSystem(quantityEntry, requestedSystem, ignoreOverrides) {
if (!ignoreOverrides) {
const overrideProps = this.getOverrideFormatPropsByQuantityType(quantityEntry.key, requestedSystem);
if (overrideProps)
return overrideProps;
}
return quantityEntry.getDefaultFormatPropsBySystem(requestedSystem);
}
async loadFormatAndParserSpec(quantityTypeDefinition, formatProps) {
const formatterSpec = await quantityTypeDefinition.generateFormatterSpec(formatProps, this.unitsProvider);
const parserSpec = await quantityTypeDefinition.generateParserSpec(formatProps, this.unitsProvider, this.alternateUnitLabelsProvider);
this._activeFormatSpecsByType.set(quantityTypeDefinition.key, formatterSpec);
this._activeParserSpecsByType.set(quantityTypeDefinition.key, parserSpec);
}
// repopulate formatSpec and parserSpec entries using only default format
async loadDefaultFormatAndParserSpecForQuantity(typeKey) {
const quantityTypeDefinition = this.quantityTypesRegistry.get(typeKey);
if (!quantityTypeDefinition)
throw new Error(`Unable to locate QuantityType by key ${typeKey}`);
const defaultFormat = quantityTypeDefinition.getDefaultFormatPropsBySystem(this.activeUnitSystem);
await this.loadFormatAndParserSpec(quantityTypeDefinition, defaultFormat);
}
async setOverrideFormatsByQuantityTypeKey(typeKey, overrideEntry) {
// extract overrides and insert into appropriate override map entry
Object.keys(overrideEntry).forEach((systemKey) => {
const unitSystemKey = systemKey;
const props = overrideEntry[unitSystemKey];
if (props) {
if (this._overrideFormatPropsByUnitSystem.has(unitSystemKey)) {
this._overrideFormatPropsByUnitSystem.get(unitSystemKey).set(typeKey, props);
}
else {
const newMap = new Map();
newMap.set(typeKey, props);
this._overrideFormatPropsByUnitSystem.set(unitSystemKey, newMap);
}
}
});
await this._unitFormattingSettingsProvider?.storeFormatOverrides({ typeKey, overrideEntry });
const formatProps = this.getOverrideFormatPropsByQuantityType(typeKey, this.activeUnitSystem);
if (formatProps) {
const typeEntry = this.quantityTypesRegistry.get(typeKey);
if (typeEntry) {
await this.loadFormatAndParserSpec(typeEntry, formatProps);
// trigger a message to let callers know the format has changed.
this.onQuantityFormatsChanged.emit({ quantityType: typeKey });
}
}
}
/** Method called to clear override and restore defaults formatter and parser spec */
async clearOverrideFormatsByQuantityTypeKey(type) {
const unitSystem = this.activeUnitSystem;
if (this.getOverrideFormatPropsByQuantityType(type, unitSystem)) {
const overrideMap = this._overrideFormatPropsByUnitSystem.get(unitSystem);
if (overrideMap && overrideMap.has(type)) {
overrideMap.delete(type);
await this._unitFormattingSettingsProvider?.storeFormatOverrides({ typeKey: type, unitSystem });
await this.loadDefaultFormatAndParserSpecForQuantity(type);
// trigger a message to let callers know the format has changed.
this.onQuantityFormatsChanged.emit({ quantityType: type });
}
}
}
/** This method is called during IModelApp initialization to load the standard quantity types into the registry and to initialize the cache.
* @internal
*/
async onInitialized() {
await this.initializeQuantityTypesRegistry();
const initialKoQs = [["AecUnits.LENGTH", "Units.M"], ["AecUnits.ANGLE", "Units.RAD"], ["AecUnits.AREA", "Units.SQ_M"], ["AecUnits.VOLUME", "Units.CUB_M"], ["AecUnits.LENGTH_COORDINATE", "Units.M"], ["RoadRailUnits.STATION", "Units.M"], ["RoadRailUnits.LENGTH", "Units.M"]];
for (const entry of initialKoQs) {
try {
await this.addFormattingSpecsToRegistry(entry[0], entry[1]);
}
catch (err) {
Logger.logWarning(`${FrontendLoggerCategory.Package}.QuantityFormatter`, err.toString());
}
}
this._removeFormatsProviderListener = IModelApp.formatsProvider.onFormatsChanged.addListener(async (args) => {
if (args.formatsChanged === "all") {
for (const [name, entry] of this._formatSpecsRegistry.entries()) {
const formatProps = await IModelApp.formatsProvider.getFormat(name);
if (formatProps) {
const persistenceUnitName = entry.formatterSpec.persistenceUnit.name;
await this.addFormattingSpecsToRegistry(name, persistenceUnitName, formatProps);
}
else {
this._formatSpecsRegistry.delete(name); // clear the specs if format was removed, or no longer exists.
}
}
}
else {
for (const name of args.formatsChanged) {
if (this._formatSpecsRegistry.has(name)) {
const formatProps = await IModelApp.formatsProvider.getFormat(name);
if (formatProps) {
const existingEntry = this._formatSpecsRegistry.get(name);
if (existingEntry) {
const persistenceUnitName = existingEntry.formatterSpec.persistenceUnit.name;
await this.addFormattingSpecsToRegistry(name, persistenceUnitName, formatProps);
}
}
else {
this._formatSpecsRegistry.delete(name);
}
}
}
}
});
// initialize default format and parsing specs
await this.loadFormatAndParsingMapsForSystem();
}
/** Return a map that serves as a registry of all standard and custom quantity types. */
get quantityTypesRegistry() {
return this._quantityTypeRegistry;
}
/** Return the class the contain map of all alternate labels for units. These alternate labels are used when parsing strings in quantity values. */
get alternateUnitLabelsProvider() {
return this._alternateUnitLabelsRegistry;
}
/**
* Add one or more alternate labels for a unit - these labels are used during string parsing.
* @param key UnitNameKey which comes from `UnitProps.name`
* @param labels one or more unit labels
*/
addAlternateLabels(key, ...labels) {
this._alternateUnitLabelsRegistry.addAlternateLabels(key, ...labels);
this.onUnitsProviderChanged.emit();
}
/** Get/Set the active UnitsProvider class. */
get unitsProvider() {
return this._unitsProvider;
}
set unitsProvider(unitsProvider) {
this.setUnitsProvider(unitsProvider); // eslint-disable-line @typescript-eslint/no-floating-promises
}
/** async method to set a units provider and reload caches */
async setUnitsProvider(unitsProvider) {
this._unitsProvider = unitsProvider;
try {
// force all cached data to be reinitialized
await IModelApp.quantityFormatter.onInitialized();
}
catch (err) {
Logger.logWarning(`${FrontendLoggerCategory.Package}.quantityFormatter`, BentleyError.getErrorMessage(err), BentleyError.getErrorMetadata(err));
Logger.logWarning(`${FrontendLoggerCategory.Package}.quantityFormatter`, "An exception occurred initializing the iModelApp.quantityFormatter with the given UnitsProvider. Defaulting back to the internal units provider.");
// If there is a problem initializing with the given provider, default back to the internal provider
await IModelApp.quantityFormatter.resetToUseInternalUnitsProvider();
return;
}
// force default tool to start so any tool that may be using cached data will not be using bad data.
if (IModelApp.toolAdmin)
await IModelApp.toolAdmin.startDefaultTool();
this.onUnitsProviderChanged.emit();
}
/** Async call typically used after IModel is closed to reset UnitsProvider to default one that does not require an Units schema. */
async resetToUseInternalUnitsProvider() {
if (this._unitsProvider instanceof BasicUnitsProvider)
return;
await this.setUnitsProvider(new BasicUnitsProvider());
}
/** Async call to register a CustomQuantityType and load the FormatSpec and ParserSpec for the new type. */
async registerQuantityType(entry, replace) {
if (!replace && this._quantityTypeRegistry.has(entry.key))
return false;
this._quantityTypeRegistry.set(entry.key, entry);
// load any overrides so any saved overrides for the type being registered are applied
if (this._unitFormattingSettingsProvider)
await this._unitFormattingSettingsProvider.loadOverrides(undefined);
if (entry.getDefaultFormatPropsBySystem) {
const formatProps = entry.getDefaultFormatPropsBySystem(this.activeUnitSystem);
await this.loadFormatAndParserSpec(entry, formatProps);
return true;
}
return false;
}
/** Reinitialize caches. Typically called by active UnitFormattingSettingsProvider.
* startDefaultTool - set to true to start the Default to instead of leaving any active tool pointing to cached unit data that is no longer valid
* @public
*/
async reinitializeFormatAndParsingsMaps(overrideFormatPropsByUnitSystem, unitSystemKey, fireUnitSystemChanged, startDefaultTool) {
this._overrideFormatPropsByUnitSystem.clear();
if (overrideFormatPropsByUnitSystem.size) {
this._overrideFormatPropsByUnitSystem = overrideFormatPropsByUnitSystem;
}
unitSystemKey && (this._activeUnitSystem = unitSystemKey);
await this.loadFormatAndParsingMapsForSystem(this._activeUnitSystem);
fireUnitSystemChanged && this.onActiveFormattingUnitSystemChanged.emit({ system: this._activeUnitSystem });
IModelApp.toolAdmin && startDefaultTool && await IModelApp.toolAdmin.startDefaultTool();
}
/** Set the Active unit system to one of the supported types. This will asynchronously load the formatter and parser specs for the activated system. */
async setActiveUnitSystem(isImperialOrUnitSystem, restartActiveTool) {
let systemType;
if (typeof isImperialOrUnitSystem === "boolean")
systemType = isImperialOrUnitSystem ? "imperial" : "metric";
else
systemType = isImperialOrUnitSystem;
if (this._activeUnitSystem === systemType)
return;
this._activeUnitSystem = systemType;
await this.loadFormatAndParsingMapsForSystem(systemType);
// allow settings provider to store the change
await this._unitFormattingSettingsProvider?.storeUnitSystemSetting({ system: systemType });
// fire current event
this.onActiveFormattingUnitSystemChanged.emit({ system: systemType });
if (IModelApp.toolAdmin && restartActiveTool)
return IModelApp.toolAdmin.startDefaultTool();
}
/** Retrieve the active [[UnitSystemKey]] which is used to determine what formats are to be used to display quantities */
get activeUnitSystem() { return this._activeUnitSystem; }
/** Clear any formatting override for specified quantity type, but only for the "active" Unit System. */
async clearOverrideFormats(type) {
await this.clearOverrideFormatsByQuantityTypeKey(this.getQuantityTypeKey(type));
}
/** Set formatting override for specified quantity type, but only for the "active" Unit System. */
async setOverrideFormats(type, overrideEntry) {
await this.setOverrideFormatsByQuantityTypeKey(this.getQuantityTypeKey(type), overrideEntry);
}
/** Set Override Format for a quantity type, but only in the "active" Unit System. */
async setOverrideFormat(type, overrideFormat) {
const typeKey = this.getQuantityTypeKey(type);
let overrideEntry = {};
if (this.activeUnitSystem === "imperial")
overrideEntry = { imperial: overrideFormat };
else if (this.activeUnitSystem === "metric")
overrideEntry = { metric: overrideFormat };
else if (this.activeUnitSystem === "usCustomary")
overrideEntry = { usCustomary: overrideFormat };
else
overrideEntry = { usSurvey: overrideFormat };
await this.setOverrideFormatsByQuantityTypeKey(typeKey, overrideEntry);
}
/** Clear formatting override for all quantity types, but only for the "active" Unit System. */
async clearAllOverrideFormats() {
if (0 === this._overrideFormatPropsByUnitSystem.size)
return;
if (this._overrideFormatPropsByUnitSystem.has(this.activeUnitSystem)) {
const overrides = this._overrideFormatPropsByUnitSystem.get(this.activeUnitSystem);
const typesRemoved = [];
if (overrides && overrides.size) {
const promises = new Array();
overrides.forEach((_props, typeKey) => {
typesRemoved.push(typeKey);
promises.push(this._unitFormattingSettingsProvider?.storeFormatOverrides({ typeKey, unitSystem: this.activeUnitSystem }));
});
await Promise.all(promises);
}
if (typesRemoved.length) {
const promises = new Array();
typesRemoved.forEach((typeRemoved) => promises.push(this.loadDefaultFormatAndParserSpecForQuantity(typeRemoved)));
await Promise.all(promises);
// trigger a message to let callers know the format has changed.
this.onQuantityFormatsChanged.emit({ quantityType: typesRemoved.join("|") });
}
}
}
/** Converts a QuantityTypeArg into a QuantityTypeKey/string value that can be used to lookup custom and standard quantity types. */
getQuantityTypeKey(type) {
return getQuantityTypeKey(type);
}
/** Return [[QuantityTypeDefinition]] if type has been registered. Standard QuantityTypes are automatically registered. */
getQuantityDefinition(type) {
return this.quantityTypesRegistry.get(this.getQuantityTypeKey(type));
}
/** Synchronous call to get a FormatterSpec of a QuantityType. If the FormatterSpec is not yet cached an undefined object is returned. The
* cache is populated by the async call loadFormatAndParsingMapsForSystem.
*/
findFormatterSpecByQuantityType(type, _unused) {
return this._activeFormatSpecsByType.get(this.getQuantityTypeKey(type));
}
/** Asynchronous Call to get a FormatterSpec for a QuantityType. This formatter spec can be used to synchronously format quantities. */
async generateFormatterSpecByType(type, formatProps) {
const quantityTypeDefinition = this.quantityTypesRegistry.get(this.getQuantityTypeKey(type));
if (quantityTypeDefinition)
return quantityTypeDefinition.generateFormatterSpec(formatProps, this.unitsProvider);
throw new Error(`Unable to generate FormatSpec for QuantityType ${type}`);
}
/** Asynchronous Call to get a FormatterSpec for a QuantityType and a Unit System. This formatter spec can be used to synchronously format quantities.
* @param type One of the built-in quantity types supported.
* @param system Requested unit system key. Note it is more efficient to use setActiveUnitSystem to set up formatters for all
* quantity types of a unit system.
* @return A FormatterSpec Promise.
*/
async getFormatterSpecByQuantityTypeAndSystem(type, system) {
const quantityKey = this.getQuantityTypeKey(type);
const requestedSystem = system ?? this.activeUnitSystem;
if (requestedSystem === this.activeUnitSystem) {
const formatterSpec = this._activeFormatSpecsByType.get(quantityKey);
if (formatterSpec)
return formatterSpec;
}
const entry = this.quantityTypesRegistry.get(quantityKey);
if (!entry)
throw new Error(`Unable to find registered quantity type with key ${quantityKey}`);
return entry.generateFormatterSpec(this.getFormatPropsByQuantityTypeEntryAndSystem(entry, requestedSystem), this.unitsProvider);
}
/** Asynchronous Call to get a FormatterSpec for a QuantityType.
* @param type One of the built-in quantity types supported.
* @param isImperial Argument to specify use of imperial or metric unit system. If left undefined the active unit system is used.
* @return A FormatterSpec Promise.
*/
async getFormatterSpecByQuantityType(type, isImperial) {
let requestedSystem = this.activeUnitSystem;
if (undefined !== isImperial)
requestedSystem = isImperial ? "imperial" : "metric";
return this.getFormatterSpecByQuantityTypeAndSystem(type, requestedSystem);
}
/** Synchronous call to get a ParserSpec for a QuantityType. If the ParserSpec is not yet cached an undefined object is returned. The
* cache is populated when the active units system is set.
*/
findParserSpecByQuantityType(type) {
return this._activeParserSpecsByType.get(this.getQuantityTypeKey(type));
}
/** Asynchronous Call to get a ParserSpec for a QuantityType. If the UnitSystemKey is not specified the active Unit System is used. **/
async getParserSpecByQuantityTypeAndSystem(type, system) {
const quantityKey = this.getQuantityTypeKey(type);
const requestedSystem = system ?? this.activeUnitSystem;
if (requestedSystem === this.activeUnitSystem) {
const parserSpec = this._activeParserSpecsByType.get(quantityKey);
if (parserSpec)
return parserSpec;
}
const entry = this.quantityTypesRegistry.get(quantityKey);
if (!entry)
throw new Error(`Unable to find registered quantity type with key ${quantityKey}`);
return entry.generateParserSpec(this.getFormatPropsByQuantityTypeEntryAndSystem(entry, requestedSystem), this.unitsProvider);
}
/** Asynchronous Call to get a ParserSpec for a QuantityType.
* @param type One of the built-in quantity types supported.
* @param isImperial Argument to specify use of imperial or metric unit system. If left undefined the active unit system is used.
* @return A FormatterSpec Promise.
*/
async getParserSpecByQuantityType(type, isImperial) {
let requestedSystem = this.activeUnitSystem;
if (undefined !== isImperial)
requestedSystem = isImperial ? "imperial" : "metric";
return this.getParserSpecByQuantityTypeAndSystem(type, requestedSystem);
}
formatQuantity(args, spec) {
if (typeof args === "number") {
/** Format a quantity value. Default FormatterSpec implementation uses Formatter.formatQuantity. */
const magnitude = args;
if (spec)
return spec.applyFormatting(magnitude);
return magnitude.toString();
}
return this.formatQuantityAsync(args);
}
async formatQuantityAsync(args) {
const { value, valueUnitName, kindOfQuantityName } = args;
const formatProps = await IModelApp.formatsProvider.getFormat(kindOfQuantityName);
if (!formatProps)
return value.toString();
const formatSpec = await this.createFormatterSpec({
persistenceUnitName: valueUnitName,
formatProps,
formatName: kindOfQuantityName,
});
return formatSpec.applyFormatting(value);
}
parseToQuantityValue(args, parserSpec) {
if (typeof args === "string") {
/** Parse a quantity value. Default ParserSpec implementation uses ParserSpec.parseToQuantityValue. */
const inString = args;
if (parserSpec)
return parserSpec.parseToQuantityValue(inString);
return { ok: false, error: ParseError.InvalidParserSpec };
}
return this.parseToQuantityValueAsync(args);
}
async parseToQuantityValueAsync(args) {
const { value, valueUnitName, kindOfQuantityName } = args;
const formatProps = await IModelApp.formatsProvider.getFormat(kindOfQuantityName);
if (!formatProps)
return { ok: false, error: ParseError.InvalidParserSpec };
const parserSpec = await this.createParserSpec({
persistenceUnitName: valueUnitName,
formatProps,
formatName: kindOfQuantityName,
});
return parserSpec.parseToQuantityValue(value);
}
/**
* Get a UnitSystemKey from a string that may have been entered via a key-in. Supports different variation of
* unit system names that have been used in the past.
*/
getUnitSystemFromString(inputSystem, fallback) {
switch (inputSystem.toLowerCase()) {
case "metric":
case "si":
return "metric";
case "imperial":
case "british-imperial":
return "imperial";
case "uscustomary":
case "us-customary":
case "us":
return "usCustomary";
case "ussurvey":
case "us-survey":
case "survey":
return "usSurvey";
default:
if (undefined !== fallback)
return fallback;
break;
}
return "imperial";
}
/** Return true if the QuantityType is using an override format. */
hasActiveOverride(type, checkOnlyActiveUnitSystem) {
const quantityTypeKey = this.getQuantityTypeKey(type);
if (checkOnlyActiveUnitSystem) {
const overrides = this._overrideFormatPropsByUnitSystem.get(this.activeUnitSystem);
if (overrides && overrides.has(quantityTypeKey))
return true;
return false;
}
for (const [_key, overrideMap] of this._overrideFormatPropsByUnitSystem) {
if (overrideMap.has(quantityTypeKey))
return true;
}
return false;
}
/** Get the cached FormatProps give a quantity type. If ignoreOverrides is false then if the format has been overridden
* the overridden format is returned, else the standard format is returned.
*/
getFormatPropsByQuantityType(quantityType, requestedSystem, ignoreOverrides) {
const quantityEntry = this.quantityTypesRegistry.get(this.getQuantityTypeKey(quantityType));
if (quantityEntry)
return this.getFormatPropsByQuantityTypeEntryAndSystem(quantityEntry, requestedSystem ?? this.activeUnitSystem, ignoreOverrides);
return undefined;
}
// keep following to maintain existing API of implementing UnitsProvider
/** Find [UnitProp] for a specific unit label. */
async findUnit(unitLabel, schemaName, phenomenon, unitSystem) {
return this._unitsProvider.findUnit(unitLabel, schemaName, phenomenon, unitSystem);
}
/** Returns all defined units for the specified Unit Family/Phenomenon. */
async getUnitsByFamily(phenomenon) {
return this._unitsProvider.getUnitsByFamily(phenomenon);
}
/** Find [UnitProp] for a specific unit name. */
async findUnitByName(unitName) {
return this._unitsProvider.findUnitByName(unitName);
}
/** Returns data needed to convert from one Unit to another in the same Unit Family/Phenomenon. */
async getConversion(fromUnit, toUnit) {
return this._unitsProvider.getConversion(fromUnit, toUnit);
}
/**
* Creates a [[FormatterSpec]] for a given persistence unit name and format properties, using the [[UnitsProvider]] to resolve the persistence unit.
* @beta
* @param props - A [[CreateFormattingSpecProps]] interface.
*/
async createFormatterSpec(props) {
const { persistenceUnitName, formatProps, formatName } = props;
const persistenceUnitProps = await this._unitsProvider.findUnitByName(persistenceUnitName);
const format = await Format.createFromJSON(formatName ?? "temp", this._unitsProvider, formatProps);
return FormatterSpec.create(`${format.name}_format_spec`, format, this._unitsProvider, persistenceUnitProps);
}
/**
* Creates a [[ParserSpec]] for a given persistence unit name and format properties, using the [[UnitsProvider]] to resolve the persistence unit.
* @beta
* @param props - A [[CreateFormattingSpecProps]] object.
*/
async createParserSpec(props) {
const { persistenceUnitName, formatProps, formatName } = props;
const persistenceUnitProps = await this._unitsProvider.findUnitByName(persistenceUnitName);
const format = await Format.createFromJSON(formatName ?? "temp", this._unitsProvider, formatProps);
return ParserSpec.create(format, this._unitsProvider, persistenceUnitProps);
}
/**
* @beta
* Returns a [[FormattingSpecEntry]] for a given name, typically a KindOfQuantity full name.
*/
getSpecsByName(name) {
return this._formatSpecsRegistry.get(name);
}
/**
* Populates the registry with a new FormatterSpec and ParserSpec entry for the given format name.
* @beta
* @param name The key used to identify the formatter and parser spec
* @param persistenceUnitName The name of the persistence unit
* @param formatProps If not supplied, tries to retrieve the [[FormatProps]] from [[IModelApp.formatsProvider]]
*/
async addFormattingSpecsToRegistry(name, persistenceUnitName, formatProps) {
if (!formatProps) {
formatProps = await IModelApp.formatsProvider.getFormat(name);
}
if (formatProps) {
const formatterSpec = await this.createFormatterSpec({
persistenceUnitName,
formatProps,
formatName: name,
});
const parserSpec = await this.createParserSpec({
persistenceUnitName,
formatProps,
formatName: name,
});
this._formatSpecsRegistry.set(name, { formatterSpec, parserSpec });
}
else {
throw new Error(`Unable to find format properties for ${name} with persistence unit ${persistenceUnitName}`);
}
}
}
// ========================================================================================================================================
// Default Data
// ========================================================================================================================================
const DEFAULT_FORMATKEY_BY_UNIT_SYSTEM = [
{
system: "metric", // PresentationUnitSystem.Metric,
entries: [
{ type: getQuantityTypeKey(QuantityType.Length), formatKey: "[units:length]meter4" },
{ type: getQuantityTypeKey(QuantityType.Angle), formatKey: "[units:angle]degree2" },
{ type: getQuantityTypeKey(QuantityType.Area), formatKey: "[units:area]mSquared4" },
{ type: getQuantityTypeKey(QuantityType.Volume), formatKey: "[units:volume]mCubed4" },
{ type: getQuantityTypeKey(QuantityType.LatLong), formatKey: "[units:angle]dms" },
{ type: getQuantityTypeKey(QuantityType.Coordinate), formatKey: "[units:length]meter2" },
{ type: getQuantityTypeKey(QuantityType.Stationing), formatKey: "[units:length]m-sta2" },
{ type: getQuantityTypeKey(QuantityType.LengthSurvey), formatKey: "[units:length]meter4" },
{ type: getQuantityTypeKey(QuantityType.LengthEngineering), formatKey: "[units:length]meter4" },
],
},
{
system: "imperial", // PresentationUnitSystem.BritishImperial,
entries: [
{ type: getQuantityTypeKey(QuantityType.Length), formatKey: "[units:length]fi8" },
{ type: getQuantityTypeKey(QuantityType.Angle), formatKey: "[units:angle]dms2" },
{ type: getQuantityTypeKey(QuantityType.Area), formatKey: "[units:area]fSquared4" },
{ type: getQuantityTypeKey(QuantityType.Volume), formatKey: "[units:volume]fCubed4" },
{ type: getQuantityTypeKey(QuantityType.LatLong), formatKey: "[units:angle]dms" },
{ type: getQuantityTypeKey(QuantityType.Coordinate), formatKey: "[units:length]feet2" },
{ type: getQuantityTypeKey(QuantityType.Stationing), formatKey: "[units:length]f-sta2" },
{ type: getQuantityTypeKey(QuantityType.LengthSurvey), formatKey: "[units:length]f-survey-4-labeled" },
{ type: getQuantityTypeKey(QuantityType.LengthEngineering), formatKey: "[units:length]feet4" },
],
},
{
system: "usCustomary", // PresentationUnitSystem.UsCustomary
entries: [
{ type: getQuantityTypeKey(QuantityType.Length), formatKey: "[units:length]fi8" },
{ type: getQuantityTypeKey(QuantityType.Angle), formatKey: "[units:angle]dms2" },
{ type: getQuantityTypeKey(QuantityType.Area), formatKey: "[units:area]fSquared4" },
{ type: getQuantityTypeKey(QuantityType.Volume), formatKey: "[units:volume]fCubed4" },
{ type: getQuantityTypeKey(QuantityType.LatLong), formatKey: "[units:angle]dms" },
{ type: getQuantityTypeKey(QuantityType.Coordinate), formatKey: "[units:length]feet2" },
{ type: getQuantityTypeKey(QuantityType.Stationing), formatKey: "[units:length]f-sta2" },
{ type: getQuantityTypeKey(QuantityType.LengthSurvey), formatKey: "[units:length]f-survey-4" },
{ type: getQuantityTypeKey(QuantityType.LengthEngineering), formatKey: "[units:length]feet4" },
],
},
{
system: "usSurvey", // PresentationUnitSystem.UsSurvey
entries: [
{ type: getQuantityTypeKey(QuantityType.Length), formatKey: "[units:length]f-survey-4" },
{ type: getQuantityTypeKey(QuantityType.Angle), formatKey: "[units:angle]dms2" },
{ type: getQuantityTypeKey(QuantityType.Area), formatKey: "[units:area]usSurveyFtSquared4" },
{ type: getQuantityTypeKey(QuantityType.Volume), formatKey: "[units:volume]usSurveyFtCubed4" },
{ type: getQuantityTypeKey(QuantityType.LatLong), formatKey: "[units:angle]dms" },
{ type: getQuantityTypeKey(QuantityType.Coordinate), formatKey: "[units:length]f-survey-2" },
{ type: getQuantityTypeKey(QuantityType.Stationing), formatKey: "[units:length]f-survey-sta2" },
{ type: getQuantityTypeKey(QuantityType.LengthSurvey), formatKey: "[units:length]f-survey-4" },
{ type: getQuantityTypeKey(QuantityType.LengthEngineering), formatKey: "[units:length]f-survey-4" },
],
},
];
/** List of default format definitions used by the Standard QuantityTypes. */
const DEFAULT_FORMATPROPS = [
{
key: "[units:length]meter4",
description: "meters (labeled) 4 decimal places",
format: {
composite: {
includeZero: true,
spacer: "",
units: [{ label: "m", name: "Units.M" }],
},
formatTraits: ["keepSingleZero", "showUnitLabel"],
precision: 4,
type: "Decimal",
},
},
{
key: "[units:length]meter2",
description: "meters (labeled) 2 decimal places",
format: {
composite: {
includeZero: true,
spacer: "",
units: [{ label: "m", name: "Units.M" }],
},
formatTraits: ["keepSingleZero", "showUnitLabel"],
precision: 2,
type: "Deci