Skip to content

Commit

Permalink
Merge pull request #24
Browse files Browse the repository at this point in the history
Add the Hydrometeorological tab
  • Loading branch information
clementprdhomme authored Nov 12, 2024
2 parents cea989f + 9bd6459 commit f222ae8
Show file tree
Hide file tree
Showing 18 changed files with 1,366 additions and 64 deletions.
6 changes: 6 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
"dependencies": {
"@artsy/fresnel": "7.1.4",
"@deck.gl/core": "9.0.34",
"@deck.gl/extensions": "9.0.35",
"@deck.gl/geo-layers": "9.0.35",
"@deck.gl/json": "9.0.34",
"@deck.gl/layers": "9.0.34",
"@deck.gl/mapbox": "9.0.35",
"@deck.gl/mesh-layers": "9.0.35",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
Expand All @@ -35,9 +39,11 @@
"@t3-oss/env-nextjs": "0.11.1",
"@tanstack/react-query": "5.59.16",
"@types/mapbox-gl": "3.4.0",
"apng-js": "1.1.4",
"axios": "1.7.7",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
"date-fns": "4.1.0",
"express": "4.21.1",
"mapbox-gl": "3.7.0",
"next": "14.2.15",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
143 changes: 121 additions & 22 deletions client/src/components/dataset-card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"use client";

import { format } from "date-fns/format";
import Link from "next/link";
import { useCallback, useMemo, useState } from "react";
import * as React from "react";

import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import MonthPicker from "@/components/ui/month-picker";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {
Select,
SelectContent,
Expand All @@ -15,10 +19,18 @@ import {
import { Switch } from "@/components/ui/switch";
import useMapLayers from "@/hooks/use-map-layers";
import { cn } from "@/lib/utils";
import CalendarDaysIcon from "@/svgs/calendar-days.svg";
import ChevronDownIcon from "@/svgs/chevron-down.svg";
import DownloadIcon from "@/svgs/download.svg";
import { DatasetLayersDataItem } from "@/types/generated/strapi.schemas";
import { LayerParamsConfig } from "@/types/layer";

import { getDefaultReturnPeriod, getDefaultSelectedLayerId, getReturnPeriods } from "./utils";
import {
getDefaultReturnPeriod,
getDefaultSelectedLayerId,
getReturnPeriods,
getDefaultDate,
} from "./utils";

interface DatasetCardProps {
id: number;
Expand All @@ -40,14 +52,29 @@ const DatasetCard = ({ id, name, defaultLayerId, layers }: DatasetCardProps) =>
[layers, layersConfiguration, defaultSelectedLayerId],
);

const defaultSelectedDate = useMemo(
() => getDefaultDate(defaultSelectedLayerId, layers, layersConfiguration),
[layers, layersConfiguration, defaultSelectedLayerId],
);

const [selectedLayerId, setSelectedLayerId] = useState(defaultSelectedLayerId);
const [selectedReturnPeriod, setSelectedReturnPeriod] = useState(defaultSelectedReturnPeriod);
const [selectedDate, setSelectedDate] = useState(defaultSelectedDate);

const selectedLayer = useMemo(
() => layers.find(({ id }) => id === selectedLayerId),
[layers, selectedLayerId],
);

const dateRange = useMemo(() => {
if (!selectedLayer) {
return undefined;
}

const paramsConfig = selectedLayer.attributes!.params_config! as LayerParamsConfig;
return paramsConfig.find(({ key }) => key === "date-range")?.default as [string, string];
}, [selectedLayer]);

const isDatasetActive = useMemo(() => {
if (selectedLayerId === undefined) {
return false;
Expand All @@ -70,27 +97,28 @@ const DatasetCard = ({ id, name, defaultLayerId, layers }: DatasetCardProps) =>
if (!active) {
removeLayer(selectedLayerId);
} else {
addLayer(selectedLayerId, { ["return-period"]: selectedReturnPeriod });
addLayer(selectedLayerId, { ["return-period"]: selectedReturnPeriod, date: selectedDate });
}
},
[selectedLayerId, addLayer, removeLayer, selectedReturnPeriod],
[selectedLayerId, addLayer, removeLayer, selectedReturnPeriod, selectedDate],
);

const onChangeSelectedLayer = useCallback(
(stringId: string) => {
const id = Number.parseInt(stringId);
const previousId = selectedLayerId;
const returnPeriod = getDefaultReturnPeriod(id, layers, layersConfiguration);
const date = getDefaultDate(id, layers, layersConfiguration);

setSelectedLayerId(id);
setSelectedReturnPeriod(returnPeriod);

// If the dataset was active and the layer is changed, we replace the current layer by the new
// one keeping all the same settings (visibility, opacity, etc.)
if (isDatasetActive && previousId !== undefined) {
updateLayer(previousId, { id, ["return-period"]: returnPeriod });
updateLayer(previousId, { id, ["return-period"]: returnPeriod, date });
} else {
addLayer(id, { ["return-period"]: returnPeriod });
addLayer(id, { ["return-period"]: returnPeriod, date });
}
},
[
Expand All @@ -107,16 +135,48 @@ const DatasetCard = ({ id, name, defaultLayerId, layers }: DatasetCardProps) =>
const onChangeSelectedReturnPeriod = useCallback(
(stringReturnPeriod: string) => {
const returnPeriod = Number.parseInt(stringReturnPeriod);
const date = getDefaultDate(selectedLayerId, layers, layersConfiguration);

setSelectedReturnPeriod(returnPeriod);

if (isDatasetActive && selectedLayerId !== undefined) {
updateLayer(selectedLayerId, { ["return-period"]: returnPeriod });
} else if (selectedLayerId !== undefined) {
addLayer(selectedLayerId, { ["return-period"]: returnPeriod });
addLayer(selectedLayerId, { ["return-period"]: returnPeriod, date });
}
},
[selectedLayerId, setSelectedReturnPeriod, isDatasetActive, addLayer, updateLayer],
[
selectedLayerId,
setSelectedReturnPeriod,
isDatasetActive,
addLayer,
updateLayer,
layers,
layersConfiguration,
],
);

const onChangeSelectedDate = useCallback(
(date: string) => {
const returnPeriod = getDefaultReturnPeriod(selectedLayerId, layers, layersConfiguration);

setSelectedDate(date);

if (isDatasetActive && selectedLayerId !== undefined) {
updateLayer(selectedLayerId, { date });
} else if (selectedLayerId !== undefined) {
addLayer(selectedLayerId, { ["return-period"]: returnPeriod, date });
}
},
[
selectedLayerId,
setSelectedReturnPeriod,
isDatasetActive,
addLayer,
updateLayer,
layers,
layersConfiguration,
],
);

return (
Expand Down Expand Up @@ -157,21 +217,23 @@ const DatasetCard = ({ id, name, defaultLayerId, layers }: DatasetCardProps) =>
</div>
</div>
<div className="mt-1 flex flex-col gap-1.5">
<Select
value={selectedLayerId !== undefined ? `${selectedLayerId}` : ""}
onValueChange={onChangeSelectedLayer}
>
<SelectTrigger aria-label="Layer">
<SelectValue placeholder="Select a layer" />
</SelectTrigger>
<SelectContent>
{layers.map((layer) => (
<SelectItem key={layer.id} value={`${layer.id}`}>
{layer.attributes?.name}
</SelectItem>
))}
</SelectContent>
</Select>
{layers.length > 1 && (
<Select
value={selectedLayerId !== undefined ? `${selectedLayerId}` : ""}
onValueChange={onChangeSelectedLayer}
>
<SelectTrigger aria-label="Layer">
<SelectValue placeholder="Select a layer" />
</SelectTrigger>
<SelectContent>
{layers.map((layer) => (
<SelectItem key={layer.id} value={`${layer.id}`}>
{layer.attributes?.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{!!layerReturnPeriods && (
<Select
value={selectedReturnPeriod !== undefined ? `${selectedReturnPeriod}` : ""}
Expand All @@ -189,6 +251,43 @@ const DatasetCard = ({ id, name, defaultLayerId, layers }: DatasetCardProps) =>
</SelectContent>
</Select>
)}
{selectedDate !== undefined && dateRange !== undefined && isDatasetActive && (
<div className="flex items-center justify-between gap-4">
<Label htmlFor={`dataset-${id}-date`} className="shrink-0 text-xs font-medium">
Displayed on map
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
id={`dataset-${id}-date`}
type="button"
variant="yellow"
className="group flex-grow justify-between px-3 xl:h-auto xl:py-1.5"
>
<CalendarDaysIcon aria-hidden />
{format(selectedDate, "MMMM, yyyy")}
<ChevronDownIcon
className="ml-auto group-data-[state=open]:rotate-180"
aria-hidden
/>
</Button>
</PopoverTrigger>
<PopoverContent
side="bottom"
align="end"
sideOffset={2}
className="w-[var(--radix-popover-trigger-width)]"
>
<MonthPicker
selected={selectedDate}
minDate={dateRange[0]}
maxDate={dateRange[1]}
onSelect={onChangeSelectedDate}
/>
</PopoverContent>
</Popover>
</div>
)}
</div>
</div>
);
Expand Down
25 changes: 25 additions & 0 deletions client/src/components/dataset-card/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,28 @@ export const getReturnPeriods = (layerId: number | undefined, layers: DatasetLay
options: [...(returnPeriod.options as number[])].sort((a, b) => a - b),
};
};

export const getDefaultDate = (
layerId: number | undefined,
layers: DatasetLayersDataItem[],
layersConfiguration: ReturnType<typeof useMapLayers>[0],
) => {
const layerConfiguration = layersConfiguration.find(({ id }) => id === layerId);

// If the layer is active and already has a selected return period, we return it
if (layerConfiguration?.["date"] !== undefined) {
return layerConfiguration["date"];
}

// Else we look for the default return period stored in `params_config`
const layer = layers.find(({ id }) => id === layerId);
const defaultDate = (layer?.attributes!.params_config as LayerParamsConfig | undefined)?.find(
({ key }) => key === "date",
);

if (!defaultDate || defaultDate.default === undefined || defaultDate.default === null) {
return undefined;
}

return defaultDate.default as string;
};
54 changes: 54 additions & 0 deletions client/src/components/map/deckgl-mapbox-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createContext, PropsWithChildren, useCallback, useMemo, useRef } from "react";

import useDeckGLMapboxOverlay from "@/hooks/use-deckgl-mapbox-overlay";

import type { Layer } from "@deck.gl/core";

interface IDeckGLMapboxOverlayContext {
addLayer: (layer: Layer) => void;
removeLayer: (layerId: string) => void;
}

export const DeckGLMapboxOverlayContext = createContext<IDeckGLMapboxOverlayContext>({
addLayer: () => {
throw new Error(
"DeckGLMapboxOverlayContext must be used within <DeckGLMapboxOverlayContext.Provider />.",
);
},
removeLayer: () => {
throw new Error(
"DeckGLMapboxOverlayContext must be used within <DeckGLMapboxOverlayContext.Provider />.",
);
},
});

const DeckglMapboxProvider = ({ children }: PropsWithChildren) => {
const layersRef = useRef<Layer[]>([]);
const deckGLMapboxOverlay = useDeckGLMapboxOverlay();

const addLayer = useCallback(
(layer: Layer) => {
layersRef.current = [...layersRef.current, layer];
deckGLMapboxOverlay.setProps({ layers: layersRef.current });
},
[deckGLMapboxOverlay],
);

const removeLayer = useCallback(
(layerId: string) => {
layersRef.current = layersRef.current.filter(({ id }) => id !== layerId);
deckGLMapboxOverlay.setProps({ layers: layersRef.current });
},
[deckGLMapboxOverlay],
);

const value = useMemo(() => ({ addLayer, removeLayer }), [addLayer, removeLayer]);

return (
<DeckGLMapboxOverlayContext.Provider value={value}>
{children}
</DeckGLMapboxOverlayContext.Provider>
);
};

export default DeckglMapboxProvider;
Loading

0 comments on commit f222ae8

Please sign in to comment.