Skip to content

feat: viewport bounds search #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: date-filter-new
Choose a base branch
from
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
29 changes: 29 additions & 0 deletions src/components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,23 @@ export default function Map() {
setStacGeoparquetItemId,
setPicked,
picked,
setViewportBounds,
} = useStacMap();

useEffect(() => {
if (mapRef.current) {
const bounds = mapRef.current.getBounds();
if (bounds) {
setViewportBounds([
bounds.getWest(),
bounds.getSouth(),
bounds.getEast(),
bounds.getNorth(),
]);
}
}
}, []);

const filteredSearchItems = useFilteredSearchItems();
const filteredCollections = useFilteredCollections();

Expand Down Expand Up @@ -218,6 +233,19 @@ export default function Map() {
layers.push(stacGeoparquetLayer);
}

const handleMapMove = () => {
if (mapRef.current) {
const bounds = mapRef.current.getBounds();
const viewportBbox: BBox = [
bounds.getWest(),
bounds.getSouth(),
bounds.getEast(),
bounds.getNorth(),
];
setViewportBounds(viewportBbox);
}
};

return (
<MaplibreMap
id="map"
Expand All @@ -232,6 +260,7 @@ export default function Map() {
width: "100dvw",
}}
mapStyle={`https://basemaps.cartocdn.com/gl/${mapStyle}/style.json`}
onMoveEnd={handleMapMove}
>
<DeckGLOverlay
layers={layers}
Expand Down
75 changes: 69 additions & 6 deletions src/components/search/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ import {
Progress,
Select,
Stack,
Switch,
Text,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { LuDownload, LuPause, LuPlay, LuSearch, LuX } from "react-icons/lu";
import {
LuDownload,
LuPause,
LuPlay,
LuSearch,
LuX,
LuMapPin,
} from "react-icons/lu";
import type { StacCollection, StacLink } from "stac-ts";
import useStacMap from "../../hooks/stac-map";
import useStacSearch from "../../hooks/stac-search";
Expand Down Expand Up @@ -42,6 +50,8 @@ export default function ItemSearch({
const [link, setLink] = useState(defaultLink);
const [collections, setCollections] = useState<StacCollection[]>([]);
const [method, setMethod] = useState((defaultLink.method as string) || "GET");
const [useViewportBounds, setUseViewportBounds] = useState(false);
const { viewportBounds, isViewportBoundsActive } = useStacMap();

const methods =
links.length > 1 &&
Expand All @@ -59,6 +69,18 @@ export default function ItemSearch({
setLink(links.find((link) => link.method == method) || defaultLink);
}, [method, defaultLink, links]);

const handleSearch = () => {
const searchParams: StacSearch = {
collections: collections.map((collection) => collection.id),
};

if (useViewportBounds && viewportBounds) {
searchParams.bbox = viewportBounds;
}

setSearch(searchParams);
};

return (
<Stack gap={4}>
<Stack>
Expand All @@ -77,6 +99,40 @@ export default function ItemSearch({
title="Search Date Filter"
description="Filter items at the server level when searching"
/>

{/* Viewport Bounds Toggle */}
<Stack gap={2}>
<HStack justify="space-between">
<HStack gap={2}>
<LuMapPin size={16} />
<Text fontSize="sm" fontWeight="medium">
Use viewport bounds
</Text>
</HStack>
<Switch.Root
size="sm"
checked={useViewportBounds}
onCheckedChange={(details) => setUseViewportBounds(details.checked)}
disabled={!isViewportBoundsActive}
>
<Switch.HiddenInput />
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
</Switch.Root>
</HStack>
{useViewportBounds && !isViewportBoundsActive && (
<Text fontSize="xs" color="orange.500">
Move the map to enable viewport-bounded search
</Text>
)}
{useViewportBounds && isViewportBoundsActive && (
<Text fontSize="xs" color="green.500">
Search will be bounded to current map viewport
</Text>
)}
</Stack>

<Alert.Root status={"warning"}>
<Alert.Indicator></Alert.Indicator>
<Alert.Content>
Expand Down Expand Up @@ -121,11 +177,8 @@ export default function ItemSearch({
{!search && (
<Button
variant={"surface"}
onClick={() =>
setSearch({
collections: collections.map((collection) => collection.id),
})
}
onClick={handleSearch}
disabled={useViewportBounds && !isViewportBoundsActive}
>
<LuSearch></LuSearch> Search
</Button>
Expand Down Expand Up @@ -216,6 +269,16 @@ function SearchResults({
</Alert.Content>
</Alert.Root>
)}
{search.bbox && (
<Alert.Root status="info">
<Alert.Indicator />
<Alert.Content>
<Alert.Description>
Showing results bounded to current map viewport
</Alert.Description>
</Alert.Content>
</Alert.Root>
)}
<Progress.Root value={value} max={numberMatched}>
<Progress.Label>
{(value && `Found ${value} item${value === 1 ? "" : "s"}`) ||
Expand Down
5 changes: 5 additions & 0 deletions src/context/stac-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createContext, type Dispatch, type SetStateAction } from "react";
import type { StacCollection, StacItem } from "stac-ts";
import type { StacGeoparquetMetadata, StacValue } from "../types/stac";
import type { DateRange } from "../components/date-filter";
import type { BBox } from "geojson";

export const StacMapContext = createContext<StacMapContextType | null>(null);

Expand Down Expand Up @@ -37,4 +38,8 @@ interface StacMapContextType {
isClientFilterActive: boolean;

hasTemporalData: boolean;

viewportBounds: BBox | undefined;
setViewportBounds: (bounds: BBox | undefined) => void;
isViewportBoundsActive: boolean;
}
24 changes: 18 additions & 6 deletions src/hooks/stac-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ import { formatDateRangeForStacSearch } from "../utils/date-filter";
import useStacMap from "./stac-map";

export default function useStacSearch(search: StacSearch, link: StacLink) {
const { dateRange } = useStacMap();
const { dateRange, viewportBounds } = useStacMap();

const searchWithDateRange = useMemo(() => {
const searchWithFilters = useMemo(() => {
let searchWithDateRange = search;
const datetime = formatDateRangeForStacSearch(dateRange);
return datetime ? { ...search, datetime } : search;
}, [search, dateRange]);
if (datetime) {
searchWithDateRange = { ...search, datetime };
}

if (viewportBounds && !search.bbox) {
searchWithDateRange = { ...searchWithDateRange, bbox: viewportBounds };
}

return searchWithDateRange;
}, [search, dateRange, viewportBounds]);

return useInfiniteQuery({
queryKey: ["search", searchWithDateRange, link, dateRange],
initialPageParam: updateLink(link, searchWithDateRange),
queryKey: ["search", searchWithFilters, link, dateRange, viewportBounds],
initialPageParam: updateLink(link, searchWithFilters),
getNextPageParam: (lastPage: StacItemCollection) =>
lastPage.links?.find((link) => link.rel == "next"),
queryFn: fetchSearch,
Expand Down Expand Up @@ -53,6 +62,9 @@ function updateLink(link: StacLink, search: StacSearch) {
if (search.datetime) {
url.searchParams.set("datetime", search.datetime);
}
if (search.bbox) {
url.searchParams.set("bbox", search.bbox.join(","));
}
} else {
link.body = search;
}
Expand Down
10 changes: 10 additions & 0 deletions src/provider/stac-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type ReactNode,
} from "react";
import type { StacItem } from "stac-ts";
import type { BBox } from "geojson";
import { StacMapContext } from "../context/stac-map";
import { useStacCollections } from "../hooks/stac-collections";
import useStacGeoparquet from "../hooks/stac-geoparquet";
Expand Down Expand Up @@ -80,6 +81,7 @@ export function StacMapProvider({ children }: { children: ReactNode }) {

const [picked, setPicked] = useState<StacValue>();
const [searchItems, setSearchItems] = useState<StacItem[][]>([]);
const [viewportBounds, setViewportBounds] = useState<BBox>();

const clearDateRange = useCallback(() => {
setDateRange({
Expand Down Expand Up @@ -117,6 +119,10 @@ export function StacMapProvider({ children }: { children: ReactNode }) {
);
}, [clientFilterDateRange]);

const isViewportBoundsActive = useMemo(() => {
return viewportBounds !== undefined;
}, [viewportBounds]);

const updateClientFilterUrl = useCallback((dateRange: DateRange) => {
const params = new URLSearchParams(location.search);
const clientFilterParam = serializeClientFilterDateRange(dateRange);
Expand Down Expand Up @@ -234,6 +240,10 @@ export function StacMapProvider({ children }: { children: ReactNode }) {
clearClientFilterDateRange,
isClientFilterActive,
hasTemporalData,

viewportBounds,
setViewportBounds,
isViewportBoundsActive,
};

return (
Expand Down