From b9b6d1ca97e29c2cc5f5e23079980f5d56922ced Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Thu, 11 Sep 2025 06:01:19 -0700 Subject: [PATCH] feat: configurable autoLoad on search --- src/components/filter.tsx | 92 --------------- src/components/map.tsx | 7 +- src/components/search/item.tsx | 169 +++++++++++++++------------ src/context.ts | 90 +++++++------- src/hooks/stac-children-and-items.ts | 100 ---------------- src/hooks/stac-value.ts | 151 ++++++++++++++++++++---- src/provider.tsx | 135 +++++---------------- src/types/datetime.d.ts | 4 + src/types/stac.d.ts | 2 + 9 files changed, 305 insertions(+), 445 deletions(-) delete mode 100644 src/components/filter.tsx delete mode 100644 src/hooks/stac-children-and-items.ts create mode 100644 src/types/datetime.d.ts diff --git a/src/components/filter.tsx b/src/components/filter.tsx deleted file mode 100644 index d3e1788..0000000 --- a/src/components/filter.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { - Center, - DataList, - Heading, - HStack, - Slider, - Stack, - Text, -} from "@chakra-ui/react"; -import { useState } from "react"; -import useStacMap from "../hooks/stac-map"; -import DownloadButtons from "./download"; - -export default function Filter({ - temporalExtents, -}: { - temporalExtents: { start: Date; end: Date }; -}) { - const { setTemporalFilter, filteredItems } = useStacMap(); - const [start, setStart] = useState(); - const [end, setEnd] = useState(); - - return ( - - Temporal extents - - - Start - - {temporalExtents.start.toLocaleString()} - - - - End - - {temporalExtents.end.toLocaleString()} - - - - - { - const start = e.value[0]; - const end = e.value[1]; - setStart(start); - setEnd(end); - setTemporalFilter({ - start: new Date(temporalExtents.start.getTime() + start * 1000), - end: new Date(temporalExtents.start.getTime() + end * 1000), - }); - }} - > - Temporal filter - - - - - - - - {start !== undefined && end !== undefined && ( -
- - {new Date( - temporalExtents.start.getTime() + start * 1000, - ).toLocaleString()}{" "} - to{" "} - {new Date( - temporalExtents.start.getTime() + end * 1000, - ).toLocaleString()} - -
- )} - - {filteredItems && filteredItems.length > 0 && ( - - )} - -
- ); -} diff --git a/src/components/map.tsx b/src/components/map.tsx index 7e40cc2..4143148 100644 --- a/src/components/map.tsx +++ b/src/components/map.tsx @@ -49,7 +49,6 @@ export default function Map() { stacGeoparquetTable, stacGeoparquetMetadata, setStacGeoparquetItemId, - filteredItems, fileUpload, } = useStacMap(); const { @@ -95,11 +94,11 @@ export default function Map() { }), new GeoJsonLayer({ id: "items", - data: filteredItems as Feature[] | undefined, + data: items as Feature[] | undefined, filled: true, stroked: true, getFillColor: fillColor, - getLineColor: fillColor, + getLineColor: lineColor, lineWidthUnits: "pixels", getLineWidth: 2, pickable: true, @@ -110,7 +109,7 @@ export default function Map() { new GeoJsonLayer({ id: "value", data: geojson, - filled: filled && !picked && !items, + filled: filled && !picked && (!items || items.length == 0), stroked: true, getFillColor: fillColor, getLineColor: lineColor, diff --git a/src/components/search/item.tsx b/src/components/search/item.tsx index 3b6b654..16a4cb1 100644 --- a/src/components/search/item.tsx +++ b/src/components/search/item.tsx @@ -3,6 +3,7 @@ import { Alert, Button, ButtonGroup, + Checkbox, createListCollection, Field, Group, @@ -14,21 +15,25 @@ import { Portal, Progress, Select, + Spinner, Stack, Switch, Text, } from "@chakra-ui/react"; import type { BBox } from "geojson"; import { useEffect, useState } from "react"; -import { LuPause, LuPlay, LuSearch, LuX } from "react-icons/lu"; +import { LuPause, LuPlay, LuSearch, LuStepForward, LuX } from "react-icons/lu"; import { useMap } from "react-map-gl/maplibre"; -import type { StacCollection, StacLink, TemporalExtent } from "stac-ts"; +import type { + StacCollection, + StacItem, + StacLink, + TemporalExtent, +} from "stac-ts"; import useStacMap from "../../hooks/stac-map"; import useStacSearch from "../../hooks/stac-search"; import type { StacSearch } from "../../types/stac"; -import DownloadButtons from "../download"; import { SpatialExtent } from "../extents"; -import { toaster } from "../ui/toaster"; interface NormalizedBbox { bbox: BBox; @@ -42,12 +47,12 @@ export default function ItemSearch({ collection: StacCollection; links: StacLink[]; }) { - const { setItems } = useStacMap(); - const [search, setSearch] = useState(); const [link, setLink] = useState(links[0]); const [normalizedBbox, setNormalizedBbox] = useState(); const [datetime, setDatetime] = useState(); const [useViewportBounds, setUseViewportBounds] = useState(true); + const { search, setSearch } = useStacMap(); + const [autoLoad, setAutoLoad] = useState(false); const { map } = useMap(); const methods = createListCollection({ @@ -78,7 +83,7 @@ export default function ItemSearch({ }, [map, useViewportBounds]); return ( - + - + Spatial - + - + Temporal - + Search @@ -171,7 +175,6 @@ export default function ItemSearch({ onValueChange={(e) => setLink(links.find((link) => (link.method || "GET") == e.value)) } - disabled={!!search} maxW={100} > @@ -196,16 +199,24 @@ export default function ItemSearch({ + setAutoLoad(!!e.checked)} + > + + + + + Auto-load? + {search && link && ( { - setSearch(undefined); - setItems(undefined); - }} + autoLoad={autoLoad} + setAutoLoad={setAutoLoad} > )} @@ -215,81 +226,87 @@ export default function ItemSearch({ function Results({ search, link, - doClear, + autoLoad, + setAutoLoad, }: { search: StacSearch; link: StacLink; - doClear: () => void; + autoLoad: boolean; + setAutoLoad: (autoLoad: boolean) => void; }) { - const { items, setItems } = useStacMap(); - const { data, isFetchingNextPage, hasNextPage, fetchNextPage, error } = - useStacSearch(search, link); - const [pause, setPause] = useState(false); + const results = useStacSearch(search, link); + const [items, setItems] = useState(); + const { setSearch, setSearchItems } = useStacMap(); useEffect(() => { - setItems(data?.pages.flatMap((page) => page.features)); - }, [data, setItems]); + setItems(results.data?.pages.flatMap((page) => page.features)); + }, [results.data]); useEffect(() => { - if (!isFetchingNextPage && !pause && hasNextPage) { - fetchNextPage(); + if (autoLoad && !results.isFetching && results.hasNextPage) { + results.fetchNextPage(); } - }, [isFetchingNextPage, pause, hasNextPage, fetchNextPage]); + }, [results, autoLoad]); useEffect(() => { - if (error) { - toaster.create({ - type: "error", - title: "Search error", - description: error.toString(), - }); - doClear(); - } - }, [error, doClear]); + setSearchItems(items); + }, [items, setSearchItems]); - return ( - - - - - - - - {items?.length || "0"} + const numberMatched = results.data?.pages[0].numberMatched; + const value = items?.length || 0; - - {(pause && ( - setPause(false)}> - - - )) || ( - setPause(true)} - > - - - )} - - - - - - - - {items && items.length > 0 && ( + return ( + + - + + + + + {items?.length || 0} / {numberMatched || "?"} + + + + + results.fetchNextPage()} + > + + + setAutoLoad(!autoLoad)} + disabled={!results.hasNextPage} + > + {(autoLoad && ) || } + + { + setSearch(undefined); + }} + > + + + + {((autoLoad && results.hasNextPage) || results.isFetching) && ( + + )} + + {results.error && ( + + + + Error while searching + {results.error.toString()} + + )} - + ); } diff --git a/src/context.ts b/src/context.ts index 3ecd92b..86d9062 100644 --- a/src/context.ts +++ b/src/context.ts @@ -2,59 +2,66 @@ import type { UseFileUploadReturn } from "@chakra-ui/react"; import type { Table } from "apache-arrow"; import { createContext } from "react"; import type { StacCatalog, StacCollection, StacItem } from "stac-ts"; -import type { StacGeoparquetMetadata, StacValue } from "./types/stac"; +import type { + StacContainer, + StacGeoparquetMetadata, + StacSearch, + StacValue, +} from "./types/stac"; export const StacMapContext = createContext(null); interface StacMapContextType { - /// The root href for the app, used to load `value`. - /// - /// This is sync'd with a url parameter. - href: string | undefined; - /// A function to set the href. setHref: (href: string | undefined) => void; - /// Is the current value a stac-geoparquet? - isStacGeoparquet: boolean; - /// A shared fileUpload structure that is the source of JSON or /// stac-geoparquet bytes. fileUpload: UseFileUploadReturn; - /// The root STAC value. - value: StacValue | undefined; + /// Set the STAC search. + setSearch: (search: StacSearch | undefined) => void; - /// The root of the STAC value. - root: StacCatalog | StacCollection | undefined; + /// Set the picked item. + setPicked: (value: StacItem | undefined) => void; - /// The parent of the STAC value. - parent: StacCatalog | StacCollection | undefined; + /// Set the id of a stac-geoparquet item that should be fetched from the + /// parquet table and loaded into the picked item. + setStacGeoparquetItemId: (id: string | undefined) => void; - /// Any catalogs that belong to the `value`. - catalogs: StacCatalog[] | undefined; + /// Set the searched items. + setSearchItems: (items: StacItem[] | undefined) => void; - /// Any collections that belong to the `value`. + /// Sets the temporal filter. + setTemporalFilter: ( + temporalFilter: { start: Date; end: Date } | undefined, + ) => void; + + /// The root href for the app, used to load `value`. /// - /// This is usually populated only if the value is a Catalog. - collections: StacCollection[] | undefined; + /// This is sync'd with a url parameter. + href: string | undefined; - /// Are we fetching pages of collections? - isFetchingCollections: boolean; + /// Is the current value stac-geoparquet? + isStacGeoparquet: boolean; - /// STAC items that belong to the value. - items: StacItem[] | undefined; + /// The root STAC value. + value: StacValue | undefined; - /// A function to set the items. - setItems: (items: StacItem[] | undefined) => void; + /// The root of the STAC value. + root: StacContainer | undefined; - /// A picked item. - /// - /// "picking" usually involves clicking on the map. - picked: StacItem | undefined; + /// The parent of the STAC value. + parent: StacContainer | undefined; - /// Set the picked item. - setPicked: (value: StacItem | undefined) => void; + /// Any catalogs linked from the value. + catalogs: StacCatalog[]; + + /// Collections either loaded from the collections endpoint or linked from the value. + collections: StacCollection[]; + + /// STAC items for visualization. + items: StacItem[] | undefined; /// The stac-geoparquet table that's currently loaded. stacGeoparquetTable: Table | undefined | null; @@ -62,18 +69,11 @@ interface StacMapContextType { /// The stac-geoparquet metadata that are currently loaded. stacGeoparquetMetadata: StacGeoparquetMetadata | undefined; - /// Set the id of a stac-geoparquet item that should be fetched from the - /// parquet table and loaded into the picked item. - setStacGeoparquetItemId: (id: string | undefined) => void; - - /// The temporal extents of the loaded data. - temporalExtents: { start: Date; end: Date } | undefined; - - /// Sets the temporal filter. - setTemporalFilter: ( - temporalFilter: { start: Date; end: Date } | undefined, - ) => void; + /// A picked item. + /// + /// "picking" usually involves clicking on the map. + picked: StacItem | undefined; - /// Items that have been filtered by the temporal filter - filteredItems: StacItem[] | undefined; + /// The active STAC search. + search: StacSearch | undefined; } diff --git a/src/hooks/stac-children-and-items.ts b/src/hooks/stac-children-and-items.ts deleted file mode 100644 index 1465475..0000000 --- a/src/hooks/stac-children-and-items.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { useInfiniteQuery, useQueries } from "@tanstack/react-query"; -import { useEffect } from "react"; -import type { StacCatalog, StacCollection, StacItem } from "stac-ts"; -import { fetchStac, fetchStacLink } from "../http"; -import type { StacCollections, StacValue } from "../types/stac"; - -import { booleanValid } from "@turf/boolean-valid"; - -export default function useStacChildrenAndItems( - value: StacValue | undefined, - href: string | undefined, -) { - const { collections, isFetching: isFetchingCollections } = - useStacCollections(value); - const { - catalogs, - collections: childCollections, - items, - warnings, - } = useStacLinks(value, href); - - return { - catalogs, - collections: collections || childCollections, - isFetchingCollections, - items, - warnings, - }; -} - -function useStacCollections(value: StacValue | undefined) { - const href = value?.links?.find((link) => link.rel == "data")?.href; - const { data, isFetching, hasNextPage, fetchNextPage } = - useInfiniteQuery({ - queryKey: ["collections", href], - enabled: !!href, - queryFn: async ({ pageParam }) => { - if (pageParam) { - // @ts-expect-error Not worth templating stuff - return await fetchStac(pageParam); - } else { - return null; - } - }, - initialPageParam: href, - getNextPageParam: (lastPage: StacCollections | null) => - lastPage?.links?.find((link) => link.rel == "next")?.href, - }); - - useEffect(() => { - if (!isFetching && hasNextPage) { - fetchNextPage(); - } - }, [isFetching, hasNextPage, fetchNextPage]); - - return { - collections: data?.pages.flatMap((page) => page?.collections || []), - isFetching, - }; -} - -function useStacLinks(value: StacValue | undefined, href: string | undefined) { - const results = useQueries({ - queries: - value?.links - ?.filter((link) => link.rel == "item" || link.rel == "child") - .map((link) => { - return { - queryKey: ["link", link, href], - queryFn: () => fetchStacLink(link, href), - }; - }) || [], - }); - const catalogs: StacCatalog[] = []; - const collections: StacCollection[] = []; - const items: StacItem[] = []; - const warnings: string[] = []; - - results.forEach((result) => { - if (result.data) { - switch (result.data.type) { - case "Catalog": - catalogs.push(result.data); - break; - case "Collection": - collections.push(result.data); - break; - case "Feature": - if (booleanValid(result.data)) { - items.push(result.data); - } else { - warnings.push(`Invalid item: ${result.data.id}`); - } - break; - } - } - }); - - return { catalogs, collections, items, warnings }; -} diff --git a/src/hooks/stac-value.ts b/src/hooks/stac-value.ts index 2e95abd..79f559c 100644 --- a/src/hooks/stac-value.ts +++ b/src/hooks/stac-value.ts @@ -1,44 +1,145 @@ import type { UseFileUploadReturn } from "@chakra-ui/react"; -import { useQuery } from "@tanstack/react-query"; +import { useInfiniteQuery, useQueries, useQuery } from "@tanstack/react-query"; import { AsyncDuckDB, isParquetFile, useDuckDb } from "duckdb-wasm-kit"; -import { fetchStac } from "../http"; -import type { StacItemCollection, StacValue } from "../types/stac"; +import { useEffect } from "react"; +import type { StacCatalog, StacCollection, StacItem, StacLink } from "stac-ts"; +import { fetchStac, fetchStacLink } from "../http"; +import type { TemporalFilter } from "../types/datetime"; +import type { + StacCollections, + StacContainer, + StacItemCollection, + StacValue, +} from "../types/stac"; -export default function useStacValue( - href: string | undefined, - fileUpload?: UseFileUploadReturn | undefined, - types?: ("Catalog" | "Collection" | "FeatureCollection" | "Feature")[], -) { +export default function useStacValue({ + href, + fileUpload, +}: { + href: string | undefined; + fileUpload?: UseFileUploadReturn; + temporalFilter?: TemporalFilter; +}): { + value?: StacValue; + parquetPath?: string; + root?: StacContainer; + parent?: StacContainer; + catalogs: StacCatalog[]; + collections: StacCollection[]; + items: StacItem[]; +} { const { db } = useDuckDb(); - const { data } = useQuery<{ + const { data } = useStacValueQuery({ href, fileUpload, db }); + const { data: rootData } = useStacValueQuery({ + href: data?.value.links?.find((link) => link.rel == "root")?.href, + }); + const { data: parentData } = useStacValueQuery({ + href: data?.value.links?.find((link) => link.rel == "parent")?.href, + }); + const { values: children } = useStacValues( + !data?.value.links?.find((link) => link.rel == "data") + ? data?.value.links?.filter((link) => link.rel == "child") + : undefined, + ); + const { values: items } = useStacValues( + data?.value.links?.filter((link) => link.rel == "item"), + ); + const { collections } = useStacCollections(data?.value); + + return { + value: data?.value, + parquetPath: data?.parquetPath, + root: + rootData?.value?.type == "Catalog" || + rootData?.value?.type == "Collection" + ? rootData.value + : undefined, + parent: + parentData?.value?.type == "Catalog" || + parentData?.value?.type == "Collection" + ? parentData.value + : undefined, + catalogs: children.filter((child) => child.type == "Catalog"), + collections: + collections || children.filter((child) => child.type == "Collection"), + items: items.filter((item) => item.type == "Feature"), + }; +} + +function useStacValueQuery({ + href, + fileUpload, + db, +}: { + href: string | undefined; + fileUpload?: UseFileUploadReturn; + db?: AsyncDuckDB; +}) { + return useQuery<{ value: StacValue; parquetPath: string | undefined; } | null>({ queryKey: ["stac-value", href, fileUpload?.acceptedFiles], queryFn: async () => { - if (href && db) { + if (href) { return await getStacValue(href, fileUpload, db); } else { return null; } }, - enabled: !!(href && db), + enabled: !!href, }); +} - let value = data?.value; - if (value && types) { - if (!types.includes(value.type)) { - value = undefined; +function useStacValues(links: StacLink[] | undefined) { + const results = useQueries({ + queries: + links?.map((link) => { + return { + queryKey: ["link", link], + queryFn: () => fetchStacLink(link), + }; + }) || [], + }); + return { + values: results.map((value) => value.data).filter((value) => !!value), + }; +} + +function useStacCollections(value: StacValue | undefined) { + const href = value?.links?.find((link) => link.rel == "data")?.href; + const { data, isFetching, hasNextPage, fetchNextPage } = + useInfiniteQuery({ + queryKey: ["collections", href], + enabled: !!href, + queryFn: async ({ pageParam }) => { + if (pageParam) { + // @ts-expect-error Not worth templating stuff + return await fetchStac(pageParam); + } else { + return null; + } + }, + initialPageParam: href, + getNextPageParam: (lastPage: StacCollections | null) => + lastPage?.links?.find((link) => link.rel == "next")?.href, + }); + + useEffect(() => { + if (!isFetching && hasNextPage) { + fetchNextPage(); } - } + }, [isFetching, hasNextPage, fetchNextPage]); - return { value, parquetPath: data?.parquetPath }; + return { + collections: data?.pages.flatMap((page) => page?.collections || []), + }; } async function getStacValue( href: string, fileUpload: UseFileUploadReturn | undefined, - db: AsyncDuckDB, + db: AsyncDuckDB | undefined, ) { if (isUrl(href)) { // TODO allow this to be forced @@ -56,11 +157,15 @@ async function getStacValue( } else if (fileUpload?.acceptedFiles.length == 1) { const file = fileUpload.acceptedFiles[0]; if (await isParquetFile(file)) { - db.registerFileBuffer(href, new Uint8Array(await file.arrayBuffer())); - return { - value: getStacGeoparquetItemCollection(href), - parquetPath: href, - }; + if (db) { + db.registerFileBuffer(href, new Uint8Array(await file.arrayBuffer())); + return { + value: getStacGeoparquetItemCollection(href), + parquetPath: href, + }; + } else { + return null; + } } else { return { value: JSON.parse(await file.text()), diff --git a/src/provider.tsx b/src/provider.tsx index 7af4d8c..8866d28 100644 --- a/src/provider.tsx +++ b/src/provider.tsx @@ -1,44 +1,37 @@ import { useFileUpload } from "@chakra-ui/react"; -import { useEffect, useMemo, useState, type ReactNode } from "react"; -import type { StacCatalog, StacCollection, StacItem } from "stac-ts"; +import { useEffect, useState, type ReactNode } from "react"; +import type { StacItem } from "stac-ts"; import { StacMapContext } from "./context"; -import useStacChildrenAndItems from "./hooks/stac-children-and-items"; import useStacGeoparquet from "./hooks/stac-geoparquet"; import useStacValue from "./hooks/stac-value"; +import type { TemporalFilter } from "./types/datetime"; +import type { StacSearch } from "./types/stac"; export function StacMapProvider({ children }: { children: ReactNode }) { + // User-defined state const [href, setHref] = useState(getInitialHref()); const fileUpload = useFileUpload({ maxFiles: 1 }); - const { value, parquetPath } = useStacValue(href, fileUpload); - const { value: root } = useStacValue( - value && value.links?.find((l) => l.rel === "root")?.href, - undefined, - ["Catalog", "Collection"], - ); - const { value: parent } = useStacValue( - value && value.links?.find((l) => l.rel === "parent")?.href, - undefined, - ["Catalog", "Collection"], - ); + const [temporalFilter, setTemporalFilter] = useState(); + const [search, setSearch] = useState(); + const [searchItems, setSearchItems] = useState(); + const [picked, setPicked] = useState(); + + // Derived state const { + value, + parquetPath, + root, + parent, catalogs, collections, - isFetchingCollections, items: linkedItems, - } = useStacChildrenAndItems(value, href); - const [searchItems, setSearchItems] = useState(); - const [temporalFilter, setTemporalFilter] = useState<{ - start: Date; - end: Date; - }>(); + } = useStacValue({ href, fileUpload }); const { table: stacGeoparquetTable, metadata: stacGeoparquetMetadata, setId: setStacGeoparquetItemId, item: stacGeoparquetItem, } = useStacGeoparquet(parquetPath, temporalFilter); - const [picked, setPicked] = useState(); - const items = searchItems || linkedItems; useEffect(() => { function handlePopState() { @@ -52,10 +45,8 @@ export function StacMapProvider({ children }: { children: ReactNode }) { }, []); useEffect(() => { - if (href) { - if (new URLSearchParams(location.search).get("href") != href) { - history.pushState(null, "", "?href=" + href); - } + if (href && new URLSearchParams(location.search).get("href") != href) { + history.pushState(null, "", "?href=" + href); } }, [href]); @@ -66,98 +57,40 @@ export function StacMapProvider({ children }: { children: ReactNode }) { } }, [fileUpload.acceptedFiles]); - useEffect(() => { - // controls when to clear search items - const shouldClearSearch = - value?.type === "Catalog" || - (value?.type === "Collection" && - searchItems && - searchItems.length > 0 && - searchItems[0].collection !== value.id); - - if (shouldClearSearch) { - setSearchItems(undefined); - } - setPicked(undefined); - setStacGeoparquetItemId(undefined); - setTemporalFilter(undefined); - }, [value, setStacGeoparquetItemId, searchItems]); - useEffect(() => { setPicked(stacGeoparquetItem); }, [stacGeoparquetItem]); - const temporalExtents = useMemo(() => { - if (items) { - let start: Date | null = null; - let end: Date | null = null; - items.forEach((item) => { - const { start: itemStart, end: itemEnd } = getStartAndEndDatetime(item); - if (!start || (itemStart && itemStart < start)) { - start = itemStart; - } - if (!end || (itemEnd && itemEnd > end)) { - end = itemEnd; - } - }); - // @ts-expect-error Don't know why start and end are never. - if (start && end && start.getTime() != end.getTime()) { - return { start, end }; - } - } else if ( - stacGeoparquetMetadata?.startDatetime && - stacGeoparquetMetadata?.endDatetime - ) { - return { - start: stacGeoparquetMetadata.startDatetime, - end: stacGeoparquetMetadata.endDatetime, - }; - } - }, [ - items, - stacGeoparquetMetadata?.startDatetime, - stacGeoparquetMetadata?.endDatetime, - ]); + useEffect(() => { + setSearch(undefined); + }, [href]); - const filteredItems = useMemo(() => { - return ( - items?.filter((item) => { - if (temporalFilter) { - const { start, end } = getStartAndEndDatetime(item); - return ( - (!start || start >= temporalFilter.start) && - (!end || end <= temporalFilter.end) - ); - } else { - return true; - } - }) || [] - ); - }, [items, temporalFilter]); + useEffect(() => { + setSearchItems(undefined); + }, [search]); return ( {children} @@ -174,11 +107,3 @@ function getInitialHref() { } return href; } - -function getStartAndEndDatetime(item: StacItem) { - const startStr = item.properties.start_datetime || item.properties.datetime; - const start = startStr ? new Date(startStr) : null; - const endStr = item.properties.end_datetime || item.properties.datetime; - const end = endStr ? new Date(endStr) : null; - return { start, end }; -} diff --git a/src/types/datetime.d.ts b/src/types/datetime.d.ts new file mode 100644 index 0000000..8fb85a9 --- /dev/null +++ b/src/types/datetime.d.ts @@ -0,0 +1,4 @@ +export interface TemporalFilter { + start: Date; + end: Date; +} diff --git a/src/types/stac.d.ts b/src/types/stac.d.ts index 4d09b0a..74cf7c7 100644 --- a/src/types/stac.d.ts +++ b/src/types/stac.d.ts @@ -7,6 +7,8 @@ export type StacValue = | StacItem | StacItemCollection; +export type StacContainer = StacCatalog | StacCollection; + export interface StacItemCollection { type: "FeatureCollection"; features: StacItem[];