From ecc5e713068d50abe139a8bfb2d81efe39e0f41b Mon Sep 17 00:00:00 2001 From: Grigas <35135765+grigasp@users.noreply.github.com> Date: Mon, 16 Oct 2023 14:42:03 +0300 Subject: [PATCH] Implement station value renderer for 3 situations: "at", "from" and "to" (#146) * Implement station value renderer for 3 situations: "at", "from" and "to". * lint --- .../StationPropertyValueRenderer.tsx | 263 ++++++++++++------ 1 file changed, 178 insertions(+), 85 deletions(-) diff --git a/app/frontend/src/app/experimental/StationPropertyValueRenderer.tsx b/app/frontend/src/app/experimental/StationPropertyValueRenderer.tsx index f70602e..908545b 100644 --- a/app/frontend/src/app/experimental/StationPropertyValueRenderer.tsx +++ b/app/frontend/src/app/experimental/StationPropertyValueRenderer.tsx @@ -8,18 +8,23 @@ import * as React from "react"; import { PropertyRecord } from "@itwin/appui-abstract"; import { IPropertyValueRenderer, PropertyValueRendererContext, PropertyValueRendererManager, useDebouncedAsyncValue } from "@itwin/components-react"; import { Id64String } from "@itwin/core-bentley"; -import { QueryBinder, QueryRowFormat } from "@itwin/core-common"; +import { ECSqlReader, QueryBinder, QueryRowFormat } from "@itwin/core-common"; import { IModelConnection } from "@itwin/core-frontend"; import { Format, Formatter, FormatterSpec } from "@itwin/core-quantity"; import { ECVersion, SchemaContext, Format as SchemaFormat, SchemaKey, SchemaUnitProvider } from "@itwin/ecschema-metadata"; import { ECSchemaRpcLocater } from "@itwin/ecschema-rpcinterface-common"; +import { Text } from "@itwin/itwinui-react"; import { KeySet } from "@itwin/presentation-common"; import { useUnifiedSelectionContext } from "@itwin/presentation-components"; import { Presentation } from "@itwin/presentation-frontend"; +type StationValueType = "from" | "to" | "at"; + Presentation.registerInitializationHandler(async (): Promise<() => void> => { const customRenderers: Array<{ name: string, renderer: IPropertyValueRenderer }> = [ - { name: "Station", renderer: new StationPropertyValueRenderer() }, + { name: "AtStation", renderer: new StationPropertyValueRenderer("at") }, + { name: "FromStation", renderer: new StationPropertyValueRenderer("from") }, + { name: "ToStation", renderer: new StationPropertyValueRenderer("to") }, ]; for (const { name, renderer } of customRenderers) { @@ -38,49 +43,63 @@ Presentation.registerInitializationHandler(async (): Promise<() => void> => { * @alpha */ export class StationPropertyValueRenderer implements IPropertyValueRenderer { + public constructor(private _type: StationValueType) {} + public canRender(record: PropertyRecord) { - return record.property.renderer?.name === "Station"; + return ["AtStation", "FromStation", "ToStation"].some((rendererName) => rendererName === record.property.renderer?.name); } public render(_record: PropertyRecord, context?: PropertyValueRendererContext) { - return ; + return ; } } -function StationPropertyValueRendererImpl(props: { context?: PropertyValueRendererContext }) { - const { value: iModelSelectedElementIds, inProgress } = useIModelSelectedElementIds(); - - if (inProgress) - return null; - - if (!iModelSelectedElementIds) - return <>Error: No selection context?; - - if (iModelSelectedElementIds.elementIds.length === 0) - return <>No elements selected; +function StationPropertyValueRendererImpl(props: { type: StationValueType, context?: PropertyValueRendererContext }) { + const { imodel, elementIds, inProgress: elementIdsInProgress } = useIModelSelectedElementIds(); + const { value, inProgress: valueInProgress } = useComputedStationValue({ imodel, elementId: elementIds ? elementIds[0] : undefined, type: props.type }); - if (iModelSelectedElementIds.elementIds.length > 1) - return <>Error: Only 1 element supported; + if (elementIdsInProgress || valueInProgress) { + return ( + + ); + } - return ; + return ( + {value} + ); } -function ElementsStationPropertyValue(props: { imodel: IModelConnection, elementId: Id64String, context?: PropertyValueRendererContext }) { - const { value } = useComputedStationValue({ imodel: props.imodel, elementId: props.elementId }); +const sampleValues = [ + "0+0", + "1+1.23", + "3+616.55", + "13+416.59", +]; +function StationValueSkeleton() { + const sampleValue = React.useMemo(() => sampleValues[Math.round(Math.random() * (sampleValues.length - 1))], []); return ( - {value} + {sampleValue} ); } function useIModelSelectedElementIds() { + // Ideally, we's like to get element id a the raw property value for this renderer. Sadly, that's + // currently not possible, so we use unified selection context. const selectionContext = useUnifiedSelectionContext(); - return useDebouncedAsyncValue(React.useCallback(async () => { - if (!selectionContext) + // The context changes on every selection, but we don't want to re-load property value on selection + // change. Instead, we want to reload it only when the component is re-mounted (generally, when property + // grid reloads due to selection change). + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const selectionContextOnce = React.useMemo(() => selectionContext, []); + const elementIds = useDebouncedAsyncValue(React.useCallback(async () => { + if (!selectionContextOnce) { + console.error("UnifiedSelectionContext is not available."); return undefined; + } const res = await Presentation.presentation.getContentInstanceKeys({ - imodel: selectionContext.imodel, - keys: new KeySet(selectionContext.getSelection()), + imodel: selectionContextOnce.imodel, + keys: new KeySet(selectionContextOnce.getSelection()), rulesetOrId: { id: "selected-elements", rules: [{ @@ -94,25 +113,35 @@ function useIModelSelectedElementIds() { }], }, }); + /* eslint-disable-next-line @typescript-eslint/no-shadow */ const elementIds = new Array(); for await (const key of res.items()) { elementIds.push(key.id); } - return { imodel: selectionContext.imodel, elementIds }; - }, [selectionContext])); + return elementIds; + }, [selectionContextOnce])); + return { + imodel: selectionContext?.imodel, + inProgress: elementIds.inProgress, + elementIds: elementIds.value, + }; } -function useComputedStationValue(props: { imodel: IModelConnection, elementId: Id64String }) { - const schemaContext = React.useMemo(() => { +function useComputedStationValue(props: { imodel?: IModelConnection, elementId?: Id64String, type: StationValueType }) { + const { schemas, units } = React.useMemo(() => { + // note: the schema context should be stored way above in the components hierarchy to avoid re-creating it on + // each property load const ctx = new SchemaContext(); - ctx.addLocater(new ECSchemaRpcLocater(props.imodel.getRpcProps())); - return ctx; + if (props.imodel) { + ctx.addLocater(new ECSchemaRpcLocater(props.imodel.getRpcProps())); + } + const unitsProvider = new SchemaUnitProvider(ctx); + return { schemas: ctx, units: unitsProvider }; }, [props.imodel]); const formatterSpec = useDebouncedAsyncValue(React.useCallback(async () => { - const unitsProvider = new SchemaUnitProvider(schemaContext); - const persistenceUnit = await unitsProvider.findUnitByName("Units:M"); - const formatsSchema = await schemaContext.getSchema(new SchemaKey("Formats", new ECVersion(1))); + const persistenceUnit = await units.findUnitByName("Units:M"); + const formatsSchema = await schemas.getSchema(new SchemaKey("Formats", new ECVersion(1))); if (!formatsSchema) { console.error(`Failed to find "Formats" schema.`); return undefined; @@ -122,59 +151,123 @@ function useComputedStationValue(props: { imodel: IModelConnection, elementId: I console.error(`Failed to find the "StationZ_1000_3" format in "Formats" schema.`); return undefined; } - return FormatterSpec.create("", await Format.createFromJSON("", unitsProvider, schemaFormat.toJSON()), unitsProvider, persistenceUnit); - }, [schemaContext])); - - return useDebouncedAsyncValue(React.useCallback(async () => { - if (formatterSpec.inProgress) - return null; - - const ecsql = ` - SELECT - linearlyLocated.DistanceAlongFromStart DistanceAlong, - ( - COALESCE(station.Station, alg.StartStation) + ( - linearlyLocated.DistanceAlongFromStart - COALESCE( - stationAt.AtPosition.DistanceAlongFromStart, - alg.StartValue, - 0 - ) - ) - ) StationValue - FROM - rralign.Alignment alg - JOIN ( - SELECT - along.TargetECInstanceId LinearElementId, - fromTo.FromPosition.DistanceAlongFromStart DistanceAlongFromStart - FROM - lr.ILinearlyLocatedAlongILinearElement along - JOIN lr.LinearlyReferencedFromToLocation fromTo ON along.SourceECInstanceId = fromTo.Element.Id - WHERE - fromTo.Element.Id = ? - ) linearlyLocated ON alg.ECInstanceId = linearlyLocated.LinearElementId - LEFT JOIN rralign.AlignmentStation station ON alg.ECInstanceId = station.Parent.Id - LEFT JOIN lr.LinearlyReferencedAtLocation stationAt ON station.ECInstanceId = stationAt.Element.Id - WHERE - station.ECInstanceId IS NULL - OR stationAt.AtPosition.DistanceAlongFromStart <= linearlyLocated.DistanceAlongFromStart - ORDER BY - stationAt.AtPosition.DistanceAlongFromStart DESC - LIMIT - 1 - `; - const queryReader = props.imodel.createQueryReader(ecsql, (new QueryBinder()).bindId(1, props.elementId), { rowFormat: QueryRowFormat.UseECSqlPropertyIndexes }); + return FormatterSpec.create("", await Format.createFromJSON("", units, schemaFormat.toJSON()), units, persistenceUnit); + }, [schemas, units])); + + const displayValue = useDebouncedAsyncValue(React.useCallback(async () => { + if (!props.imodel || !props.elementId || formatterSpec.inProgress) + return undefined; + + const queryReader = createStationValueReader(props.imodel, props.elementId, props.type); if (!await queryReader.step()) - return ""; + return ""; - const queryResult = { - distanceAlong: queryReader.current[0], - stationValue: queryReader.current[1], - }; + const distanceAlong = queryReader.current[0]; + const stationValue = queryReader.current[1]; + return formatterSpec.value + ? Formatter.formatQuantity(stationValue, formatterSpec.value) + : `Distance along: ${distanceAlong}; \nStation value: ${stationValue}`; + }, [formatterSpec.inProgress, formatterSpec.value, props.imodel, props.elementId, props.type])); - if (!formatterSpec.value) - return `Distance along: ${queryResult.distanceAlong}; \nStation value: ${queryResult.stationValue}`; + return { + inProgress: formatterSpec.inProgress || displayValue.inProgress, + value: displayValue.value, + }; +} - return Formatter.formatQuantity(queryResult.stationValue, formatterSpec.value); - }, [formatterSpec.inProgress, formatterSpec.value, props.imodel, props.elementId])); +function createStationValueReader(imodel: IModelConnection, elementId: Id64String, type: StationValueType): ECSqlReader { + function createLinearlyLocatedQuery() { + switch (type) { + case "at": + return ` + SELECT + COALESCE(lrOnObj.LinearElementId, lrOnSisterObj.LinearElementId) LinearElementId, + COALESCE(lrOnObj.DistanceAlongFromStart, lrOnSisterObj.DistanceAlongFromStart) DistanceAlongFromStart + FROM ( + SELECT + along.TargetECInstanceId LinearElementId, + at.AtPosition.DistanceAlongFromStart DistanceAlongFromStart + FROM lr.ILinearlyLocatedAlongILinearElement along + JOIN lr.LinearlyReferencedAtLocation at ON along.SourceECInstanceId = at.Element.Id + WHERE along.SourceECInstanceId = :elementId + ) lrOnObj + FULL JOIN ( + SELECT + along.TargetECInstanceId LinearElementId, + at.AtPosition.DistanceAlongFromStart DistanceAlongFromStart + FROM lr.ILinearlyLocatedAlongILinearElement along + JOIN lr.LinearlyReferencedAtLocation at ON along.SourceECInstanceId = at.Element.Id + JOIN lr.ILinearLocationLocatesElement locates ON locates.SourceECInstanceId = along.SourceECInstanceId + WHERE locates.TargetECInstanceId = :elementId + ) lrOnSisterObj ON lrOnObj.LinearElementId = lrOnSisterObj.LinearElementId + LIMIT 1 + `; + case "from": + return ` + SELECT + COALESCE(lrOnObj.LinearElementId, lrOnSisterObj.LinearElementId) LinearElementId, + COALESCE(lrOnObj.DistanceAlongFromStart, lrOnSisterObj.DistanceAlongFromStart) DistanceAlongFromStart + FROM ( + SELECT + along.TargetECInstanceId LinearElementId, + fromTo.FromPosition.DistanceAlongFromStart DistanceAlongFromStart + FROM lr.ILinearlyLocatedAlongILinearElement along + JOIN lr.LinearlyReferencedFromToLocation fromTo ON along.SourceECInstanceId = fromTo.Element.Id + WHERE along.SourceECInstanceId = :elementId + ) lrOnObj + FULL JOIN ( + SELECT + along.TargetECInstanceId LinearElementId, + fromTo.FromPosition.DistanceAlongFromStart DistanceAlongFromStart + FROM lr.ILinearlyLocatedAlongILinearElement along + JOIN lr.LinearlyReferencedFromToLocation fromTo ON along.SourceECInstanceId = fromTo.Element.Id + JOIN lr.ILinearLocationLocatesElement locates ON locates.SourceECInstanceId = along.SourceECInstanceId + WHERE locates.TargetECInstanceId = :elementId + ) lrOnSisterObj ON lrOnObj.LinearElementId = lrOnSisterObj.LinearElementId + LIMIT 1 + `; + case "to": + return ` + SELECT + COALESCE(lrOnObj.LinearElementId, lrOnSisterObj.LinearElementId) LinearElementId, + COALESCE(lrOnObj.DistanceAlongFromStart, lrOnSisterObj.DistanceAlongFromStart) DistanceAlongFromStart + FROM ( + SELECT + along.TargetECInstanceId LinearElementId, + fromTo.ToPosition.DistanceAlongFromStart DistanceAlongFromStart + FROM lr.ILinearlyLocatedAlongILinearElement along + JOIN lr.LinearlyReferencedFromToLocation fromTo ON along.SourceECInstanceId = fromTo.Element.Id + WHERE along.SourceECInstanceId = :elementId + ) lrOnObj + FULL JOIN ( + SELECT + along.TargetECInstanceId LinearElementId, + fromTo.ToPosition.DistanceAlongFromStart DistanceAlongFromStart + FROM lr.ILinearlyLocatedAlongILinearElement along + JOIN lr.LinearlyReferencedFromToLocation fromTo ON along.SourceECInstanceId = fromTo.Element.Id + JOIN lr.ILinearLocationLocatesElement locates ON locates.SourceECInstanceId = along.SourceECInstanceId + WHERE locates.TargetECInstanceId = :elementId + ) lrOnSisterObj ON lrOnObj.LinearElementId = lrOnSisterObj.LinearElementId + LIMIT 1 + `; + } + } + const ecsql = ` + SELECT + linearlyLocated.DistanceAlongFromStart DistanceAlong, + COALESCE(station.Station, alg.StartStation) + (linearlyLocated.DistanceAlongFromStart - COALESCE(stationAt.AtPosition.DistanceAlongFromStart, alg.StartValue, 0)) StationValue + FROM rralign.Alignment alg + JOIN (${createLinearlyLocatedQuery()}) linearlyLocated ON alg.ECInstanceId = linearlyLocated.LinearElementId + LEFT JOIN rralign.AlignmentStation station ON alg.ECInstanceId = station.Parent.Id + LEFT JOIN lr.LinearlyReferencedAtLocation stationAt ON station.ECInstanceId = stationAt.Element.Id + WHERE + station.ECInstanceId IS NULL + OR stationAt.AtPosition.DistanceAlongFromStart <= linearlyLocated.DistanceAlongFromStart + ORDER BY + stationAt.AtPosition.DistanceAlongFromStart DESC + LIMIT + 1 + `; + const bindings = (new QueryBinder()).bindId("elementId", elementId); + return imodel.createQueryReader(ecsql, bindings, { rowFormat: QueryRowFormat.UseECSqlPropertyIndexes }); }