/** @jsx jsx */ /** @jsxFrag React.Fragment */ import update from 'immutability-helper'; import React from 'react'; import { jsx } from '@emotion/react'; import { ControlGroup, InputGroup } from '@blueprintjs/core'; import { PropertyDetailView } from '@riboseinc/paneron-registry-kit/views/util'; import { itemRefToItemPath } from '@riboseinc/paneron-registry-kit/views/itemPathUtils'; //import { BrowserCtx } from '@riboseinc/paneron-registry-kit/views/BrowserCtx'; import type { RegisterItem, InternalItemReference, ItemClassConfiguration, ItemListView } from '@riboseinc/paneron-registry-kit/types'; import { type Extent, CombinedExtentWidget, DEFAULT_EXTENT, } from './extent'; import { type CommonGRItemData, DEFAULTS as SHARED_DEFAULTS, EditView as CommonEditView, ListItemView as CommonListItemView, COMMON_PROPERTIES, ItemList, RelatedItem, type Accuracy, AccuracyEdit, ACCURACY_STUB, } from './common'; import type { TransformationData } from './transformation'; import type { ConversionData } from './conversion'; export interface ConcatenatedOperationData extends CommonGRItemData { /** * Ordered list of references to single operations. * Must contain at least two. */ operations: Readonly scope: string extent: Extent extentRef?: string operationVersion: string accuracy: Accuracy /** Source CRS must match the first operation’s source CRS. */ sourceCRS?: InternalItemReference /** Target CRS must match the last operation’s target CRS. */ targetCRS?: InternalItemReference } export const concatenatedOperation: ItemClassConfiguration = { ...COMMON_PROPERTIES, meta: { title: "Concatenated Operation", description: "Coordinate Operations — Concatenated Operation", id: 'coordinate-ops--concatenated', alternativeNames: [] as const, }, defaults: { ...SHARED_DEFAULTS, operationVersion: '', extent: DEFAULT_EXTENT, operations: [], accuracy: ACCURACY_STUB, }, views: { listItemView: CommonListItemView as ItemListView, editView: function ConcatenatedOperationEditView ({ itemData, onChange, ...props }) { const operationItemPaths = itemData.operations.map(ref => itemRefToItemPath(ref)); const operationItemUUIDs = itemData.operations.map(ref => ref.itemID); /** Item data for every linked single operation. */ const operationItemData = (typeof props.useRegisterItemData === 'function' ? props.useRegisterItemData({ itemPaths: operationItemPaths, }).value : {} ) as Record | null>; // NOTE: We cannot obtain the above useRegisterItemData via context, apparently, // so we pass it as prop again. /** * [source, target] CRS item paths for every linked single operation. * Operations with either CRS missing are omitted. */ const crsByOperation: Record = React.useMemo(() => Object.values(operationItemData). filter(op => op !== null && op !== undefined). map(op => { let crsPaths: [string, string] = ['', '']; if (op!.data) { const opData = op!.data as any; crsPaths = [ opData.sourceCRS ? itemRefToItemPath(opData.sourceCRS) : '', opData.targetCRS ? itemRefToItemPath(opData.targetCRS) : '', ]; } return [op!.id, crsPaths] as [string, [string, string]]; }). map(([opID, paths]) => ({ [opID]: [paths[0], paths[1]] as [string, string] }) ). reduce((prev, curr) => ({ ...prev, ...curr }), {}), [operationItemData]); /** * Maps each operation item UUID to custom validation error * (empty string if no error). */ const operationValidationErrors: Record = React.useMemo(() => { const errors: Record = {}; let previousTarget: string | null = null; for (const opUUID of operationItemUUIDs.filter(uuid => uuid !== '')) { if (crsByOperation[opUUID]) { const [currentSource, currentTarget] = crsByOperation[opUUID]; if (currentSource && currentTarget) { if (!previousTarget) { previousTarget = currentTarget; errors[opUUID] = ""; } else { if (currentSource !== previousTarget) { errors[opUUID] = "Source CRS of this operation does not match target CRS of previous operation"; } else { errors[opUUID] = ""; } } } else { errors[opUUID] = "This operation has no target and/or no source CRS specified."; } } else { console.warn("Source/target CRS for operation with UUID were not retrieved:", opUUID); errors[opUUID] = "Unable to fetch source/target CRS for this operation."; } } return errors; }, [crsByOperation, operationItemUUIDs.toString()]); return ( <> { if (!onChange) { return; } onChange({ ...itemData, ...newData }); } : undefined}> onChange!(update(itemData, { $unset: ['sourceCRS'] })))} onSet={onChange ? ((spec) => onChange!(update(itemData, { sourceCRS: spec }))) : undefined} classIDs={['crs--vertical', 'crs--geodetic']} /> onChange!(update(itemData, { $unset: ['targetCRS'] })))} onSet={onChange ? ((spec) => onChange!(update(itemData, { targetCRS: spec }))) : undefined} classIDs={['crs--vertical', 'crs--geodetic']} /> onChange!(update(itemData, { extentRef: { $set: ref } })) : undefined} /> onChange(update(itemData, { accuracy: { $set: accuracy } } ))) : undefined} /> ) => { onChange ? onChange(update(itemData, { scope: { $set: evt.currentTarget.value } })) : void 0; }} /> For example, GA v2}> ) => { onChange ? onChange(update(itemData, { operationVersion: { $set: evt.currentTarget.value } })) : void 0; }} /> onChange!(update(itemData, { operations: spec })) : undefined} placeholderItem={{ classID: 'coordinate-ops--transformation', itemID: '' }} simpleItems itemRenderer={(opRef, _idx, handleChange, deleteButton) => handleChange!(spec) : undefined} /> {deleteButton} } /> ) }, }, validatePayload: async () => true, sanitizePayload: async (t) => t, } as const;