UNPKG

@itwin/core-frontend

Version:
911 lines 58.3 kB
/*--------------------------------------------------------------------------------------------- * 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