/** @jsx jsx */ /** @jsxFrag React.Fragment */ /** * Items pertaining to extent data. */ import update from 'immutability-helper'; import { jsx, css, ClassNames } from '@emotion/react'; import React from 'react'; import { InputGroup, ControlGroup, FormGroup, Button, TextArea, MenuItem, Tag, ProgressBar, Colors, } from '@blueprintjs/core'; import { Select2 as Select, type ItemRenderer } from '@blueprintjs/select'; import { Tooltip2 as Tooltip } from '@blueprintjs/popover2'; import { useDebounce, DatasetContext } from '@riboseinc/paneron-extension-kit'; import { PropertyDetailView } from '@riboseinc/paneron-registry-kit'; import type { ItemClassConfiguration, ItemListView, } from '@riboseinc/paneron-registry-kit/types'; import { type CommonGRItemData, DEFAULTS as SHARED_DEFAULTS, COMMON_PROPERTIES, EditView as CommonEditView, ListItemView as CommonListItemView, RelatedItem, } from './common'; interface ExtentBoundingPolygonPoint { /** Latitude (+/-90, Northern hemisphere positive) */ lat: number /** Longitude (+/-180, Eastern hemisphere positive) */ lon: number } export interface Extent { n: string e: string s: string w: string name: string polygonPoints?: ExtentBoundingPolygonPoint[] /** Start date on which item is valid. */ temporalExtentStartDate?: string /** End date on which item stops being valid. */ temporalExtentEndDate?: string } /** A.k.a. “domain of validity”. */ export interface ExtentItemData extends CommonGRItemData { extent: Extent } const EXTENT_COORDS = ['n', 'e', 's', 'w'] as const; function isExtent(val: any): val is Extent { return ( val && EXTENT_COORDS.every(c => val.hasOwnProperty(c)) && (EXTENT_COORDS.every(c => typeof val[c] === 'string') || EXTENT_COORDS.every(c => typeof val[c] === 'number')) // for backwards compatibility && val.hasOwnProperty('name') && typeof val.name === 'string' ); } /** Placeholder/stub extent value. */ export const DEFAULT_EXTENT: Extent = { name: '', n: '0', e: '0', s: '0', w: '0' } as const; export const EXTENT_DEFAULTS: ExtentItemData = { ...SHARED_DEFAULTS, extent: DEFAULT_EXTENT, } as const; /** * A widget for editing extent data. */ export const ExtentEdit: React.FC<{ extent: Extent, onChange?: (ext: Extent) => void }> = function ({ extent, onChange }) { const isStub = JSON.stringify(extent) === JSON.stringify(DEFAULT_EXTENT); const { getMapReducedData } = React.useContext(DatasetContext); const [_allImportableExtents, setAllImportableExtents] = React.useState(undefined); if (!isExtent(extent)) { throw new Error("Item given to ExtentEdit is not an extent"); } const allImportableExtents = useDebounce(_allImportableExtents, 400); const extentsAreLoading = allImportableExtents === undefined; async function loadImportableExtents() { const data = await getMapReducedData({ chains: { extents: { mapFunc: EXTENT_MAP_FUNC, reduceFunc: EXTENT_REDUCE_FUNC, } } }); const result = (data.extents ?? []).filter(isSuggestedExtentListItem); if (result.length < 1) { throw new Error("No extents were found across items in the register"); } else { return data.extents; } } React.useEffect(() => { let cancelled = false; async function importExtents() { setAllImportableExtents(undefined); try { const result = await loadImportableExtents(); if (cancelled) return; setAllImportableExtents(result); } catch (e) { if (cancelled) return; setAllImportableExtents([]); } } importExtents(); return function cleanUp() { cancelled = true; }; }, []); function handleImportSuggestedExtent(item: SuggestedExtentListItem) { if (onChange && isExtent(item.extentData)) { onChange({ ...item.extentData, n: `${item.extentData.n}`, e: `${item.extentData.e}`, s: `${item.extentData.s}`, w: `${item.extentData.w}`, }); } } function extentInput(side: 'n' | 'e' | 's' | 'w') { const nm = side === 'n' || side === 's' ? 'Lat' : 'Lon'; return ( onChange!({ ...extent, [side]: val }) : undefined} label={<>{side.toUpperCase()} {nm}} value={extent[side] ?? 0} /> ); } return ( <>