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", () => {