Skip to content

Date filter #58

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

Closed
wants to merge 10 commits into from
Closed
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
15 changes: 8 additions & 7 deletions src/components/stac/collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,18 @@ export function Collections({
const [bounds, setBounds] = useState<LngLatBounds>();
const { map } = useMap();


useEffect(() => {
let filtered = collections;

if (filterToMapBounds && bounds) {
setFilteredCollections(
collections.filter((collection) =>
isCollectionWithinBounds(collection, bounds),
),
filtered = filtered.filter((collection) =>
isCollectionWithinBounds(collection, bounds),
);
} else {
setFilteredCollections(collections);
}
}, [filterToMapBounds, map, setFilteredCollections, collections, bounds]);

setFilteredCollections(filtered);
}, [filterToMapBounds, map, collections, bounds]);

useEffect(() => {
if (map) {
Expand Down
168 changes: 168 additions & 0 deletions src/components/stac/date-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
Button,
Card,
HStack,
Input,
Stack,
Text,
VStack,
} from "@chakra-ui/react";
import { LuCalendar, LuX } from "react-icons/lu";
import { useStacMap } from "../../hooks";

export default function DateFilter() {
const { dateRange, setDateRange } = useStacMap();

const handleStartDateChange = (value: string) => {
const currentStartTime = dateRange?.startTime || "00:00";
setDateRange({
startDate: value || null,
startTime: currentStartTime,
endDate: dateRange?.endDate || null,
endTime: dateRange?.endTime || "23:59",
});
};

const handleStartTimeChange = (value: string) => {
setDateRange({
startDate: dateRange?.startDate || null,
startTime: value || "00:00",
endDate: dateRange?.endDate || null,
endTime: dateRange?.endTime || "23:59",
});
};

const handleEndDateChange = (value: string) => {
const currentEndTime = dateRange?.endTime || "23:59";
setDateRange({
startDate: dateRange?.startDate || null,
startTime: dateRange?.startTime || "00:00",
endDate: value || null,
endTime: currentEndTime,
});
};

const handleEndTimeChange = (value: string) => {
setDateRange({
startDate: dateRange?.startDate || null,
startTime: dateRange?.startTime || "00:00",
endDate: dateRange?.endDate || null,
endTime: value || "23:59",
});
};

const handleClearFilter = () => {
setDateRange(null);
};

const isFilterActive = dateRange && (dateRange.startDate || dateRange.endDate);

return (
<Card.Root variant="outline">
<Card.Header pb={2}>
<HStack justify="space-between" align="center">
<HStack gap={2}>
<LuCalendar size={16} />
<Text fontWeight="medium">Date & Time Range Filter</Text>
</HStack>
{isFilterActive && (
<Button
size="xs"
variant="ghost"
onClick={handleClearFilter}
>
<LuX size={12} /> Clear
</Button>
)}
</HStack>
</Card.Header>
<Card.Body pt={0}>
<VStack gap={3} align="stretch">
<Stack gap={2}>
<Text as="label" fontSize="sm" fontWeight="medium">
Start Date & Time
</Text>
<HStack gap={2}>
<Input
id="start-date"
type="date"
value={dateRange?.startDate || ""}
onChange={(e) => handleStartDateChange(e.target.value)}
placeholder="Select start date"
size="sm"
flex={2}
/>
<Input
id="start-time"
type="time"
value={dateRange?.startTime || "00:00"}
onChange={(e) => handleStartTimeChange(e.target.value)}
size="sm"
flex={1}
/>
</HStack>
</Stack>

<Stack gap={2}>
<Text as="label" fontSize="sm" fontWeight="medium">
End Date & Time
</Text>
<HStack gap={2}>
<Input
id="end-date"
type="date"
value={dateRange?.endDate || ""}
onChange={(e) => handleEndDateChange(e.target.value)}
placeholder="Select end date"
size="sm"
flex={2}
min={dateRange?.startDate || undefined}
/>
<Input
id="end-time"
type="time"
value={dateRange?.endTime || "23:59"}
onChange={(e) => handleEndTimeChange(e.target.value)}
size="sm"
flex={1}
/>
</HStack>
</Stack>

{isFilterActive && (
<Card.Root variant="elevated" bg="blue.50" borderColor="blue.200">
<Card.Body py={2}>
<Text fontSize="sm" color="blue.700">
<strong>Active Filter:</strong>{" "}
{dateRange?.startDate && dateRange?.endDate
? `${formatDateTime(dateRange.startDate, dateRange.startTime || "00:00")} to ${formatDateTime(dateRange.endDate, dateRange.endTime || "23:59")}`
: dateRange?.startDate
? `From ${formatDateTime(dateRange.startDate, dateRange.startTime || "00:00")}`
: `Until ${formatDateTime(dateRange.endDate!, dateRange.endTime || "23:59")}`}
</Text>
</Card.Body>
</Card.Root>
)}

{dateRange?.startDate && dateRange?.endDate && (
<Text fontSize="xs" color="fg.muted">
Showing items between {formatDateTime(dateRange.startDate, dateRange.startTime || "00:00")} and{" "}
{formatDateTime(dateRange.endDate, dateRange.endTime || "23:59")}
</Text>
)}
</VStack>
</Card.Body>
</Card.Root>
);
}

function formatDateTime(dateString: string, timeString: string): string {
const date = new Date(`${dateString}T${timeString}`);
return date.toLocaleString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
3 changes: 3 additions & 0 deletions src/components/stac/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ export function useItemSearch(searchRequest: StacSearchRequest | undefined) {
pageParam.search.collections.join(","),
);
}
if (pageParam.search.datetime) {
url.searchParams.set("datetime", pageParam.search.datetime);
}
} else {
if (pageParam.link.body) {
init.body = JSON.stringify(pageParam.link.body);
Expand Down
53 changes: 49 additions & 4 deletions src/components/stac/item-collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ import {
Portal,
Stack,
Stat,
Text,
} from "@chakra-ui/react";
import { useMemo } from "react";
import { LuDownload, LuEyeOff, LuFileJson, LuFocus } from "react-icons/lu";
import type { StacItem } from "stac-ts";
import { useFitBbox, useStacMap } from "../../hooks";
import Loading from "../loading";
import Item from "./item";
import { type StacGeoparquetMetadata } from "./stac-geoparquet";
import type { StacItemCollection } from "./types";
import { getItemCollectionExtent } from "./utils";
import { getItemCollectionExtent, isItemWithinDateRange } from "./utils";
import Value from "./value";

export default function ItemCollection({
Expand All @@ -32,12 +34,41 @@ export default function ItemCollection({
stacGeoparquetMetadata,
stacGeoparquetMetadataIsPending,
stacGeoparquetItem,
dateRange,
} = useStacMap();
const fitBbox = useFitBbox();

const filteredFeatures = useMemo(() => {
if (!dateRange || (!dateRange.startDate && !dateRange.endDate)) {
return itemCollection.features;
}

return itemCollection.features.filter((feature) =>
isItemWithinDateRange(feature, dateRange)
);
}, [itemCollection.features, dateRange]);

const filteredItemCollection = useMemo(() => ({
...itemCollection,
features: filteredFeatures,
}), [itemCollection, filteredFeatures]);

const isFilterActive = dateRange && (dateRange.startDate || dateRange.endDate);

return (
<Stack>
<Value value={itemCollection} type="Item collection"></Value>
<Value value={filteredItemCollection} type="Item collection"></Value>

{isFilterActive && (
<Card.Root variant="elevated" bg="blue.50" borderColor="blue.200">
<Card.Body py={2}>
<Text fontSize="sm" color="blue.700">
<strong>Date Filter Active:</strong> Showing {filteredFeatures.length} of {itemCollection.features.length} items
</Text>
</Card.Body>
</Card.Root>
)}

<HStack>
<DownloadTrigger
asChild
Expand All @@ -49,11 +80,11 @@ export default function ItemCollection({
<LuDownload></LuDownload>
</IconButton>
</DownloadTrigger>
{itemCollection.features.length > 0 && (
{filteredFeatures.length > 0 && (
<IconButton
size={"sm"}
variant={"subtle"}
onClick={() => fitBbox(getItemCollectionExtent(itemCollection))}
onClick={() => fitBbox(getItemCollectionExtent(filteredItemCollection))}
>
<LuFocus></LuFocus>
</IconButton>
Expand All @@ -77,6 +108,9 @@ function StacGeoparquetMetadata({
}: {
metadata: StacGeoparquetMetadata;
}) {
const { dateRange } = useStacMap();
const isFilterActive = dateRange && (dateRange.startDate || dateRange.endDate);

return (
<Stack gap={4}>
<Stat.Root>
Expand All @@ -85,6 +119,17 @@ function StacGeoparquetMetadata({
<FormatNumber value={metadata.count}></FormatNumber>
</Stat.ValueText>
</Stat.Root>

{isFilterActive && (
<Card.Root variant="elevated" bg="green.50" borderColor="green.200">
<Card.Body py={2}>
<Text fontSize="sm" color="green.700">
<strong>Date Filter Active:</strong> Showing {metadata.count} filtered items
</Text>
</Card.Body>
</Card.Root>
)}

<HStack>
<MetadataDrawer metadata={metadata}></MetadataDrawer>
</HStack>
Expand Down
33 changes: 29 additions & 4 deletions src/components/stac/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import Collection from "./collection";
import Item from "./item";
import ItemCollection from "./item-collection";
import { NaturalLanguageCollectionSearch } from "./natural-language";
import { getCollectionsExtent } from "./utils";
import DateFilter from "./date-filter";
import { getCollectionsExtent, formatDateRangeForStacSearch } from "./utils";

export default function Search({
itemSearchLinks,
Expand Down Expand Up @@ -50,6 +51,15 @@ export default function Search({
onValueChange={(e) => setValue(e.value)}
variant={"enclosed"}
>
<Accordion.Item value="date-filter">
<Accordion.ItemTrigger>
<Span flex="1">Filter</Span>
<Accordion.ItemIndicator></Accordion.ItemIndicator>
</Accordion.ItemTrigger>
<Accordion.ItemContent py={4}>
<DateFilter></DateFilter>
</Accordion.ItemContent>
</Accordion.Item>
{itemSearchLinks && itemSearchLinks.length > 0 && (
<Accordion.Item value="item">
<Accordion.ItemTrigger>
Expand All @@ -74,6 +84,7 @@ export default function Search({
</Accordion.ItemContent>
</Accordion.Item>
)}

</Accordion.Root>
);
}
Expand All @@ -88,6 +99,7 @@ function ItemSearch({ links }: { links: StacLink[] }) {
searchHasNextPage,
searchNumberMatched,
item,
dateRange,
} = useStacMap();
const selectedCollections = useSelectedCollections();
const fitBbox = useFitBbox();
Expand All @@ -114,10 +126,23 @@ function ItemSearch({ links }: { links: StacLink[] }) {
if (selectedCollections) {
fitBbox(getCollectionsExtent(selectedCollections));
}

// Build search parameters including date range
const searchParams: {
collections: string[];
datetime?: string;
} = {
collections: [...selectedCollectionIds],
};

// Add datetime parameter if date range is active
const datetimeParam = formatDateRangeForStacSearch(dateRange);
if (datetimeParam) {
searchParams.datetime = datetimeParam;
}

setSearchRequest({
search: {
collections: [...selectedCollectionIds],
},
search: searchParams,
// TODO allow configuration
link: links[0],
});
Expand Down
Loading