Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement station value renderer for 3 situations: "at", "from" and "to" #146

Merged
merged 2 commits into from
Oct 16, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 178 additions & 85 deletions app/frontend/src/app/experimental/StationPropertyValueRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 <StationPropertyValueRendererImpl context={context} />;
return <StationPropertyValueRendererImpl context={context} type={this._type} />;
}
}

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 (
<StationValueSkeleton />
);
}

return <ElementsStationPropertyValue imodel={iModelSelectedElementIds.imodel} elementId={iModelSelectedElementIds.elementIds[0]} context={props.context} />;
return (
<Text style={props.context?.style} title={value ?? ""}>{value}</Text>
);
}

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 (
<span style={props.context?.style} title={value ?? ""}>{value}</span>
<Text isSkeleton={true} title="Loading...">{sampleValue}</Text>
);
}

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: [{
Expand All @@ -94,25 +113,35 @@ function useIModelSelectedElementIds() {
}],
},
});
/* eslint-disable-next-line @typescript-eslint/no-shadow */
const elementIds = new Array<Id64String>();
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;
Expand All @@ -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 "<no value>";
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 });
}
Loading