diff --git a/components/AvalancheForecastZoneMap.tsx b/components/AvalancheForecastZoneMap.tsx index 59ba3169..99f14e25 100644 --- a/components/AvalancheForecastZoneMap.tsx +++ b/components/AvalancheForecastZoneMap.tsx @@ -38,7 +38,7 @@ export interface MapProps { export const AvalancheForecastZoneMap: React.FunctionComponent = ({center, requestedTime}: MapProps) => { const {logger} = React.useContext(LoggerContext); - const mapLayerResult = useMapLayer(center); + const mapLayerResult = useMapLayer(center, requestedTime); const mapLayer = mapLayerResult.data; const metadataResult = useAvalancheCenterMetadata(center); const metadata = metadataResult.data; diff --git a/components/forecast/WeatherTab.tsx b/components/forecast/WeatherTab.tsx index 20049a5f..7d4c562b 100644 --- a/components/forecast/WeatherTab.tsx +++ b/components/forecast/WeatherTab.tsx @@ -151,7 +151,7 @@ export const NWACWeatherTab: React.FC = ({zone, center_id, requ const metadata = avalancheCenterMetadataResult.data; const nwacForecastResult = useNWACWeatherForecast(center_id, zone.id, requestedTime); const nwacForecast = nwacForecastResult.data; - const mapLayerResult = useMapLayer(center_id); + const mapLayerResult = useMapLayer(center_id, requestedTime); const mapLayer = mapLayerResult.data; const weatherStationsResult = useWeatherStationsMetadata(center_id, metadata?.widget_config.stations?.token); const weatherStations = weatherStationsResult.data; diff --git a/components/form/LocationField.tsx b/components/form/LocationField.tsx index 24a51f96..8c71ce5b 100644 --- a/components/form/LocationField.tsx +++ b/components/form/LocationField.tsx @@ -14,25 +14,27 @@ import MapView, {LatLng, MapMarker, Region} from 'react-native-maps'; import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; import {colorLookup} from 'theme'; import {AvalancheCenterID} from 'types/nationalAvalancheCenter'; +import {RequestedTime} from 'utils/date'; interface LocationFieldProps { name: KeysMatching; label: string; center: AvalancheCenterID; disabled?: boolean; + requestedTime: RequestedTime; } const latLngToLocationPoint = (latLng: LatLng) => ({lat: latLng.latitude, lng: latLng.longitude}); const locationPointToLatLng = (locationPoint: LocationPoint) => ({latitude: locationPoint.lat, longitude: locationPoint.lng}); -export const LocationField = React.forwardRef(({name, label, center, disabled}, ref) => { +export const LocationField = React.forwardRef(({name, label, center, disabled, requestedTime}, ref) => { const { field, fieldState: {error}, } = useController({name: name}); const [modalVisible, setModalVisible] = useState(false); - const mapLayerResult = useMapLayer(center); + const mapLayerResult = useMapLayer(center, requestedTime); const mapLayer = mapLayerResult.data; const [initialRegion, setInitialRegion] = useState(defaultMapRegionForZones([])); const [mapReady, setMapReady] = useState(false); diff --git a/components/observations/ObservationDetailView.tsx b/components/observations/ObservationDetailView.tsx index e2c4a1cd..0860f4dc 100644 --- a/components/observations/ObservationDetailView.tsx +++ b/components/observations/ObservationDetailView.tsx @@ -50,14 +50,14 @@ import { SnowAvailableForTransport, WindLoading, } from 'types/nationalAvalancheCenter'; -import {pacificDateToLocalShortDateString, utcDateToLocalShortDateString, utcDateToLocalTimeString} from 'utils/date'; +import {pacificDateToLocalShortDateString, parseRequestedTimeString, utcDateToLocalShortDateString, utcDateToLocalTimeString} from 'utils/date'; export const NWACObservationDetailView: React.FunctionComponent<{ id: string; }> = ({id}) => { const observationResult = useNWACObservation(parseInt(id)); const observation = observationResult.data; - const mapResult = useMapLayer(observation?.center_id); + const mapResult = useMapLayer(observation?.center_id, observation?.created_at ? parseRequestedTimeString(observation?.created_at) : undefined); const mapLayer = mapResult.data; if (incompleteQueryState(observationResult, mapResult) || !observation || !mapLayer) { @@ -72,7 +72,10 @@ export const ObservationDetailView: React.FunctionComponent<{ }> = ({id}) => { const observationResult = useNACObservation(id); const observation = observationResult.data; - const mapResult = useMapLayer(observation?.center_id?.toUpperCase() as AvalancheCenterID); + const mapResult = useMapLayer( + observation?.center_id?.toUpperCase() as AvalancheCenterID, + observation?.created_at ? parseRequestedTimeString(observation?.created_at) : undefined, + ); const mapLayer = mapResult.data; if (incompleteQueryState(observationResult, mapResult) || !observation || !mapLayer) { diff --git a/components/observations/ObservationsListView.tsx b/components/observations/ObservationsListView.tsx index c0e2a1d4..487461d7 100644 --- a/components/observations/ObservationsListView.tsx +++ b/components/observations/ObservationsListView.tsx @@ -38,7 +38,7 @@ import { import {ObservationsStackNavigationProps} from 'routes'; import {colorLookup} from 'theme'; import {AvalancheCenterID, DangerLevel, MediaType, ObservationFragment, PartnerType} from 'types/nationalAvalancheCenter'; -import {RequestedTime, pacificDateToLocalDateString, requestedTimeToUTCDate} from 'utils/date'; +import {RequestedTime, formatRequestedTime, pacificDateToLocalDateString, requestedTimeToUTCDate} from 'utils/date'; interface ObservationsListViewItem { id: ObservationFragment['id']; @@ -70,7 +70,7 @@ export const ObservationsListView: React.FunctionComponent createDefaultFilterConfig(requestedTime, additionalFilters), [requestedTime, additionalFilters]); const [filterConfig, setFilterConfig] = useState(originalFilterConfig); const [filterModalVisible, {set: setFilterModalVisible, on: showFilterModal}] = useToggle(false); - const mapResult = useMapLayer(center_id); + const mapResult = useMapLayer(center_id, requestedTime); const mapLayer = mapResult.data; const postHog = usePostHog(); @@ -220,8 +220,8 @@ export const ObservationsListView: React.FunctionComponent { - navigation.navigate('observationSubmit', {center_id}); - }, [navigation, center_id]); + navigation.navigate('observationSubmit', {center_id: center_id, requestedTime: formatRequestedTime(requestedTime)}); + }, [navigation, center_id, requestedTime]); const [showSubmitButtonText, setShowSubmitButtonText] = useState(true); const onScroll = useCallback( diff --git a/components/observations/ObservationsPortal.tsx b/components/observations/ObservationsPortal.tsx index 45e5aecd..ef7dea33 100644 --- a/components/observations/ObservationsPortal.tsx +++ b/components/observations/ObservationsPortal.tsx @@ -19,7 +19,10 @@ export const ObservationsPortal: React.FC<{ () => navigation.navigate('observationsList', {center_id, requestedTime: formatRequestedTime(requestedTime)}), [center_id, navigation, requestedTime], ); - const onSubmit = useCallback(() => navigation.navigate('observationSubmit', {center_id}), [center_id, navigation]); + const onSubmit = useCallback( + () => navigation.navigate('observationSubmit', {center_id: center_id, requestedTime: formatRequestedTime(requestedTime)}), + [center_id, navigation, requestedTime], + ); const postHog = usePostHog(); const recordAnalytics = useCallback(() => { diff --git a/components/observations/SimpleForm.tsx b/components/observations/SimpleForm.tsx index 9260c7e8..602b45c8 100644 --- a/components/observations/SimpleForm.tsx +++ b/components/observations/SimpleForm.tsx @@ -35,7 +35,7 @@ import Toast from 'react-native-toast-message'; import {ObservationsStackNavigationProps} from 'routes'; import {colorLookup} from 'theme'; import {AvalancheCenterID, ImageMediaItem, InstabilityDistribution, MediaType, userFacingCenterId} from 'types/nationalAvalancheCenter'; -import {startOfSeasonLocalDate} from 'utils/date'; +import {RequestedTime, startOfSeasonLocalDate} from 'utils/date'; const ImageListOverlay: React.FC<{index: number; onPress: (index: number) => void}> = ({index, onPress}) => { const onPressHandler = useCallback(() => { @@ -58,8 +58,9 @@ const ImageListOverlay: React.FC<{index: number; onPress: (index: number) => voi export const SimpleForm: React.FC<{ center_id: AvalancheCenterID; + requestedTime: RequestedTime; onClose?: () => void; -}> = ({center_id, onClose}) => { +}> = ({center_id, requestedTime, onClose}) => { const metadataResult = useAvalancheCenterMetadata(center_id); const metadata = metadataResult.data; const navigation = useNavigation(); @@ -458,7 +459,14 @@ export const SimpleForm: React.FC<{ }} disabled={disableFormControls} /> - + Signs of instability}> diff --git a/components/screens/ObservationsScreen.tsx b/components/screens/ObservationsScreen.tsx index 225fbcad..aeb94b1b 100644 --- a/components/screens/ObservationsScreen.tsx +++ b/components/screens/ObservationsScreen.tsx @@ -33,8 +33,8 @@ const ObservationsPortalScreen = ({route}: NativeStackScreenProps) => { - const {center_id} = route.params; - return ; + const {center_id, requestedTime} = route.params; + return ; }; const ObservationsListScreen = ({route}: NativeStackScreenProps) => { diff --git a/components/weather_data/NWACWeatherStationList.tsx b/components/weather_data/NWACWeatherStationList.tsx index 94ab0897..a9c64412 100644 --- a/components/weather_data/NWACWeatherStationList.tsx +++ b/components/weather_data/NWACWeatherStationList.tsx @@ -13,7 +13,7 @@ import {logger} from 'logger'; import {WeatherStackNavigationProps} from 'routes'; import {colorLookup} from 'theme'; import {MapLayer, MapLayerFeature, WeatherStationCollection, WeatherStationProperties, WeatherStationSource} from 'types/nationalAvalancheCenter'; -import {RequestedTimeString} from 'utils/date'; +import {parseRequestedTimeString, RequestedTimeString} from 'utils/date'; const stationGroupMapping = { // Snoqualmie Pass @@ -115,7 +115,7 @@ export const NWACStationsByZone = (mapLayer: MapLayer | undefined, stations: Wea export const NWACStationList: React.FunctionComponent<{token: string; requestedTime: RequestedTimeString}> = ({token, requestedTime}) => { const navigation = useNavigation(); - const mapLayerResult = useMapLayer('NWAC'); + const mapLayerResult = useMapLayer('NWAC', parseRequestedTimeString(requestedTime)); const mapLayer = mapLayerResult.data; const weatherStationsResult = useWeatherStationsMetadata('NWAC', token); const weatherStations = weatherStationsResult.data; diff --git a/components/weather_data/WeatherStationPage.tsx b/components/weather_data/WeatherStationPage.tsx index 1b57e2f3..ed0baab0 100644 --- a/components/weather_data/WeatherStationPage.tsx +++ b/components/weather_data/WeatherStationPage.tsx @@ -11,7 +11,7 @@ import {usePostHog} from 'posthog-react-native'; import React, {useCallback} from 'react'; import {AvalancheCenterID} from 'types/nationalAvalancheCenter'; import {NotFoundError} from 'types/requests'; -import {RequestedTimeString} from 'utils/date'; +import {parseRequestedTimeString, RequestedTimeString} from 'utils/date'; interface Props { center_id: AvalancheCenterID; @@ -51,7 +51,7 @@ export const WeatherStations: React.FunctionComponent<{ requestedTime: RequestedTimeString; }> = ({center_id, token, requestedTime}) => { const [list, {toggle: toggleList}] = useToggle(false); - const mapLayerResult = useMapLayer(center_id); + const mapLayerResult = useMapLayer(center_id, parseRequestedTimeString(requestedTime)); const mapLayer = mapLayerResult.data; const weatherStationsResult = useWeatherStationsMetadata(center_id, token); const weatherStations = weatherStationsResult.data; diff --git a/hooks/useMapLayer.ts b/hooks/useMapLayer.ts index 929c369e..8cc21db2 100644 --- a/hooks/useMapLayer.ts +++ b/hooks/useMapLayer.ts @@ -11,12 +11,13 @@ import {formatDistanceToNowStrict} from 'date-fns'; import {safeFetch} from 'hooks/fetch'; import {LoggerContext, LoggerProps} from 'loggerContext'; import {AvalancheCenterID, MapLayer, mapLayerSchema} from 'types/nationalAvalancheCenter'; +import {apiDateString, RequestedTime, requestedTimeToUTCDate} from 'utils/date'; import {ZodError} from 'zod'; -export const useMapLayer = (center_id: AvalancheCenterID | undefined): UseQueryResult => { +export const useMapLayer = (center_id: AvalancheCenterID | undefined, requestedTime: RequestedTime | undefined): UseQueryResult => { const {nationalAvalancheCenterHost} = React.useContext(ClientContext); const {logger} = React.useContext(LoggerContext); - const key = center_id && queryKey(nationalAvalancheCenterHost, center_id); + const key = center_id && requestedTime && queryKey(nationalAvalancheCenterHost, center_id, requestedTime); const [thisLogger] = useState(logger.child({query: key})); useEffect(() => { thisLogger.debug('initiating query'); @@ -24,18 +25,25 @@ export const useMapLayer = (center_id: AvalancheCenterID | undefined): UseQueryR return useQuery({ queryKey: key, - queryFn: async (): Promise => (center_id ? fetchMapLayer(nationalAvalancheCenterHost, center_id, thisLogger) : new Promise(() => null)), - enabled: !!center_id, + queryFn: async (): Promise => + center_id && requestedTime ? fetchMapLayer(nationalAvalancheCenterHost, center_id, requestedTime, thisLogger) : new Promise(() => null), + enabled: !!center_id && !!requestedTime, cacheTime: Infinity, // hold on to this cached data forever }); }; -function queryKey(nationalAvalancheCenterHost: string, center_id: string) { - return ['map-layer', {host: nationalAvalancheCenterHost, center: center_id}]; +function queryKey(nationalAvalancheCenterHost: string, center_id: string, requestedTime: RequestedTime) { + return [`map-layer`, {host: nationalAvalancheCenterHost, center: center_id, requestedTime: requestedTime}]; } -export const prefetchMapLayer = async (queryClient: QueryClient, nationalAvalancheCenterHost: string, center_id: AvalancheCenterID, logger: Logger) => { - const key = queryKey(nationalAvalancheCenterHost, center_id); +export const prefetchMapLayer = async ( + queryClient: QueryClient, + nationalAvalancheCenterHost: string, + center_id: AvalancheCenterID, + requestedTime: RequestedTime, + logger: Logger, +) => { + const key = queryKey(nationalAvalancheCenterHost, center_id, requestedTime); const thisLogger = logger.child({query: key}); thisLogger.debug('initiating query'); @@ -44,7 +52,7 @@ export const prefetchMapLayer = async (queryClient: QueryClient, nationalAvalanc queryFn: async (): Promise => { const start = new Date(); thisLogger.trace(`prefetching`); - const result = await fetchMapLayer(nationalAvalancheCenterHost, center_id, thisLogger); + const result = await fetchMapLayer(nationalAvalancheCenterHost, center_id, requestedTime, thisLogger); thisLogger.trace({duration: formatDistanceToNowStrict(start)}, `finished prefetching`); return result; }, @@ -53,11 +61,24 @@ export const prefetchMapLayer = async (queryClient: QueryClient, nationalAvalanc }); }; -const fetchMapLayer = async (nationalAvalancheCenterHost: string, center_id: AvalancheCenterID, logger: Logger): Promise => { +const fetchMapLayer = async (nationalAvalancheCenterHost: string, center_id: AvalancheCenterID, requestedTime: RequestedTime, logger: Logger): Promise => { const url = `${nationalAvalancheCenterHost}/v2/public/products/map-layer/${center_id}`; + const params = + requestedTime === 'latest' + ? {} + : { + day: apiDateString(requestedTimeToUTCDate(requestedTime)), + }; const what = 'avalanche avalanche center map layer'; - const thisLogger = logger.child({url: url, what: what}); - const data = await safeFetch(() => axios.get>(url), thisLogger, what); + const thisLogger = logger.child({url: url, params: params, what: what}); + const data = await safeFetch( + () => + axios.get>(url, { + params: params, + }), + thisLogger, + what, + ); const parseResult = mapLayerSchema.safeParse(data); if (!parseResult.success) { diff --git a/network/prefetchAllActiveForecasts.ts b/network/prefetchAllActiveForecasts.ts index 56ea8342..32c6c11c 100644 --- a/network/prefetchAllActiveForecasts.ts +++ b/network/prefetchAllActiveForecasts.ts @@ -21,6 +21,7 @@ import TimeseriesQuery from 'hooks/useWeatherStationTimeseries'; import { AllAvalancheCenterCapabilities, AvalancheCenter, + AvalancheCenterCapabilities, AvalancheCenterID, AvalancheForecastZoneStatus, ForecastResult, @@ -58,11 +59,13 @@ export const prefetchAllActiveForecasts = async ( void preloadAvalancheCenterLogo(queryClient, logger, id); }); + const centerCapabilities: AvalancheCenterCapabilities | undefined = capabilities?.centers.find(center => (center.id as AvalancheCenterID) === center_id); + await AvalancheCenterMetadataQuery.prefetch(queryClient, nationalAvalancheCenterHost, center_id, logger); const metadata = queryClient.getQueryData(AvalancheCenterMetadataQuery.queryKey(nationalAvalancheCenterHost, center_id)); if (metadata?.widget_config?.danger_map) { - void AvalancheCenterMapLayerQuery.prefetch(queryClient, nationalAvalancheCenterHost, center_id, logger); + void AvalancheCenterMapLayerQuery.prefetch(queryClient, nationalAvalancheCenterHost, center_id, requestedTime, logger); } const endDate: Date = currentDateTime; @@ -71,12 +74,12 @@ export const prefetchAllActiveForecasts = async ( // This call will prefetch the 50 most recent void NWACObservationsQuery.prefetch(queryClient, nwacHost, center_id, endDate, logger); } - if (metadata?.widget_config?.danger_map) { + if (centerCapabilities?.platforms.obs) { // NAC fetches in 2 week chunks working backwards from endDate void NACObservationsQuery.prefetch(queryClient, nationalAvalancheCenterHost, center_id, endDate, logger); } - if (metadata?.widget_config?.stations?.token) { + if (centerCapabilities?.platforms.stations && metadata?.widget_config?.stations?.token) { await WeatherStationsQuery.prefetch(queryClient, snowboundHost, center_id, metadata?.widget_config?.stations?.token, logger); const weatherStations = queryClient.getQueryData(WeatherStationsQuery.queryKey(snowboundHost, center_id)); const stationIds: Record = weatherStations @@ -85,7 +88,7 @@ export const prefetchAllActiveForecasts = async ( void TimeseriesQuery.prefetch(queryClient, snowboundHost, metadata?.widget_config?.stations?.token, logger, stationIds, requestedTime, {days: 1}); } - if (metadata?.widget_config?.forecast) { + if (centerCapabilities?.platforms.forecasts && metadata?.widget_config?.forecast) { metadata?.zones .filter(zone => zone.status === AvalancheForecastZoneStatus.Active) .forEach(zone => { diff --git a/routes.ts b/routes.ts index 097e7b5b..f32dacb2 100644 --- a/routes.ts +++ b/routes.ts @@ -84,6 +84,7 @@ export type ObservationsStackParamList = { }; observationSubmit: { center_id: AvalancheCenterID; + requestedTime: RequestedTimeString; }; observationsList: { center_id: AvalancheCenterID;