Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
tests/__screenshots__/
tests/**/__screenshots__/
codebook.toml
119 changes: 22 additions & 97 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, 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";
import useStacValue from "./hooks/stac-value";
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 {
isCog,
isCollectionInBbox,
isCollectionInDatetimeBounds,
isItemInBbox,
isItemInDatetimeBounds,
isVisual,
} from "./utils/stac";
import { getCogTileHref } from "./utils/stac";

// TODO make this configurable by the user.
const lineColor: Color = [207, 63, 2, 100];
const fillColor: Color = [207, 63, 2, 50];

export default function App() {
// User state
const [href, setHref] = useState<string | undefined>(getInitialHref());
const fileUpload = useFileUpload({ maxFiles: 1 });
const { href, setHref } = useHrefParam();
const fileUpload = useFileUpload({
maxFiles: 1,
onFileChange: (details) => {
if (details.acceptedFiles.length === 1) {
setHref(details.acceptedFiles[0].name);
}
},
});
const [userCollections, setCollections] = useState<StacCollection[]>();
const [userItems, setItems] = useState<StacItem[]>();
const [picked, setPicked] = useState<StacValue>();
Expand Down Expand Up @@ -54,77 +57,22 @@ export default function App() {
});
const collections = collectionsLink ? userCollections : linkedCollections;
const items = userItems || linkedItems;
const filteredCollections = useMemo(() => {
if (filter && collections) {
return collections.filter(
(collection) =>
(!bbox || isCollectionInBbox(collection, bbox)) &&
(!datetimeBounds ||
isCollectionInDatetimeBounds(collection, datetimeBounds))
);
} else {
return undefined;
}
}, [collections, filter, bbox, datetimeBounds]);
const filteredItems = useMemo(() => {
if (filter && items) {
return items.filter(
(item) =>
(!bbox || isItemInBbox(item, bbox)) &&
(!datetimeBounds || isItemInDatetimeBounds(item, datetimeBounds))
);
} else {
return undefined;
}
}, [items, filter, bbox, datetimeBounds]);
const { filteredCollections, filteredItems } = useStacFilters({
collections,
items,
filter,
bbox,
datetimeBounds,
});

// Effects
useEffect(() => {
function handlePopState() {
setHref(new URLSearchParams(location.search).get("href") ?? "");
}
window.addEventListener("popstate", handlePopState);

const href = new URLSearchParams(location.search).get("href");
if (href) {
try {
new URL(href);
} catch {
history.pushState(null, "", location.pathname);
}
}

return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);

useEffect(() => {
if (href && new URLSearchParams(location.search).get("href") != href) {
history.pushState(null, "", "?href=" + href);
} else if (href === "") {
history.pushState(null, "", location.pathname);
}
}, [href]);

useEffect(() => {
// It should never be more than 1.
if (fileUpload.acceptedFiles.length == 1) {
setHref(fileUpload.acceptedFiles[0].name);
}
}, [fileUpload.acceptedFiles]);
useDocumentTitle(value);

useEffect(() => {
setPicked(undefined);
setItems(undefined);
setDatetimeBounds(undefined);
setCogTileHref(value && getCogTileHref(value));

if (value && (value.title || value.id)) {
document.title = "stac-map | " + (value.title || value.id);
} else {
document.title = "stac-map";
}
}, [value]);

useEffect(() => {
Expand Down Expand Up @@ -201,26 +149,3 @@ export default function App() {
</>
);
}

function getInitialHref() {
const href = new URLSearchParams(location.search).get("href") || "";
try {
new URL(href);
} catch {
return undefined;
}
return href;
}

function getCogTileHref(value: StacValue) {
let cogTileHref = undefined;
if (value.assets) {
for (const asset of Object.values(value.assets)) {
if (isCog(asset) && isVisual(asset)) {
cogTileHref = asset.href as string;
break;
}
}
}
return cogTileHref;
}
12 changes: 12 additions & 0 deletions src/hooks/document-title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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]);
}
47 changes: 47 additions & 0 deletions src/hooks/href-param.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useEffect, useState } from "react";

function getInitialHref(): string | undefined {
const href = new URLSearchParams(location.search).get("href") || "";
try {
new URL(href);
} catch {
return undefined;
}
return href;
}

export default function useHrefParam() {
const [href, setHref] = useState<string | undefined>(getInitialHref());

// Sync href with URL params
useEffect(() => {
if (href && new URLSearchParams(location.search).get("href") != href) {
history.pushState(null, "", "?href=" + href);
} else if (href === "") {
history.pushState(null, "", location.pathname);
}
}, [href]);

// Handle browser back/forward
useEffect(() => {
function handlePopState() {
setHref(new URLSearchParams(location.search).get("href") ?? "");
}
window.addEventListener("popstate", handlePopState);

const href = new URLSearchParams(location.search).get("href");
if (href) {
try {
new URL(href);
} catch {
history.pushState(null, "", location.pathname);
}
}

return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);

return { href, setHref };
}
53 changes: 53 additions & 0 deletions src/hooks/stac-filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useMemo } from "react";
import type { StacCollection, StacItem } from "stac-ts";
import type { BBox2D } from "../types/map";
import type { DatetimeBounds } from "../types/stac";
import {
isCollectionInBbox,
isCollectionInDatetimeBounds,
isItemInBbox,
isItemInDatetimeBounds,
} from "../utils/stac";

interface UseStacFiltersProps {
collections?: StacCollection[];
items?: StacItem[];
filter: boolean;
bbox?: BBox2D;
datetimeBounds?: DatetimeBounds;
}

export default function useStacFilters({
collections,
items,
filter,
bbox,
datetimeBounds,
}: UseStacFiltersProps) {
const filteredCollections = useMemo(() => {
if (filter && collections) {
return collections.filter(
(collection) =>
(!bbox || isCollectionInBbox(collection, bbox)) &&
(!datetimeBounds ||
isCollectionInDatetimeBounds(collection, datetimeBounds))
);
} else {
return undefined;
}
}, [collections, filter, bbox, datetimeBounds]);

const filteredItems = useMemo(() => {
if (filter && items) {
return items.filter(
(item) =>
(!bbox || isItemInBbox(item, bbox)) &&
(!datetimeBounds || isItemInDatetimeBounds(item, datetimeBounds))
);
} else {
return undefined;
}
}, [items, filter, bbox, datetimeBounds]);

return { filteredCollections, filteredItems };
}
14 changes: 14 additions & 0 deletions src/utils/stac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,17 @@ export function isVisual(asset: StacAsset) {
}
return false;
}

export function getCogTileHref(value: StacValue): string | undefined {
if (!value.assets) {
return undefined;
}

for (const asset of Object.values(value.assets)) {
if (isCog(asset) && isVisual(asset)) {
return asset.href as string;
}
}

return undefined;
}
Loading