diff --git a/src/components/preview/StationPreview/RangeBlock.js b/src/components/preview/StationPreview/RangeBlock.js
index 7f58cece..50598608 100644
--- a/src/components/preview/StationPreview/RangeBlock.js
+++ b/src/components/preview/StationPreview/RangeBlock.js
@@ -8,75 +8,76 @@ import differenceInYears from "date-fns/differenceInYears";
import startOfDecade from "date-fns/startOfDecade";
import endOfDecade from "date-fns/endOfDecade";
import { useStore } from "../../../state/state-store";
+import { useConfig } from "../../../state/query-hooks/use-config";
+import { useStationVariables } from "../../../state/query-hooks/use-station-variables";
const millisecondsPerMonth = 2629746000;
const millisedondsPerDay = 86400000;
const RangeBlock = ({}) => {
- const {
- config,
- minStartDate,
- maxEndDate,
- selectedStartDate,
- selectedEndDate,
- setSelectedStartDate,
- setSelectedEndDate,
- previewStationVariables,
- } = useStore((state) => ({
- config: state.config,
+ const { data: config } = useConfig();
+ const storeData = useStore((state) => ({
+ stationId: state.stationId,
minStartDate: state.minStartDate,
maxEndDate: state.maxEndDate,
selectedStartDate: state.selectedStartDate,
selectedEndDate: state.selectedEndDate,
+ }));
+ const actions = useStore((state) => ({
setSelectedStartDate: state.setSelectedStartDate,
setSelectedEndDate: state.setSelectedEndDate,
- previewStationVariables: state.previewStationVariables,
}));
- const startTime = startOfDecade(minStartDate);
- const endTime = addDays(endOfDecade(maxEndDate), 1);
+ const {
+ data: previewStationVariables,
+ isLoading,
+ isError,
+ } = useStationVariables(storeData.stationId);
+
+ if (!(previewStationVariables.variables?.length ?? 0 > 0)) {
+ return
This station has no variables associated with it.
;
+ }
+
+ if (isLoading || !storeData.selectedStartDate || !storeData.selectedEndDate) {
+ return
Loading...
;
+ }
- // console.log("### start", startTime);
- // console.log("### end", endTime);
+ console.log("### RangeBlock", storeData, previewStationVariables);
- const selectedInterval = [selectedStartDate, selectedEndDate];
+ const startTime = startOfDecade(storeData.minStartDate);
+ const endTime = addDays(endOfDecade(storeData.maxEndDate), 1);
+
+ const selectedInterval = [
+ storeData.selectedStartDate,
+ storeData.selectedEndDate,
+ ];
const error = null;
const ticks = differenceInYears(endTime, startTime) / 10 + 1;
- //console.log("### ticks", ticks);
-
- const onTimeRangeChange = (range) => {
- const [start, end] = range;
+ const onTimeRangeChange = ([start, end]) => {
console.log(
"### onTimeRangeChange",
start,
end,
- selectedStartDate,
- selectedEndDate,
+ storeData.selectedStartDate,
+ storeData.selectedEndDate,
);
console.log(
"### diff",
- differenceInDays(start, selectedStartDate),
- differenceInDays(end, selectedEndDate),
+ differenceInDays(start, storeData.selectedStartDate),
+ differenceInDays(end, storeData.selectedEndDate),
);
// the range control will try to adjust its range to be aligned with its "step" value.
// rejecting small adjustments made by the control prevent us getting into a loop of constant adjustments
// if changing the step value, this may need to be adjusted to reject a larger range
- if (Math.abs(differenceInDays(start, selectedStartDate)) > 1) {
- setSelectedStartDate(start);
- } else if (Math.abs(differenceInDays(end, selectedEndDate)) > 1) {
- setSelectedEndDate(end);
+ if (Math.abs(differenceInDays(start, storeData.selectedStartDate)) > 1) {
+ actions.setSelectedStartDate(start);
+ } else if (Math.abs(differenceInDays(end, storeData.selectedEndDate)) > 1) {
+ actions.setSelectedEndDate(end);
}
};
-
- // const onMode = (curr, next, step, reversed, getValue) => {
- // console.log("### mode", curr, next);
- // return curr;
- // };
-
- console.log("### config", config);
return (
{
onUpdateCallback={() => {}}
onChangeCallback={onTimeRangeChange}
//mode={onMode}
- dataIntervals={previewStationVariables.map((data) => ({
- start: new Date(data.min_obs_time),
- end: new Date(data.max_obs_time),
- type: "observation",
- color: config.plotColor,
- }))}
+ dataIntervals={
+ previewStationVariables.variables?.map((data) => ({
+ start: new Date(data.min_obs_time),
+ end: new Date(data.max_obs_time),
+ type: "observation",
+ color: config.plotColor,
+ })) ?? []
+ }
//hideHandles={true}
/>
);
diff --git a/src/components/preview/StationPreview/StationPreview.js b/src/components/preview/StationPreview/StationPreview.js
index 5943b510..04c9ff41 100644
--- a/src/components/preview/StationPreview/StationPreview.js
+++ b/src/components/preview/StationPreview/StationPreview.js
@@ -1,43 +1,24 @@
-import { useLoaderData } from "react-router-dom";
+import { useLoaderData, Link } from "react-router-dom";
import React, { useEffect } from "react";
-import { Container, Spinner } from "react-bootstrap";
-import { useShallow } from "zustand/react/shallow";
-import { useStore } from "../../../state/state-store";
+import { Container, Spinner, Row, Col } from "react-bootstrap";
import HeaderBlock from "./HeaderBlock";
import NavBlock from "./NavBlock";
import GraphsBlock from "./GraphsBlock";
+import { useStation } from "../../../state/query-hooks/use-station";
+import { useStationVariablesDefaults } from "../../../state/client-server-hooks/use-station-variables-defaults";
export default function StationPreview() {
const urlParams = useLoaderData();
- const data = useStore(
- useShallow((state) => ({
- previewStation: state.previewStation,
- previewVariables: state.previewStationVariables,
- config: state.config,
- })),
- );
- const actions = useStore((state) => ({
- isConfigLoaded: state.isConfigLoaded,
- loadPreviewStation: state.loadPreviewStation,
- loadPreviewStationVariables: state.loadPreviewStationVariables,
- }));
-
- // load station we want to do a preview for, this may be instant if the stations are
- // already loaded into the metadata. If missing it will ask the server
- useEffect(() => {
- if (actions.isConfigLoaded()) {
- actions.loadPreviewStation(urlParams.stationId);
- }
- }, [data.config, urlParams.stationId]);
+ // initialize the zustand store with defaults based on data loaded from react query.
+ const {
+ data: previewStation,
+ isLoading: isStationLoading,
+ isError: isStationError,
+ } = useStation(urlParams.stationId);
+ const { data: previewStationVariables, isLoading } =
+ useStationVariablesDefaults(urlParams.stationId);
- // once station is loaded we need to load our preview information
- useEffect(() => {
- if (actions.isConfigLoaded() && data.previewStation) {
- actions.loadPreviewStationVariables(data.previewStation.id);
- }
- }, [data.config, data.previewStation]);
-
- if (!data.previewStation) {
+ if (isLoading || isStationLoading) {
return (
@@ -47,11 +28,17 @@ export default function StationPreview() {
);
}
+ const hasVariables = (previewStationVariables?.variables?.length ?? 0) > 0;
+
return (
-
-
+ {hasVariables && (
+ <>
+
+
+ >
+ )}
);
}
diff --git a/src/index.js b/src/index.js
index 2e6f34b5..eb5ee10b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -7,7 +7,11 @@ import {
} from "react-router-dom";
import { createRoot } from "react-dom/client";
import App from "./components/main/App";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import {
+ QueryClient,
+ QueryClientProvider,
+ QueryCache,
+} from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import "bootstrap/dist/css/bootstrap.css";
@@ -38,7 +42,12 @@ const getBaseName = () => {
};
// Create a client
-const queryClient = new QueryClient();
+const queryClient = new QueryClient({
+ queryCache: new QueryCache({
+ onError: (error) =>
+ console.error("An error occurred in the query cache", error),
+ }),
+});
// Code split our bundle along our primary routes using the "lazy" function.
// https://reactrouter.com/en/main/route/lazy
diff --git a/src/state/client-server-hooks/use-config-defaults.js b/src/state/client-server-hooks/use-config-defaults.js
new file mode 100644
index 00000000..768bff29
--- /dev/null
+++ b/src/state/client-server-hooks/use-config-defaults.js
@@ -0,0 +1,55 @@
+import { useEffect } from "react";
+import L from "leaflet";
+import { setLethargicMapScrolling } from "../../utils/leaflet-extensions";
+import { setTimingEnabled } from "../../utils/timing";
+import { useConfig } from "../query-hooks/use-config";
+import { useStore } from "../state-store";
+
+/**
+ * This hook effectively runs once after config loads and provides various defaults to the app.
+ * @returns {object} results from useConfig hook.
+ */
+export const useConfigDefaults = () => {
+ const { data: config, isLoading, isError } = useConfig();
+ const setStationLimit = useStore((state) => state.setStationLimit);
+
+ useEffect(() => {
+ if (isLoading || isError || !!config) {
+ return;
+ }
+
+ // Set browser title
+ document.title = config.appTitle;
+
+ // Provide Leaflet defaults.
+ L.drawLocal.edit.toolbar.buttons = {
+ edit: "Edit shapes",
+ editDisabled: "No shapes to edit",
+ remove: "Remove shapes",
+ removeDisabled: "No shapes to remove",
+ };
+ L.drawLocal.edit.handlers.remove.tooltip = "Click shape to remove";
+ L.drawLocal.edit.toolbar.actions.clearAll = {
+ title: "Remove all shapes",
+ text: "Remove all",
+ };
+
+ // Initialize Lethargy, which fixes scrolling problems with Mac computers.
+ if (config.lethargy.enabled) {
+ setLethargicMapScrolling(
+ config.lethargy.stability,
+ config.lethargy.sensitivity,
+ config.lethargy.tolerance,
+ );
+ }
+
+ // Export timing values to non-component code
+ setTimingEnabled(config.timingEnabled);
+
+ setStationLimit(config.stationLimit);
+ }, [config]);
+
+ return { isLoading, isError, data: config };
+};
+
+export default useConfigDefaults;
diff --git a/src/state/client-server-hooks/use-station-variables-defaults.js b/src/state/client-server-hooks/use-station-variables-defaults.js
new file mode 100644
index 00000000..ef093825
--- /dev/null
+++ b/src/state/client-server-hooks/use-station-variables-defaults.js
@@ -0,0 +1,56 @@
+import { useEffect } from "react";
+import flow from "lodash/fp/flow";
+import map from "lodash/fp/map";
+import max from "date-fns/max";
+import min from "date-fns/min";
+import subMonths from "date-fns/subMonths";
+import parseIso from "date-fns/parseISO";
+import { useStore } from "../state-store";
+import { useStationVariables } from "../query-hooks/use-station-variables";
+
+const getMaxEndDate = flow(
+ map("max_obs_time"), // (string []) Pluck max_obs_time from variable objects (ISO 8601 date string)
+ map(parseIso), // (Date []) Parse ISO 8601 date strings to JS Date objects
+ max, // (Date) Get the latest date
+);
+const getMinStartDate = flow(
+ map("min_obs_time"), // (string []) Pluck min_obs_time from variable objects (ISO 8601 date string)
+ map(parseIso), // (Date []) Parse ISO 8601 date strings to JS Date objects
+ min, // (Date) Get the earliest date
+);
+
+/**
+ * Layer 3. Integration between async server state and Zustand client state for preview station variables
+ * @param {number} stationId
+ * @returns
+ */
+export const useStationVariablesDefaults = (stationId) => {
+ const { data, isLoading, isError } = useStationVariables(stationId);
+ const selectedDuration = useStore((state) => state.selectedDuration);
+ const storeActions = useStore((state) => ({
+ setStationId: state.setStationId,
+ setSelectedEndDate: state.setSelectedEndDate,
+ setMaxEndDate: state.setMaxEndDate,
+ setMinStartDate: state.setMinStartDate,
+ }));
+
+ useEffect(() => {
+ // if stationid changes, this will clear default ranges and set duration back to default
+ storeActions.setStationId(stationId);
+
+ if (data && data.variables?.length > 0) {
+ const maxEndDate = getMaxEndDate(data.variables);
+ const selectedStartDate = subMonths(maxEndDate, selectedDuration);
+
+ console.log("### selected date range", selectedStartDate, maxEndDate);
+
+ // set default ranges
+ storeActions.setMinStartDate(getMinStartDate(data.variables));
+ storeActions.setMaxEndDate(maxEndDate);
+ // end date will also update start date based on selected duration.
+ storeActions.setSelectedEndDate(maxEndDate);
+ }
+ }, [stationId, data]);
+
+ return { isLoading, isError, stationId, data };
+};
diff --git a/src/state/query-hooks/use-config.js b/src/state/query-hooks/use-config.js
index b37faf99..7d00e29c 100644
--- a/src/state/query-hooks/use-config.js
+++ b/src/state/query-hooks/use-config.js
@@ -84,32 +84,17 @@ const getZoomMarkerRadius = (zmrSpec) => {
};
};
+/**
+ * Layer 1. Server fetch and config parsing.
+ * @returns {Promise