diff --git a/src/app.tsx b/src/app.tsx index c44bbbb..e94f9b6 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,8 +1,7 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Box, Container, FileUpload, useFileUpload } from "@chakra-ui/react"; import type { StacCollection, StacItem } from "stac-ts"; import { Toaster } from "./components/ui/toaster"; -import useDocumentTitle from "./hooks/document-title"; import useHrefParam from "./hooks/href-param"; import useStacChildren from "./hooks/stac-children"; import useStacFilters from "./hooks/stac-filters"; @@ -11,7 +10,9 @@ import Map from "./layers/map"; import Overlay from "./layers/overlay"; import type { BBox2D, Color } from "./types/map"; import type { DatetimeBounds, StacValue } from "./types/stac"; +import getDateTimes from "./utils/datetimes"; import { getCogTileHref } from "./utils/stac"; +import getDocumentTitle from "./utils/title"; // TODO make this configurable by the user. const lineColor: Color = [207, 63, 2, 100]; @@ -65,8 +66,15 @@ export default function App() { datetimeBounds, }); + const datetimes = useMemo( + () => (value ? getDateTimes(value, items, collections) : null), + [value, items, collections] + ); + // Effects - useDocumentTitle(value); + useEffect(() => { + document.title = getDocumentTitle(value); + }, [value]); useEffect(() => { setPicked(undefined); @@ -99,8 +107,7 @@ export default function App() { table={table} collections={collections} filteredCollections={filteredCollections} - items={items} - filteredItems={filteredItems} + items={filteredItems} fillColor={fillColor} lineColor={lineColor} setBbox={setBbox} @@ -130,19 +137,19 @@ export default function App() { error={error} catalogs={catalogs} setCollections={setCollections} - collections={collections} - filteredCollections={filteredCollections} + collections={filteredCollections} + totalNumOfCollections={collections?.length} filter={filter} setFilter={setFilter} bbox={bbox} setPicked={setPicked} picked={picked} - items={items} - filteredItems={filteredItems} + items={filteredItems} setItems={setItems} setDatetimeBounds={setDatetimeBounds} cogTileHref={cogTileHref} setCogTileHref={setCogTileHref} + datetimes={datetimes} > diff --git a/src/components/panel.tsx b/src/components/panel.tsx index 58b64a3..83441a6 100644 --- a/src/components/panel.tsx +++ b/src/components/panel.tsx @@ -21,7 +21,13 @@ export default function Panel({ error, fileUpload, ...props -}: PanelProps) { +}: { + totalNumOfCollections: number | undefined; + datetimes: { + start: Date; + end: Date; + } | null; +} & PanelProps) { if (error) return ( diff --git a/src/components/sections/collections.tsx b/src/components/sections/collections.tsx index 7192d80..20224e1 100644 --- a/src/components/sections/collections.tsx +++ b/src/components/sections/collections.tsx @@ -11,23 +11,21 @@ interface CollectionsProps { export default function CollectionsSection({ collections, - filteredCollections, - numberOfCollections, + collectionsNumberMatched, + totalNumOfCollections, setHref, }: { - filteredCollections: StacCollection[] | undefined; - numberOfCollections: number | undefined; + collectionsNumberMatched: number | undefined; + totalNumOfCollections: number | undefined; } & CollectionsProps) { - const parenthetical = filteredCollections - ? `${filteredCollections.length}/${numberOfCollections || collections.length}` - : collections.length; + const parenthetical = + collections.length !== collectionsNumberMatched + ? `${collections.length}/${collectionsNumberMatched || totalNumOfCollections}` + : collections.length; const title = `Collections (${parenthetical})`; return (
- +
); } diff --git a/src/components/sections/filter.tsx b/src/components/sections/filter.tsx index d22dee7..00e2aa8 100644 --- a/src/components/sections/filter.tsx +++ b/src/components/sections/filter.tsx @@ -4,7 +4,6 @@ import { Checkbox, DataList, Slider, Stack, Text } from "@chakra-ui/react"; import type { StacCollection, StacItem } from "stac-ts"; import type { BBox2D } from "../../types/map"; import type { DatetimeBounds, StacValue } from "../../types/stac"; -import { getItemDatetimes } from "../../utils/stac"; import { SpatialExtent } from "../extent"; import Section from "../section"; @@ -16,6 +15,10 @@ interface FilterProps { value: StacValue; items: StacItem[] | undefined; collections: StacCollection[] | undefined; + datetimes: { + start: Date; + end: Date; + } | null; } export default function FilterSection({ filter, ...props }: FilterProps) { @@ -35,50 +38,11 @@ function Filter({ setFilter, bbox, setDatetimeBounds, - value, - items, - collections, + datetimes, }: FilterProps) { const [filterStart, setFilterStart] = useState(); const [filterEnd, setFilterEnd] = useState(); - const datetimes = useMemo(() => { - let start = - value.start_datetime && typeof value.start_datetime === "string" - ? new Date(value.start_datetime as string) - : null; - let end = - value.end_datetime && typeof value.end_datetime === "string" - ? new Date(value.end_datetime as string) - : null; - - if (items) { - for (const item of items) { - const itemDatetimes = getItemDatetimes(item); - if (itemDatetimes.start && (!start || itemDatetimes.start < start)) - start = itemDatetimes.start; - if (itemDatetimes.end && (!end || itemDatetimes.end > end)) - end = itemDatetimes.end; - } - } - - if (collections) { - for (const collection of collections) { - const extents = collection.extent?.temporal?.interval?.[0]; - if (extents) { - const collectionStart = extents[0] ? new Date(extents[0]) : null; - if (collectionStart && (!start || collectionStart < start)) - start = collectionStart; - const collectionEnd = extents[1] ? new Date(extents[1]) : null; - if (collectionEnd && (!end || collectionEnd > end)) - end = collectionEnd; - } - } - } - - return start && end ? { start, end } : null; - }, [value, items, collections]); - const sliderValue = useMemo(() => { if (!datetimes) return undefined; if (filterStart && filterEnd) { diff --git a/src/components/sections/items.tsx b/src/components/sections/items.tsx index fb02ef2..0c558ac 100644 --- a/src/components/sections/items.tsx +++ b/src/components/sections/items.tsx @@ -9,17 +9,18 @@ interface ItemsProps { } export default function ItemsSection({ - filteredItems, + totalNumOfItems, items, ...props -}: { filteredItems: StacItem[] | undefined } & ItemsProps) { - const parenthetical = filteredItems - ? `${filteredItems.length}/${items.length}` - : items.length; +}: { totalNumOfItems: number | undefined } & ItemsProps) { + const parenthetical = + items.length !== totalNumOfItems + ? `${items.length}/${totalNumOfItems}` + : items.length; const title = `Items (${parenthetical})`; return (
- +
); } diff --git a/src/components/value.tsx b/src/components/value.tsx index b592e92..22bf7ad 100644 --- a/src/components/value.tsx +++ b/src/components/value.tsx @@ -46,9 +46,7 @@ export interface SharedValueProps { catalogs: StacCatalog[] | undefined; setCollections: (collections: StacCollection[] | undefined) => void; collections: StacCollection[] | undefined; - filteredCollections: StacCollection[] | undefined; items: StacItem[] | undefined; - filteredItems: StacItem[] | undefined; setHref: (href: string | undefined) => void; filter: boolean; setFilter: (filter: boolean) => void; @@ -70,10 +68,8 @@ export function Value({ value, catalogs, collections, - filteredCollections, setCollections, items, - filteredItems, setItems, filter, setFilter, @@ -81,7 +77,15 @@ export function Value({ setDatetimeBounds, cogTileHref, setCogTileHref, -}: ValueProps) { + totalNumOfCollections, + datetimes, +}: { + totalNumOfCollections: number | undefined; + datetimes: { + start: Date; + end: Date; + } | null; +} & ValueProps) { const [search, setSearch] = useState(); const [fetchAllCollections, setFetchAllCollections] = useState(false); const [thumbnailError, setThumbnailError] = useState(false); @@ -131,10 +135,12 @@ export function Value({ ); }, [assets]); - const numberOfCollections = useMemo(() => { + const collectionsNumberMatched = useMemo(() => { return collectionsResult.data?.pages.at(0)?.numberMatched; }, [collectionsResult.data]); + const totalNumOfItems = items?.length; + useEffect(() => { setCollections( collectionsResult.data?.pages.flatMap((page) => page?.collections || []) @@ -274,8 +280,8 @@ export function Value({ {collections && collections.length && ( )} @@ -310,13 +316,14 @@ export function Value({ value={value} items={items} collections={collections} + datetimes={datetimes} /> )} {items && ( )} diff --git a/src/hooks/document-title.ts b/src/hooks/document-title.ts deleted file mode 100644 index 8bb999d..0000000 --- a/src/hooks/document-title.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useEffect } from "react"; -import type { StacValue } from "../types/stac"; - -export default function useDocumentTitle(value: StacValue | undefined) { - useEffect(() => { - if (value && (value.title || value.id)) { - document.title = "stac-map | " + (value.title || value.id); - } else { - document.title = "stac-map"; - } - }, [value]); -} diff --git a/src/hooks/href-param.ts b/src/hooks/href-param.ts index c7fa69c..686f28a 100644 --- a/src/hooks/href-param.ts +++ b/src/hooks/href-param.ts @@ -1,7 +1,11 @@ import { useEffect, useState } from "react"; +function getCurrentHref(): string { + return new URLSearchParams(location.search).get("href") || ""; +} + function getInitialHref(): string | undefined { - const href = new URLSearchParams(location.search).get("href") || ""; + const href = getCurrentHref(); try { new URL(href); } catch { @@ -15,7 +19,7 @@ export default function useHrefParam() { // Sync href with URL params useEffect(() => { - if (href && new URLSearchParams(location.search).get("href") != href) { + if (href && getCurrentHref() != href) { history.pushState(null, "", "?href=" + href); } else if (href === "") { history.pushState(null, "", location.pathname); @@ -25,14 +29,13 @@ export default function useHrefParam() { // Handle browser back/forward useEffect(() => { function handlePopState() { - setHref(new URLSearchParams(location.search).get("href") ?? ""); + setHref(getCurrentHref() ?? ""); } window.addEventListener("popstate", handlePopState); - const href = new URLSearchParams(location.search).get("href"); - if (href) { + if (getCurrentHref()) { try { - new URL(href); + new URL(getCurrentHref()); } catch { history.pushState(null, "", location.pathname); } diff --git a/src/hooks/stac-filters.ts b/src/hooks/stac-filters.ts index 3bfdba2..6d22e02 100644 --- a/src/hooks/stac-filters.ts +++ b/src/hooks/stac-filters.ts @@ -23,30 +23,33 @@ export default function useStacFilters({ filter, bbox, datetimeBounds, -}: UseStacFiltersProps) { +}: UseStacFiltersProps): { + filteredCollections: StacCollection[] | undefined; + filteredItems: StacItem[] | undefined; +} { const filteredCollections = useMemo(() => { if (filter && collections) { - return collections.filter( + const filtered = collections.filter( (collection) => (!bbox || isCollectionInBbox(collection, bbox)) && (!datetimeBounds || isCollectionInDatetimeBounds(collection, datetimeBounds)) ); - } else { - return undefined; + return filtered; } + return collections; }, [collections, filter, bbox, datetimeBounds]); const filteredItems = useMemo(() => { if (filter && items) { - return items.filter( + const filtered = items.filter( (item) => (!bbox || isItemInBbox(item, bbox)) && (!datetimeBounds || isItemInDatetimeBounds(item, datetimeBounds)) ); - } else { - return undefined; + return filtered; } + return items; }, [items, filter, bbox, datetimeBounds]); return { filteredCollections, filteredItems }; diff --git a/src/layers/map.tsx b/src/layers/map.tsx index 38dcb83..049142c 100644 --- a/src/layers/map.tsx +++ b/src/layers/map.tsx @@ -24,7 +24,6 @@ export default function Map({ collections, filteredCollections, items, - filteredItems, fillColor, lineColor, setBbox, @@ -38,7 +37,6 @@ export default function Map({ collections: StacCollection[] | undefined; filteredCollections: StacCollection[] | undefined; items: StacItem[] | undefined; - filteredItems: StacItem[] | undefined; fillColor: Color; lineColor: Color; setBbox: (bbox: BBox2D | undefined) => void; @@ -135,7 +133,7 @@ export default function Map({ }), new GeoJsonLayer({ id: "items", - data: (filteredItems || items) as Feature[] | undefined, + data: items as Feature[] | undefined, filled: true, getFillColor: fillColor, getLineColor: lineColor, diff --git a/src/layers/overlay.tsx b/src/layers/overlay.tsx index bbf46e0..ea8929c 100644 --- a/src/layers/overlay.tsx +++ b/src/layers/overlay.tsx @@ -29,9 +29,14 @@ export default function Overlay({ picked, setPicked, items, - filteredItems, ...props -}: OverlayProps) { +}: { + totalNumOfCollections: number | undefined; + datetimes: { + start: Date; + end: Date; + } | null; +} & OverlayProps) { return ( @@ -63,7 +68,6 @@ export default function Overlay({ value={picked || value} fileUpload={fileUpload} items={picked ? undefined : items} - filteredItems={picked ? undefined : filteredItems} {...props} /> diff --git a/src/utils/datetimes.ts b/src/utils/datetimes.ts new file mode 100644 index 0000000..91a4893 --- /dev/null +++ b/src/utils/datetimes.ts @@ -0,0 +1,44 @@ +import type { StacCollection, StacItem } from "stac-ts"; +import type { StacValue } from "../types/stac"; +import { getItemDatetimes } from "../utils/stac"; + +const getDateTimes = ( + value: StacValue, + items: StacItem[] | undefined, + collections: StacCollection[] | undefined +) => { + let start = + value.start_datetime && typeof value.start_datetime === "string" + ? new Date(value.start_datetime as string) + : null; + let end = + value.end_datetime && typeof value.end_datetime === "string" + ? new Date(value.end_datetime as string) + : null; + + if (items) { + for (const item of items) { + const itemDatetimes = getItemDatetimes(item); + if (itemDatetimes.start && (!start || itemDatetimes.start < start)) + start = itemDatetimes.start; + if (itemDatetimes.end && (!end || itemDatetimes.end > end)) + end = itemDatetimes.end; + } + } + + if (collections) { + for (const collection of collections) { + const extents = collection.extent?.temporal?.interval?.[0]; + if (extents) { + const collectionStart = extents[0] ? new Date(extents[0]) : null; + if (collectionStart && (!start || collectionStart < start)) + start = collectionStart; + const collectionEnd = extents[1] ? new Date(extents[1]) : null; + if (collectionEnd && (!end || collectionEnd > end)) end = collectionEnd; + } + } + } + return start && end ? { start, end } : null; +}; + +export default getDateTimes; diff --git a/src/utils/title.ts b/src/utils/title.ts new file mode 100644 index 0000000..0c49f8b --- /dev/null +++ b/src/utils/title.ts @@ -0,0 +1,9 @@ +import type { StacValue } from "../types/stac"; + +export default function getDocumentTitle(value: StacValue | undefined) { + let title = "stac-map"; + if (value && (value.title || value.id)) { + title = "stac-map | " + (value.title || value.id); + } + return title; +} diff --git a/tests/hooks/document-title.spec.ts b/tests/hooks/document-title.spec.ts index fda04f8..4a3316d 100644 --- a/tests/hooks/document-title.spec.ts +++ b/tests/hooks/document-title.spec.ts @@ -1,6 +1,7 @@ import type { StacCatalog, StacCollection, StacItem } from "stac-ts"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import type { StacItemCollection, StacValue } from "../../src/types/stac"; +import getDocumentTitle from "../../src/utils/title"; describe("useDocumentTitle logic", () => { let originalTitle: string; @@ -18,11 +19,7 @@ describe("useDocumentTitle logic", () => { }); function setDocumentTitle(value: StacValue | undefined) { - if (value && (value.title || value.id)) { - document.title = "stac-map | " + (value.title || value.id); - } else { - document.title = "stac-map"; - } + document.title = getDocumentTitle(value); } test("should set default title when value is undefined", () => {