@itwin/core-frontend 5.9.0-dev.9 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/lib/cjs/BriefcaseConnection.d.ts +2 -2
  3. package/lib/cjs/BriefcaseConnection.js +2 -2
  4. package/lib/cjs/BriefcaseConnection.js.map +1 -1
  5. package/lib/cjs/PerModelCategoryVisibility.d.ts.map +1 -1
  6. package/lib/cjs/PerModelCategoryVisibility.js +83 -81
  7. package/lib/cjs/PerModelCategoryVisibility.js.map +1 -1
  8. package/lib/cjs/SpatialViewState.d.ts.map +1 -1
  9. package/lib/cjs/SpatialViewState.js +3 -1
  10. package/lib/cjs/SpatialViewState.js.map +1 -1
  11. package/lib/cjs/ViewState.d.ts +32 -0
  12. package/lib/cjs/ViewState.d.ts.map +1 -1
  13. package/lib/cjs/ViewState.js +41 -4
  14. package/lib/cjs/ViewState.js.map +1 -1
  15. package/lib/cjs/internal/render/webgl/FrustumUniforms.d.ts.map +1 -1
  16. package/lib/cjs/internal/render/webgl/FrustumUniforms.js.map +1 -1
  17. package/lib/cjs/internal/render/webgl/RenderCommands.js.map +1 -1
  18. package/lib/cjs/quantity-formatting/AlternateUnitLabels.d.ts +8 -0
  19. package/lib/cjs/quantity-formatting/AlternateUnitLabels.d.ts.map +1 -0
  20. package/lib/cjs/quantity-formatting/AlternateUnitLabels.js +26 -0
  21. package/lib/cjs/quantity-formatting/AlternateUnitLabels.js.map +1 -0
  22. package/lib/cjs/quantity-formatting/QuantityFormatter.d.ts +119 -22
  23. package/lib/cjs/quantity-formatting/QuantityFormatter.d.ts.map +1 -1
  24. package/lib/cjs/quantity-formatting/QuantityFormatter.js +271 -65
  25. package/lib/cjs/quantity-formatting/QuantityFormatter.js.map +1 -1
  26. package/lib/cjs/tile/GltfReader.js.map +1 -1
  27. package/lib/cjs/tile/map/MapLayerFormatRegistry.js.map +1 -1
  28. package/lib/cjs/tools/PrimitiveTool.d.ts +1 -1
  29. package/lib/cjs/tools/PrimitiveTool.js +1 -1
  30. package/lib/cjs/tools/PrimitiveTool.js.map +1 -1
  31. package/lib/cjs/tools/ToolAdmin.d.ts +16 -1
  32. package/lib/cjs/tools/ToolAdmin.d.ts.map +1 -1
  33. package/lib/cjs/tools/ToolAdmin.js +52 -5
  34. package/lib/cjs/tools/ToolAdmin.js.map +1 -1
  35. package/lib/cjs/tools/ToolSettings.d.ts +4 -0
  36. package/lib/cjs/tools/ToolSettings.d.ts.map +1 -1
  37. package/lib/cjs/tools/ToolSettings.js +4 -0
  38. package/lib/cjs/tools/ToolSettings.js.map +1 -1
  39. package/lib/esm/BriefcaseConnection.d.ts +2 -2
  40. package/lib/esm/BriefcaseConnection.js +2 -2
  41. package/lib/esm/BriefcaseConnection.js.map +1 -1
  42. package/lib/esm/PerModelCategoryVisibility.d.ts.map +1 -1
  43. package/lib/esm/PerModelCategoryVisibility.js +84 -82
  44. package/lib/esm/PerModelCategoryVisibility.js.map +1 -1
  45. package/lib/esm/SpatialViewState.d.ts.map +1 -1
  46. package/lib/esm/SpatialViewState.js +3 -1
  47. package/lib/esm/SpatialViewState.js.map +1 -1
  48. package/lib/esm/ViewState.d.ts +32 -0
  49. package/lib/esm/ViewState.d.ts.map +1 -1
  50. package/lib/esm/ViewState.js +42 -5
  51. package/lib/esm/ViewState.js.map +1 -1
  52. package/lib/esm/internal/render/webgl/FrustumUniforms.d.ts.map +1 -1
  53. package/lib/esm/internal/render/webgl/FrustumUniforms.js.map +1 -1
  54. package/lib/esm/internal/render/webgl/RenderCommands.js.map +1 -1
  55. package/lib/esm/quantity-formatting/AlternateUnitLabels.d.ts +8 -0
  56. package/lib/esm/quantity-formatting/AlternateUnitLabels.d.ts.map +1 -0
  57. package/lib/esm/quantity-formatting/AlternateUnitLabels.js +23 -0
  58. package/lib/esm/quantity-formatting/AlternateUnitLabels.js.map +1 -0
  59. package/lib/esm/quantity-formatting/QuantityFormatter.d.ts +119 -22
  60. package/lib/esm/quantity-formatting/QuantityFormatter.d.ts.map +1 -1
  61. package/lib/esm/quantity-formatting/QuantityFormatter.js +269 -63
  62. package/lib/esm/quantity-formatting/QuantityFormatter.js.map +1 -1
  63. package/lib/esm/tile/GltfReader.js.map +1 -1
  64. package/lib/esm/tile/map/MapLayerFormatRegistry.js.map +1 -1
  65. package/lib/esm/tools/PrimitiveTool.d.ts +1 -1
  66. package/lib/esm/tools/PrimitiveTool.js +1 -1
  67. package/lib/esm/tools/PrimitiveTool.js.map +1 -1
  68. package/lib/esm/tools/ToolAdmin.d.ts +16 -1
  69. package/lib/esm/tools/ToolAdmin.d.ts.map +1 -1
  70. package/lib/esm/tools/ToolAdmin.js +52 -5
  71. package/lib/esm/tools/ToolAdmin.js.map +1 -1
  72. package/lib/esm/tools/ToolSettings.d.ts +4 -0
  73. package/lib/esm/tools/ToolSettings.d.ts.map +1 -1
  74. package/lib/esm/tools/ToolSettings.js +4 -0
  75. package/lib/esm/tools/ToolSettings.js.map +1 -1
  76. package/lib/public/scripts/parse-imdl-worker.js +1 -1
  77. package/lib/workers/webpack/parse-imdl-worker.js +1 -1
  78. package/package.json +20 -20
  79. package/lib/cjs/quantity-formatting/BasicUnitsProvider.d.ts +0 -38
  80. package/lib/cjs/quantity-formatting/BasicUnitsProvider.d.ts.map +0 -1
  81. package/lib/cjs/quantity-formatting/BasicUnitsProvider.js +0 -160
  82. package/lib/cjs/quantity-formatting/BasicUnitsProvider.js.map +0 -1
  83. package/lib/esm/quantity-formatting/BasicUnitsProvider.d.ts +0 -38
  84. package/lib/esm/quantity-formatting/BasicUnitsProvider.d.ts.map +0 -1
  85. package/lib/esm/quantity-formatting/BasicUnitsProvider.js +0 -155
  86. package/lib/esm/quantity-formatting/BasicUnitsProvider.js.map +0 -1
@@ -5,11 +5,11 @@
5
5
  /** @packageDocumentation
6
6
  * @module QuantityFormatting
7
7
  */
8
- import { BeEvent, BentleyError, BeUiEvent, Logger } from "@itwin/core-bentley";
9
- import { Format, FormatterSpec, ParseError, ParserSpec, } from "@itwin/core-quantity";
8
+ import { BeEvent, BentleyError, BeUiEvent, BeUnorderedUiEvent, Logger } from "@itwin/core-bentley";
9
+ import { BasicUnitsProvider, Format, FormatSpecHandle, FormatterSpec, FormattingReadyCollector, ParseError, ParserSpec, } from "@itwin/core-quantity";
10
10
  import { FrontendLoggerCategory } from "../common/FrontendLoggerCategory";
11
11
  import { IModelApp } from "../IModelApp";
12
- import { BasicUnitsProvider, getDefaultAlternateUnitLabels } from "./BasicUnitsProvider";
12
+ import { getDefaultAlternateUnitLabels } from "./AlternateUnitLabels";
13
13
  // cSpell:ignore FORMATPROPS FORMATKEY ussurvey uscustomary USCUSTOM
14
14
  /**
15
15
  * Defines standard format types for tools that need to display measurements to user.
@@ -162,7 +162,7 @@ export class QuantityTypeFormatsProvider {
162
162
  ["CivilUnits.LENGTH", QuantityType.LengthEngineering],
163
163
  ["AecUnits.LENGTH", QuantityType.LengthEngineering]
164
164
  ]);
165
- async getFormat(name) {
165
+ async getFormat(name, _system) {
166
166
  const quantityType = this._kindOfQuantityMap.get(name);
167
167
  if (!quantityType)
168
168
  return undefined;
@@ -177,36 +177,34 @@ export class QuantityTypeFormatsProvider {
177
177
  export class FormatsProviderManager {
178
178
  _formatsProvider;
179
179
  onFormatsChanged = new BeEvent();
180
+ _removeProviderListener;
180
181
  constructor(_formatsProvider) {
181
182
  this._formatsProvider = _formatsProvider;
182
- this._formatsProvider.onFormatsChanged.addListener((args) => {
183
+ this._removeProviderListener = this._formatsProvider.onFormatsChanged.addListener((args) => {
183
184
  this.onFormatsChanged.raiseEvent(args);
184
185
  });
185
186
  }
186
- async getFormat(name) {
187
- return this._formatsProvider.getFormat(name);
187
+ async getFormat(name, system) {
188
+ return this._formatsProvider.getFormat(name, system);
188
189
  }
189
190
  get formatsProvider() { return this; }
190
191
  set formatsProvider(formatsProvider) {
192
+ this._removeProviderListener?.();
191
193
  this._formatsProvider = formatsProvider;
192
- this._formatsProvider.onFormatsChanged.addListener((args) => {
194
+ this._removeProviderListener = this._formatsProvider.onFormatsChanged.addListener((args) => {
193
195
  this.onFormatsChanged.raiseEvent(args);
194
196
  });
195
197
  this.onFormatsChanged.raiseEvent({ formatsChanged: "all" });
196
198
  }
197
199
  }
198
- /** Class that supports formatting quantity values into strings and parsing strings into quantity values. This class also maintains
199
- * the "active" unit system and caches FormatterSpecs and ParserSpecs for the "active" unit system to allow synchronous access to
200
- * parsing and formatting values. The support unit systems are defined by [[UnitSystemKey]] and is kept in synch with the unit systems
201
- * provided by the Presentation Manager on the backend. The QuantityFormatter contains a registry of quantity type definitions. These definitions implement
202
- * the [[QuantityTypeDefinition]] interface, which among other things, provide default [[FormatProps]], and provide methods
203
- * to generate both a [[FormatterSpec]] and a [[ParserSpec]]. There are built-in quantity types that are
200
+ /** The QuantityFormatter class provides methods for formatting and parsing quantities. There are a set of standard quantity types
204
201
  * identified by the [[QuantityType]] enum. [[CustomQuantityTypeDefinition]] can be registered to extend the available quantity types available
205
202
  * by frontend tools. The QuantityFormatter also allows the default formats to be overriden.
206
203
  *
207
204
  * @public
208
205
  */
209
206
  export class QuantityFormatter {
207
+ static _allUnitSystems = ["metric", "imperial", "usCustomary", "usSurvey"];
210
208
  _unitsProvider = new BasicUnitsProvider();
211
209
  _alternateUnitLabelsRegistry = new AlternateUnitLabelsRegistry(getDefaultAlternateUnitLabels());
212
210
  /** Registry containing available quantity type definitions. */
@@ -217,9 +215,9 @@ export class QuantityFormatter {
217
215
  _formatSpecsRegistry = new Map();
218
216
  /** Active UnitSystem key - must be one of "imperial", "metric", "usCustomary", or "usSurvey". */
219
217
  _activeUnitSystem = "imperial";
220
- /** Map of FormatSpecs for all available QuantityTypes and the active Unit System */
218
+ /** Map of FormatSpecs for all available QuantityTypes, keyed by quantity type */
221
219
  _activeFormatSpecsByType = new Map();
222
- /** Map of ParserSpecs for all available QuantityTypes and the active Unit System */
220
+ /** Map of ParserSpecs for all available QuantityTypes, keyed by quantity type */
223
221
  _activeParserSpecsByType = new Map();
224
222
  /** Map of FormatSpecs that have been overriden from the default. */
225
223
  _overrideFormatPropsByUnitSystem = new Map();
@@ -241,6 +239,45 @@ export class QuantityFormatter {
241
239
  onQuantityFormatsChanged = new BeUiEvent();
242
240
  /** Fired when the active UnitsProvider is updated. This will allow cached Formatter and Parser specs to be updated if necessary. */
243
241
  onUnitsProviderChanged = new BeUiEvent();
242
+ /** Fired after every reload path completes (initialization, unit system change, provider change, reinitialize).
243
+ * This is the terminal "ready" signal — it fires once the QuantityFormatter has fully rebuilt its caches and is
244
+ * ready to format/parse values. Subscribe to this event to know when formatting specs are available.
245
+ *
246
+ * Uses `BeUnorderedUiEvent` (Set-backed) so listeners can safely add/remove themselves during emit —
247
+ * this is critical for `FormatSpecHandle` instances that subscribe/dispose at volume.
248
+ * @beta
249
+ */
250
+ onFormattingReady = new BeUnorderedUiEvent();
251
+ /** Event for formatting providers to register async work before the formatter signals ready.
252
+ * Fires synchronously after each reload completes. Providers should call
253
+ * `collector.addPendingWork(promise)` to register work. The formatter awaits all
254
+ * registered work (with a 20-second default timeout) before emitting [[onFormattingReady]].
255
+ *
256
+ * Use this for **providers** that need async loading. Use [[onFormattingReady]] for
257
+ * **consumers** that read specs.
258
+ * @beta
259
+ */
260
+ onBeforeFormattingReady = new BeEvent();
261
+ /** Whether the QuantityFormatter has completed at least one successful reload and is ready to format/parse.
262
+ * @beta
263
+ */
264
+ get isReady() { return this._isReady; }
265
+ /** A promise that resolves after the first successful initialization. This is one-shot — it resolves once
266
+ * and stays resolved forever. For subsequent reloads, subscribe to [[onFormattingReady]].
267
+ *
268
+ * This promise never rejects. If the first initialization attempt fails, it stays pending until a
269
+ * subsequent reload succeeds (e.g., triggered by `setUnitsProvider` or a format change). There is no
270
+ * finite retry limit, so rejection would prematurely close the door on recovery.
271
+ * @beta
272
+ */
273
+ get whenInitialized() { return this._initializedPromise; }
274
+ _isReady = false;
275
+ _hasEverBeenReady = false;
276
+ _initializedPromise;
277
+ _resolveInitialized;
278
+ _reloadInFlight = false;
279
+ _pendingReload;
280
+ _deferredSystemChangedEmit;
244
281
  _removeFormatsProviderListener;
245
282
  /**
246
283
  * constructor
@@ -248,6 +285,9 @@ export class QuantityFormatter {
248
285
  * set it to a specific unit system pass a UnitSystemKey.
249
286
  */
250
287
  constructor(showMetricOrUnitSystem) {
288
+ this._initializedPromise = new Promise((resolve) => {
289
+ this._resolveInitialized = resolve;
290
+ });
251
291
  if (undefined !== showMetricOrUnitSystem) {
252
292
  if (typeof showMetricOrUnitSystem === "boolean")
253
293
  this._activeUnitSystem = showMetricOrUnitSystem ? "metric" : "imperial";
@@ -261,6 +301,116 @@ export class QuantityFormatter {
261
301
  this._removeFormatsProviderListener = undefined;
262
302
  }
263
303
  }
304
+ /** Schedule an async reload. If no reload is in flight, runs immediately. If a reload is
305
+ * already in flight, stores the intent as pending (latest-wins: only the last scheduled reload
306
+ * is kept). `finalizeReload()` fires only when the queue is fully drained.
307
+ *
308
+ * **Await semantics:** When a reload is already in flight, the returned promise resolves
309
+ * immediately *without* the requested reload having run. Callers that need to know when
310
+ * the reload has actually completed should listen for `onFormattingReady` instead.
311
+ *
312
+ * Reload intents:
313
+ * - `"full"` — rebuild entire registry + re-register provider listener (onInitialized, setUnitsProvider)
314
+ * - `"formatsChanged"` — patch registry from provider + load maps (formatsChanged listener)
315
+ * - `"activeSystem"` — reload format/parsing maps for current unit system (setActiveUnitSystem, reinitializeFormatAndParsingsMaps)
316
+ * @internal
317
+ */
318
+ async scheduleReload(intent) {
319
+ if (this._reloadInFlight) {
320
+ // A reload is already running — queue this one (latest-wins)
321
+ this._pendingReload = intent;
322
+ return;
323
+ }
324
+ this._reloadInFlight = true;
325
+ this._isReady = false;
326
+ this._deferredSystemChangedEmit = undefined; // Clear stale deferred from prior cycle
327
+ try {
328
+ await this._executeReload(intent);
329
+ }
330
+ catch (err) {
331
+ Logger.logError(`${FrontendLoggerCategory.Package}.QuantityFormatter`, BentleyError.getErrorMessage(err));
332
+ this._reloadInFlight = false;
333
+ // If there's a pending reload, still try to run it
334
+ if (this._pendingReload) {
335
+ const next = this._pendingReload;
336
+ this._pendingReload = undefined;
337
+ return this.scheduleReload(next);
338
+ }
339
+ // Restore prior ready state so stale-but-usable specs remain accessible
340
+ if (this._hasEverBeenReady) {
341
+ Logger.logWarning(`${FrontendLoggerCategory.Package}.QuantityFormatter`, "Reload failed — restoring previous ready state. Cached specs may be stale.");
342
+ this._isReady = true;
343
+ }
344
+ return;
345
+ }
346
+ // Current reload succeeded — check if another was queued
347
+ if (this._pendingReload) {
348
+ const next = this._pendingReload;
349
+ this._pendingReload = undefined;
350
+ this._reloadInFlight = false;
351
+ return this.scheduleReload(next);
352
+ }
353
+ // Queue is drained — finalize
354
+ await this.finalizeReload();
355
+ // A new reload may have been queued during the async finalizeReload window
356
+ if (this._pendingReload) {
357
+ const next = this._pendingReload;
358
+ this._pendingReload = undefined;
359
+ this._reloadInFlight = false;
360
+ return this.scheduleReload(next);
361
+ }
362
+ this._reloadInFlight = false;
363
+ }
364
+ /** Execute the reload work for a given intent. All reload logic is centralized here.
365
+ * @internal
366
+ */
367
+ async _executeReload(intent) {
368
+ switch (intent.scope) {
369
+ case "full":
370
+ await this._reloadCore();
371
+ break;
372
+ case "formatsChanged": {
373
+ const { args } = intent;
374
+ await this._rebuildRegistryFromProvider(args);
375
+ if (args.impliedUnitSystem && args.impliedUnitSystem !== this._activeUnitSystem) {
376
+ this._activeUnitSystem = args.impliedUnitSystem;
377
+ }
378
+ await this.loadFormatAndParsingMapsForSystem(this._activeUnitSystem);
379
+ if (args.impliedUnitSystem) {
380
+ this._deferredSystemChangedEmit = { system: this._activeUnitSystem };
381
+ }
382
+ break;
383
+ }
384
+ case "activeSystem":
385
+ await this.loadFormatAndParsingMapsForSystem(this._activeUnitSystem);
386
+ if (intent.emitSystemChanged) {
387
+ this._deferredSystemChangedEmit = { system: this._activeUnitSystem };
388
+ }
389
+ break;
390
+ }
391
+ }
392
+ /** Called when the reload queue is fully drained after a successful reload.
393
+ * Sets `isReady` to true, resolves `whenInitialized` (one-shot), and emits ready events.
394
+ * @internal
395
+ */
396
+ async finalizeReload() {
397
+ // Phase 1: Let providers register async work
398
+ const collector = new FormattingReadyCollector();
399
+ this.onBeforeFormattingReady.raiseEvent(collector);
400
+ await collector.awaitAll();
401
+ // Phase 2: Signal ready to consumers
402
+ this._isReady = true;
403
+ this._hasEverBeenReady = true;
404
+ this._resolveInitialized();
405
+ this.onFormattingReady.emit();
406
+ // Phase 3: Emit deferred unit-system-changed if the winning reload set one.
407
+ // This fires after isReady === true so listeners can safely use the formatter.
408
+ if (this._deferredSystemChangedEmit) {
409
+ const args = this._deferredSystemChangedEmit;
410
+ this._deferredSystemChangedEmit = undefined;
411
+ this.onActiveFormattingUnitSystemChanged.emit(args);
412
+ }
413
+ }
264
414
  getOverrideFormatPropsByQuantityType(quantityTypeKey, unitSystem) {
265
415
  const requestedUnitSystem = unitSystem ?? this.activeUnitSystem;
266
416
  const overrideMap = this._overrideFormatPropsByUnitSystem.get(requestedUnitSystem);
@@ -387,6 +537,12 @@ export class QuantityFormatter {
387
537
  * @internal
388
538
  */
389
539
  async onInitialized() {
540
+ await this.scheduleReload({ scope: "full" });
541
+ }
542
+ /** Core reload logic — does all async I/O and cache rebuilding without events or state management.
543
+ * @internal
544
+ */
545
+ async _reloadCore() {
390
546
  // Remove any existing listener before re-registering to avoid duplicates when called via setUnitsProvider.
391
547
  if (this._removeFormatsProviderListener) {
392
548
  this._removeFormatsProviderListener();
@@ -395,46 +551,61 @@ export class QuantityFormatter {
395
551
  await this.initializeQuantityTypesRegistry();
396
552
  const initialKoQs = [["DefaultToolsUnits.LENGTH", "Units.M"], ["DefaultToolsUnits.ANGLE", "Units.RAD"], ["DefaultToolsUnits.AREA", "Units.SQ_M"], ["DefaultToolsUnits.VOLUME", "Units.CUB_M"], ["DefaultToolsUnits.LENGTH_COORDINATE", "Units.M"], ["CivilUnits.STATION", "Units.M"], ["CivilUnits.LENGTH", "Units.M"], ["AecUnits.LENGTH", "Units.M"]];
397
553
  for (const entry of initialKoQs) {
398
- try {
399
- await this.addFormattingSpecsToRegistry(entry[0], entry[1]);
554
+ for (const system of QuantityFormatter._allUnitSystems) {
555
+ try {
556
+ await this.addFormattingSpecsToRegistry({ name: entry[0], persistenceUnitName: entry[1], system });
557
+ }
558
+ catch (err) {
559
+ Logger.logWarning(`${FrontendLoggerCategory.Package}.QuantityFormatter`, err.toString());
560
+ }
561
+ }
562
+ }
563
+ // Register formatsProvider listener that triggers a queued reload when formats change
564
+ this._removeFormatsProviderListener = IModelApp.formatsProvider.onFormatsChanged.addListener((args) => {
565
+ void this.scheduleReload({ scope: "formatsChanged", args });
566
+ });
567
+ // initialize default format and parsing specs
568
+ await this.loadFormatAndParsingMapsForSystem();
569
+ }
570
+ /** Rebuild the formatting specs registry from the current formatsProvider based on changed args.
571
+ * @internal
572
+ */
573
+ async _rebuildRegistryFromProvider(args) {
574
+ if (args.formatsChanged === "all") {
575
+ for (const [name, unitMap] of this._formatSpecsRegistry.entries()) {
576
+ await this._rebuildRegistryForName(name, unitMap);
400
577
  }
401
- catch (err) {
402
- Logger.logWarning(`${FrontendLoggerCategory.Package}.QuantityFormatter`, err.toString());
578
+ }
579
+ else {
580
+ for (const name of args.formatsChanged) {
581
+ const unitMap = this._formatSpecsRegistry.get(name);
582
+ if (unitMap) {
583
+ await this._rebuildRegistryForName(name, unitMap);
584
+ }
403
585
  }
404
586
  }
405
- this._removeFormatsProviderListener = IModelApp.formatsProvider.onFormatsChanged.addListener(async (args) => {
406
- if (args.formatsChanged === "all") {
407
- for (const [name, entry] of this._formatSpecsRegistry.entries()) {
408
- const formatProps = await IModelApp.formatsProvider.getFormat(name);
409
- if (formatProps) {
410
- const persistenceUnitName = entry.formatterSpec.persistenceUnit.name;
411
- await this.addFormattingSpecsToRegistry(name, persistenceUnitName, formatProps);
412
- }
413
- else {
414
- this._formatSpecsRegistry.delete(name); // clear the specs if format was removed, or no longer exists.
415
- }
587
+ }
588
+ /** Rebuild all system entries for a single KoQ name in the registry. */
589
+ async _rebuildRegistryForName(name, unitMap) {
590
+ let anySystemHadFormat = false;
591
+ for (const system of QuantityFormatter._allUnitSystems) {
592
+ const formatProps = await IModelApp.formatsProvider.getFormat(name, system);
593
+ if (formatProps) {
594
+ anySystemHadFormat = true;
595
+ for (const [persistenceUnitName] of unitMap.entries()) {
596
+ await this.addFormattingSpecsToRegistry({ name, persistenceUnitName, formatProps, system });
416
597
  }
417
598
  }
418
599
  else {
419
- for (const name of args.formatsChanged) {
420
- if (this._formatSpecsRegistry.has(name)) {
421
- const formatProps = await IModelApp.formatsProvider.getFormat(name);
422
- if (formatProps) {
423
- const existingEntry = this._formatSpecsRegistry.get(name);
424
- if (existingEntry) {
425
- const persistenceUnitName = existingEntry.formatterSpec.persistenceUnit.name;
426
- await this.addFormattingSpecsToRegistry(name, persistenceUnitName, formatProps);
427
- }
428
- }
429
- else {
430
- this._formatSpecsRegistry.delete(name);
431
- }
432
- }
600
+ // Remove stale entries for this system
601
+ for (const [, systemMap] of unitMap.entries()) {
602
+ systemMap.delete(system);
433
603
  }
434
604
  }
435
- });
436
- // initialize default format and parsing specs
437
- await this.loadFormatAndParsingMapsForSystem();
605
+ }
606
+ if (!anySystemHadFormat) {
607
+ this._formatSpecsRegistry.delete(name);
608
+ }
438
609
  }
439
610
  /** Return a map that serves as a registry of all standard and custom quantity types. */
440
611
  get quantityTypesRegistry() {
@@ -469,7 +640,7 @@ export class QuantityFormatter {
469
640
  this._unitsProvider = unitsProvider;
470
641
  try {
471
642
  // force all cached data to be reinitialized
472
- await IModelApp.quantityFormatter.onInitialized();
643
+ await this.scheduleReload({ scope: "full" });
473
644
  }
474
645
  catch (err) {
475
646
  Logger.logWarning(`${FrontendLoggerCategory.Package}.quantityFormatter`, BentleyError.getErrorMessage(err), BentleyError.getErrorMetadata(err));
@@ -487,6 +658,7 @@ export class QuantityFormatter {
487
658
  * `IModelApp.toolAdmin.restartPrimitiveTool()` to allow the tool to reinitialize itself.
488
659
  */
489
660
  async resetToUseInternalUnitsProvider() {
661
+ // Coupled to createUnitsProvider() returning BasicUnitsProvider directly when no primary is set.
490
662
  if (this._unitsProvider instanceof BasicUnitsProvider)
491
663
  return;
492
664
  await this.setUnitsProvider(new BasicUnitsProvider());
@@ -516,8 +688,7 @@ export class QuantityFormatter {
516
688
  this._overrideFormatPropsByUnitSystem = overrideFormatPropsByUnitSystem;
517
689
  }
518
690
  unitSystemKey && (this._activeUnitSystem = unitSystemKey);
519
- await this.loadFormatAndParsingMapsForSystem(this._activeUnitSystem);
520
- fireUnitSystemChanged && this.onActiveFormattingUnitSystemChanged.emit({ system: this._activeUnitSystem });
691
+ await this.scheduleReload({ scope: "activeSystem", emitSystemChanged: fireUnitSystemChanged });
521
692
  IModelApp.toolAdmin && startDefaultTool && await IModelApp.toolAdmin.startDefaultTool();
522
693
  }
523
694
  /** Set the Active unit system to one of the supported types. This will asynchronously load the formatter and parser specs for the activated system. */
@@ -530,11 +701,9 @@ export class QuantityFormatter {
530
701
  if (this._activeUnitSystem === systemType)
531
702
  return;
532
703
  this._activeUnitSystem = systemType;
533
- await this.loadFormatAndParsingMapsForSystem(systemType);
704
+ await this.scheduleReload({ scope: "activeSystem", emitSystemChanged: true });
534
705
  // allow settings provider to store the change
535
706
  await this._unitFormattingSettingsProvider?.storeUnitSystemSetting({ system: systemType });
536
- // fire current event
537
- this.onActiveFormattingUnitSystemChanged.emit({ system: systemType });
538
707
  if (IModelApp.toolAdmin && restartActiveTool)
539
708
  return IModelApp.toolAdmin.startDefaultTool();
540
709
  }
@@ -780,7 +949,10 @@ export class QuantityFormatter {
780
949
  }
781
950
  /** Returns data needed to convert from one Unit to another in the same Unit Family/Phenomenon. */
782
951
  async getConversion(fromUnit, toUnit) {
783
- return this._unitsProvider.getConversion(fromUnit, toUnit);
952
+ const result = await this._unitsProvider.getConversion(fromUnit, toUnit);
953
+ if (result.error)
954
+ Logger.logWarning(`${FrontendLoggerCategory.Package}.quantityFormatter`, `Unit conversion from "${fromUnit.name}" to "${toUnit.name}" could not be resolved.`);
955
+ return result;
784
956
  }
785
957
  /**
786
958
  * Creates a [[FormatterSpec]] for a given persistence unit name and format properties, using the [[UnitsProvider]] to resolve the persistence unit.
@@ -806,21 +978,50 @@ export class QuantityFormatter {
806
978
  }
807
979
  /**
808
980
  * @beta
809
- * Returns a [[FormattingSpecEntry]] for a given name, typically a KindOfQuantity full name.
981
+ * Returns a map of [[FormattingSpecEntry]] keyed by persistence unit for a given name, typically a KindOfQuantity full name.
810
982
  */
811
983
  getSpecsByName(name) {
812
- return this._formatSpecsRegistry.get(name);
984
+ const unitMap = this._formatSpecsRegistry.get(name);
985
+ if (!unitMap)
986
+ return undefined;
987
+ // Return active-system projection
988
+ const result = new Map();
989
+ for (const [persistenceUnit, systemMap] of unitMap) {
990
+ const entry = systemMap.get(this._activeUnitSystem);
991
+ if (entry)
992
+ result.set(persistenceUnit, entry);
993
+ }
994
+ return result.size > 0 ? result : undefined;
995
+ }
996
+ /**
997
+ * Returns a [[FormattingSpecEntry]] for a given name and persistence unit.
998
+ * @beta
999
+ */
1000
+ getSpecsByNameAndUnit(args) {
1001
+ const effectiveSystem = args.system ?? this._activeUnitSystem;
1002
+ return this._formatSpecsRegistry.get(args.name)?.get(args.persistenceUnitName)?.get(effectiveSystem);
1003
+ }
1004
+ /** Create a cacheable handle to formatting specs for a specific KoQ and persistence unit.
1005
+ * The handle auto-refreshes when the QuantityFormatter reloads. Call `dispose()` when done.
1006
+ *
1007
+ * @param koqName - The KindOfQuantity name (e.g., "DefaultToolsUnits.LENGTH")
1008
+ * @param persistenceUnit - The persistence unit name (e.g., "Units.M")
1009
+ * @returns A FormatSpecHandle that auto-updates on reload
1010
+ * @beta
1011
+ */
1012
+ getFormatSpecHandle(koqName, persistenceUnit, system) {
1013
+ return new FormatSpecHandle({ provider: this, name: koqName, persistenceUnitName: persistenceUnit, system });
813
1014
  }
814
1015
  /**
815
1016
  * Populates the registry with a new FormatterSpec and ParserSpec entry for the given format name.
816
1017
  * @beta
817
- * @param name The key used to identify the formatter and parser spec
818
- * @param persistenceUnitName The name of the persistence unit
819
- * @param formatProps If not supplied, tries to retrieve the [[FormatProps]] from [[IModelApp.formatsProvider]]
820
1018
  */
821
- async addFormattingSpecsToRegistry(name, persistenceUnitName, formatProps) {
1019
+ async addFormattingSpecsToRegistry(args) {
1020
+ const { name, persistenceUnitName } = args;
1021
+ const effectiveSystem = args.system ?? this._activeUnitSystem;
1022
+ let formatProps = args.formatProps;
822
1023
  if (!formatProps) {
823
- formatProps = await IModelApp.formatsProvider.getFormat(name);
1024
+ formatProps = await IModelApp.formatsProvider.getFormat(name, effectiveSystem);
824
1025
  }
825
1026
  if (formatProps) {
826
1027
  const formatterSpec = await this.createFormatterSpec({
@@ -833,7 +1034,12 @@ export class QuantityFormatter {
833
1034
  formatProps,
834
1035
  formatName: name,
835
1036
  });
836
- this._formatSpecsRegistry.set(name, { formatterSpec, parserSpec });
1037
+ if (!this._formatSpecsRegistry.has(name))
1038
+ this._formatSpecsRegistry.set(name, new Map());
1039
+ const unitMap = this._formatSpecsRegistry.get(name);
1040
+ if (!unitMap.has(persistenceUnitName))
1041
+ unitMap.set(persistenceUnitName, new Map());
1042
+ unitMap.get(persistenceUnitName).set(effectiveSystem, { formatterSpec, parserSpec });
837
1043
  }
838
1044
  else {
839
1045
  throw new Error(`Unable to find format properties for ${name} with persistence unit ${persistenceUnitName}`);