Skip to content

Commit 92d5ddd

Browse files
committed
Finish FormatManager
1 parent 671f52f commit 92d5ddd

File tree

5 files changed

+144
-118
lines changed

5 files changed

+144
-118
lines changed

apps/test-viewer/src/components/FormatManager.tsx

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { BeEvent } from "@itwin/core-bentley";
7-
import { IModelApp, IModelConnection } from "@itwin/core-frontend";
7+
import type { IModelConnection } from "@itwin/core-frontend";
8+
import { IModelApp } from "@itwin/core-frontend";
89
import type { FormatDefinition, FormatsChangedArgs, FormatsProvider, MutableFormatsProvider } from "@itwin/core-quantity";
910
import type { FormatSet } from "@itwin/ecschema-metadata";
1011
import { SchemaFormatsProvider, SchemaItemType, SchemaKey, SchemaMatchType } from "@itwin/ecschema-metadata";
@@ -14,10 +15,14 @@ export class FormatManager {
1415
private _formatSets: FormatSet[] = [];
1516
private _fallbackFormatProvider?: FormatsProvider;
1617
private _activeFormatSet?: FormatSet;
18+
private _activeFormatSetFormatsProvider?: FormatSetFormatsProvider;
1719
private _iModelOpened: boolean = false;
1820
private _removeListeners: (() => void)[] = [];
1921

20-
public static get instance(): FormatManager {
22+
/** Event raised when the active format set changes */
23+
public readonly onActiveFormatSetChanged = new BeEvent<(formatSet: FormatSet | undefined) => void>();
24+
25+
public static get instance(): FormatManager | undefined {
2126
return this._instance;
2227
}
2328

@@ -33,22 +38,15 @@ export class FormatManager {
3338
return this._activeFormatSet;
3439
}
3540

41+
public get activeFormatSetFormatsProvider(): FormatSetFormatsProvider | undefined {
42+
return this._activeFormatSetFormatsProvider;
43+
}
44+
3645
/** Initialize with a set of format sets to use */
3746
public static async initialize(formatSets: FormatSet[], fallbackProvider?: FormatsProvider): Promise<void> {
38-
if (this._instance)
39-
throw new Error("FormatManager is already initialized.");
47+
if (this._instance) throw new Error("FormatManager is already initialized.");
4048

4149
this._instance = new FormatManager(formatSets, fallbackProvider);
42-
43-
this._instance._removeListeners.push(IModelConnection.onOpen.addListener(async (iModel: IModelConnection) => {
44-
// Initialize the formats provider for the opened iModel
45-
this._instance._iModelOpened = true;
46-
await this._instance.onIModelOpen(iModel);
47-
}));
48-
49-
this._instance._removeListeners.push(IModelConnection.onClose.addListener(async () => {
50-
this._instance._iModelOpened = false;
51-
}));
5250
}
5351

5452
public constructor(formatSets: FormatSet[], fallbackProvider?: FormatsProvider) {
@@ -66,10 +64,13 @@ export class FormatManager {
6664
public setActiveFormatSet(formatSet: FormatSet): void {
6765
const formatSetFormatsProvider = new FormatSetFormatsProvider(formatSet, this._fallbackFormatProvider);
6866
this._activeFormatSet = formatSet;
67+
this._activeFormatSetFormatsProvider = formatSetFormatsProvider;
6968

7069
if (this._iModelOpened) {
7170
IModelApp.formatsProvider = formatSetFormatsProvider;
7271
}
72+
73+
this.onActiveFormatSetChanged.raiseEvent(formatSet);
7374
}
7475

7576
// Typically, enables a SchemaFormatsProvider to be the fallback during runtime.
@@ -78,6 +79,7 @@ export class FormatManager {
7879
if (this._activeFormatSet) {
7980
// If we have an active format set, we need to update the formats provider to include the new fallback.
8081
const newFormatSetFormatsProvider = new FormatSetFormatsProvider(this._activeFormatSet, this._fallbackFormatProvider);
82+
this._activeFormatSetFormatsProvider = newFormatSetFormatsProvider;
8183
IModelApp.formatsProvider = newFormatSetFormatsProvider;
8284
}
8385
}
@@ -86,37 +88,46 @@ export class FormatManager {
8688
return this._fallbackFormatProvider;
8789
}
8890

91+
public async onIModelClose() {
92+
// Clean up listeners
93+
this._removeListeners.forEach((removeListener) => removeListener());
94+
this._fallbackFormatProvider = undefined;
95+
if (this._activeFormatSetFormatsProvider) {
96+
this._activeFormatSetFormatsProvider.clearFallbackProvider(); // Works here because the fallback provider is the SchemaFormatsProvider used onIModelOpen.
97+
}
98+
this._iModelOpened = false;
99+
}
100+
101+
/**
102+
* If FormatSetFormatsProvider was successfully set, renders the usage of IModelApp.quantityFormatter.activeUnitSystem pointless when formatting.
103+
*/
89104
public async onIModelOpen(iModel: IModelConnection): Promise<void> {
90105
// Set up schema-based units and formats providers
91-
92106
const schemaFormatsProvider = new SchemaFormatsProvider(iModel.schemaContext, IModelApp.quantityFormatter.activeUnitSystem);
93107
this.fallbackFormatsProvider = schemaFormatsProvider;
94-
108+
this._removeListeners.push(
109+
IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener((args) => {
110+
schemaFormatsProvider.unitSystem = args.system;
111+
}),
112+
);
95113
// Query schemas for KindOfQuantity items
96114
try {
97115
const schemaFormatSet: FormatSet = {
98116
name: "SchemaFormats",
99117
label: "Example Format Set",
100-
formats: {}
118+
formats: {},
101119
};
102-
const reader = iModel.createQueryReader("SELECT\n ks.Name || '.' || k.Name AS kindOfQuantityFullName,\n COUNT(*) AS propertyCount,\n json_group_array(p.Name) AS propertyNames\nFROM\n ECDbMeta.ECPropertyDef p\n JOIN ECDbMeta.KindOfQuantityDef k ON k.ECInstanceId = p.KindOfQuantity.Id\n JOIN ECDbMeta.ECSchemaDef ks ON ks.ECInstanceId = k.Schema.Id\nGROUP BY\n ks.Name,\n k.Name\nORDER BY\n propertyCount DESC;");
103-
while (await reader.step()) {
104-
console.log(reader.current[0]);
105-
const formatName = reader.current[0].kindOfQuantityFullName;
106-
const format = await schemaFormatsProvider.getFormat(formatName);
107-
if (format) {
108-
schemaFormatSet.formats[formatName] = format;
109-
}
110-
}
120+
121+
111122
// Try to get known schemas that typically contain KindOfQuantity items, and get all the formats from kind of quantities
112123
const schemaNames = ["AecUnits"];
113124

114125
for (const schemaName of schemaNames) {
115126
try {
116127
const schema = await iModel.schemaContext.getSchema(new SchemaKey(schemaName, SchemaMatchType.Latest));
117-
if (schema) {
128+
if (schema) {
118129
for (const schemaItem of schema.getItems()) {
119-
console.log(schemaItem)
130+
console.log(schemaItem);
120131
if (schemaItem.schemaItemType === SchemaItemType.KindOfQuantity) {
121132
const format = await schemaFormatsProvider.getFormat(schemaItem.fullName);
122133
if (format) {
@@ -130,14 +141,29 @@ export class FormatManager {
130141
}
131142
}
132143

144+
// Get all used KindOfQuantities from the iModel, and populate the formatSet.
145+
const reader = iModel.createQueryReader(
146+
"SELECT\n ks.Name || '.' || k.Name AS kindOfQuantityFullName,\n COUNT(*) AS propertyCount,\n json_group_array(p.Name) AS propertyNames\nFROM\n ECDbMeta.ECPropertyDef p\n JOIN ECDbMeta.KindOfQuantityDef k ON k.ECInstanceId = p.KindOfQuantity.Id\n JOIN ECDbMeta.ECSchemaDef ks ON ks.ECInstanceId = k.Schema.Id\nGROUP BY\n ks.Name,\n k.Name\nORDER BY\n propertyCount DESC;",
147+
);
148+
const allRows = await reader.toArray();
149+
for (const row of allRows) {
150+
const formatName = row[0];
151+
const format = await schemaFormatsProvider.getFormat(formatName);
152+
if (format) {
153+
schemaFormatSet.formats[formatName] = format;
154+
}
155+
}
156+
157+
133158
// Set this as the active format set if we found any formats
134159
if (Object.keys(schemaFormatSet.formats).length > 0) {
160+
this._iModelOpened = true;
135161
this.setActiveFormatSet(schemaFormatSet);
162+
136163
console.log(`Created schema-based format set with ${Object.keys(schemaFormatSet.formats).length} formats`);
137164
} else {
138165
console.log("No KindOfQuantity items found in known schemas");
139166
}
140-
141167
} catch (error) {
142168
console.error("Failed to query schema items:", error);
143169
}
@@ -160,6 +186,10 @@ export class FormatSetFormatsProvider implements MutableFormatsProvider {
160186
this.onFormatsChanged.raiseEvent({ formatsChanged: [name] });
161187
}
162188

189+
public clearFallbackProvider(): void {
190+
this._fallbackProvider = undefined;
191+
}
192+
163193
public async getFormat(name: string): Promise<FormatDefinition | undefined> {
164194
const format = this._formatSet.formats[name];
165195
if (format)

apps/test-viewer/src/components/QuantityFormatUiItemsProvider.tsx

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@ import React, { useCallback, useState } from "react";
77
import { StagePanelLocation, StagePanelSection } from "@itwin/appui-react";
88
import { FormatSelector, QuantityFormatPanel } from "@itwin/quantity-formatting-react";
99
import { IModelApp } from "@itwin/core-frontend";
10-
import type { FormatDefinition, FormatProps } from "@itwin/core-quantity";
10+
import type { FormatDefinition } from "@itwin/core-quantity";
1111
import { Button, Modal, ModalButtonBar } from "@itwin/itwinui-react";
1212
import { FormatManager } from "./FormatManager";
1313

1414
import type { UiItemsProvider, Widget } from "@itwin/appui-react";
15+
import type { FormatSet } from "@itwin/ecschema-metadata";
1516

1617
/** Widget component that shows a button to open the Quantity Format Panel in a modal */
1718
const QuantityFormatWidget: React.FC = () => {
1819
// Initial format definition with basic decimal format
19-
const [formatDefinition, setFormatDefinition] = useState<FormatProps>({
20+
const [formatDefinition, setFormatDefinition] = useState<FormatDefinition>({
2021
precision: 4,
2122
type: "Decimal",
2223
composite: {
@@ -26,11 +27,12 @@ const QuantityFormatWidget: React.FC = () => {
2627

2728
const [isModalOpen, setIsModalOpen] = useState(false);
2829
const [unitsProvider, setUnitsProvider] = useState(() => IModelApp.quantityFormatter.unitsProvider);
29-
const [activeFormatSet] = useState(() => FormatManager.instance.activeFormatSet);
30+
const [activeFormatSet, setActiveFormatSet] = useState<FormatSet | undefined>(FormatManager.instance?.activeFormatSet);
3031
const [activeFormatDefinitionKey, setActiveFormatDefinitionKey] = useState<string | undefined>();
3132

32-
const handleFormatChange = useCallback((newFormat: FormatProps) => {
33+
const handleFormatChange = useCallback(async (newFormat: FormatDefinition) => {
3334
setFormatDefinition(newFormat);
35+
await FormatManager.instance?.activeFormatSetFormatsProvider?.addFormat(newFormat.name ?? "", newFormat);
3436
}, []);
3537

3638
const handleFormatSelectorChange = useCallback((formatDef: FormatDefinition, key: string) => {
@@ -46,11 +48,6 @@ const QuantityFormatWidget: React.FC = () => {
4648
setIsModalOpen(false);
4749
}, []);
4850

49-
const handleApply = useCallback(() => {
50-
// Here you could apply the format changes to the active iModel
51-
console.log("Applied format changes:", formatDefinition);
52-
setIsModalOpen(false);
53-
}, [formatDefinition]);
5451

5552
// Memoize the unitsProvider to prevent unnecessary re-renders
5653
React.useEffect(() => {
@@ -63,49 +60,43 @@ const QuantityFormatWidget: React.FC = () => {
6360
};
6461
}, []);
6562

63+
// Listen for active format set changes
64+
React.useEffect(() => {
65+
const removeFormatSetListener = FormatManager.instance?.onActiveFormatSetChanged.addListener((formatSet) => {
66+
setActiveFormatSet(formatSet);
67+
setActiveFormatDefinitionKey(undefined); // Reset selection when format set changes
68+
});
69+
return () => {
70+
if (removeFormatSetListener) removeFormatSetListener();
71+
};
72+
}, []);
73+
6674
return (
6775
<>
6876
<div style={{ padding: "16px" }}>
69-
<Button
70-
onClick={handleOpenModal}
71-
styleType="high-visibility"
72-
size="large"
73-
style={{ width: "100%" }}
74-
>
77+
<Button onClick={handleOpenModal} styleType="high-visibility" size="large" style={{ width: "100%" }}>
7578
Configure Quantity Format
7679
</Button>
7780

78-
{activeFormatSet && (
79-
<div style={{ padding: "16px 0" }}>
80-
<FormatSelector
81-
activeFormatSet={activeFormatSet}
82-
activeFormatDefinitionKey={activeFormatDefinitionKey}
83-
onListItemChange={handleFormatSelectorChange}
84-
/>
85-
</div>
86-
)}
81+
8782
</div>
8883

89-
<Modal
90-
isOpen={isModalOpen}
91-
onClose={handleCloseModal}
92-
title="Quantity Format Settings"
93-
style={{ width: "600px", maxWidth: "90vw" }}
94-
>
84+
<Modal isOpen={isModalOpen} onClose={handleCloseModal} title="Quantity Format Settings" style={{ width: "600px", maxWidth: "90vw" }}>
9585
<div style={{ padding: "16px", height: "1200px", overflow: "auto" }}>
96-
<QuantityFormatPanel
97-
formatDefinition={formatDefinition}
98-
unitsProvider={unitsProvider}
99-
onFormatChange={handleFormatChange}
100-
/>
86+
<div style={{ padding: "16px 0" }}>
87+
<FormatSelector
88+
activeFormatSet={activeFormatSet}
89+
activeFormatDefinitionKey={activeFormatDefinitionKey}
90+
onListItemChange={handleFormatSelectorChange}
91+
/>
92+
</div>
93+
94+
<QuantityFormatPanel formatDefinition={formatDefinition} unitsProvider={unitsProvider} onFormatChange={handleFormatChange} />
10195
</div>
10296

10397
<ModalButtonBar>
10498
<Button styleType="default" onClick={handleCloseModal}>
105-
Cancel
106-
</Button>
107-
<Button styleType="high-visibility" onClick={handleApply}>
108-
Apply
99+
Close
109100
</Button>
110101
</ModalButtonBar>
111102
</Modal>

apps/test-viewer/src/components/Viewer.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { useCallback, useEffect, useState } from "react";
77
import { useNavigate, useSearchParams } from "react-router-dom";
88
import { IModelApp, IModelConnection } from "@itwin/core-frontend";
9-
import { SchemaFormatsProvider, SchemaKey, SchemaMatchType, SchemaUnitProvider } from "@itwin/ecschema-metadata";
9+
import { SchemaFormatsProvider, SchemaUnitProvider } from "@itwin/ecschema-metadata";
1010
import { ECSchemaRpcInterface } from "@itwin/ecschema-rpcinterface-common";
1111
import { FrontendDevTools } from "@itwin/frontend-devtools";
1212
import { ArcGisAccessClient } from "@itwin/map-layers-auth";
@@ -41,7 +41,7 @@ function ViewerWithOptions() {
4141

4242
// Initialize FormatManager with sample format sets
4343
await FormatManager.initialize([]);
44-
44+
console.log(FormatManager.instance)
4545
// ArcGIS Oauth setup
4646
const accessClient = new ArcGisAccessClient();
4747
accessClient.initialize({
@@ -102,32 +102,26 @@ function onIModelConnected(imodel: IModelConnection) {
102102
}, 1000);
103103
const setupFormatsProvider = async () => {
104104
try {
105-
const schema = await imodel.schemaContext.getSchema(new SchemaKey("AecUnits", SchemaMatchType.Latest));
106-
if (!schema) throw new Error("AecUnits schema not found in iModel");
107-
108105
const schemaUnitsProvider = new SchemaUnitProvider(imodel.schemaContext);
109106
IModelApp.quantityFormatter.unitsProvider = schemaUnitsProvider;
110-
111-
const schemaFormatsProvider = new SchemaFormatsProvider(imodel.schemaContext, IModelApp.quantityFormatter.activeUnitSystem);
112-
FormatManager.instance.fallbackFormatsProvider = schemaFormatsProvider;
113-
const removeListener = IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener((args) => {
114-
schemaFormatsProvider.unitSystem = args.system;
115-
});
116-
await FormatManager.instance.onIModelOpen(imodel);
107+
if (FormatManager.instance) {
108+
await FormatManager.instance?.onIModelOpen(imodel);
109+
} else {
110+
const schemaFormatsProvider = new SchemaFormatsProvider(imodel.schemaContext, IModelApp.quantityFormatter.activeUnitSystem);
111+
IModelApp.formatsProvider = schemaFormatsProvider;
112+
}
117113
console.log("Registered SchemaFormatsProvider, SchemaUnitProvider");
118-
console.log(FormatManager.instance.activeFormatSet)
119114
IModelConnection.onClose.addOnce(() => {
120-
removeListener();
121115
IModelApp.resetFormatsProvider();
122116
void IModelApp.quantityFormatter.resetToUseInternalUnitsProvider();
117+
if (FormatManager.instance) void FormatManager.instance.onIModelClose();
123118
console.log("Unregistered SchemaFormatsProvider, SchemaUnitProvider");
124119
});
125120
} catch (err) {
126121
console.error("Error while setting up formats provider:", err);
127122
}
128123
};
129124

130-
// Only load a schema formats provider if the iModel has the AecUnits schema
131125
void setupFormatsProvider();
132126
}
133127

packages/itwin/quantity-formatting/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,16 @@
5050
"react-dom": "^18.0.0"
5151
},
5252
"devDependencies": {
53+
"@itwin/appui-abstract": "^5.1.1",
5354
"@itwin/build-tools": "^5.1.1",
5455
"@itwin/core-bentley": "^5.1.1",
5556
"@itwin/core-common": "^5.1.1",
5657
"@itwin/core-frontend": "^5.1.1",
58+
"@itwin/core-geometry": "^5.1.1",
59+
"@itwin/core-orbitgt": "^5.1.1",
5760
"@itwin/core-quantity": "^5.1.1",
5861
"@itwin/ecschema-metadata": "^5.1.1",
62+
"@itwin/ecschema-rpcinterface-common": "^5.1.1",
5963
"@itwin/eslint-plugin": "^5.2.1",
6064
"@itwin/itwinui-react": "^3.19.3",
6165
"@testing-library/react": "^16.3.0",

0 commit comments

Comments
 (0)