From e1b57ad0fa0cedbb187754f9ac4f156010c560c7 Mon Sep 17 00:00:00 2001 From: Jonas Jaszkowic Date: Wed, 17 Jul 2024 09:11:04 +0200 Subject: [PATCH] feat: use snapshotted demo data, make independent of backend (#124) * feat: initial migration * feat: use mock data for shading and rain * fix: types * chore: prettier * fix: path to static files * fix: type errors * feat: fetch tree data from file * fix: type errors * fix: paths to static files * feat: move all data into one static file * feat: use static forecast data * feat: use randomly generated forecast values * fix: types, delete test * fix: types * feat: constrict random values to be more realistic * feat: demo data hint * chore: cleanup * chore: add comments * chore: readme * feat: return random watering data for some trees * chore: readme hint * fix: types * feat: mock issue types * feat: mock feedback requests * fix: types * fix: async types * fix: typo --- README.md | 36 ++++++++ locales/de/common.json | 4 + pages/trees/[id].tsx | 14 +-- public/issue_types.json | 14 +++ public/trees.geojson | 1 + .../FeedbackConfirmation.tsx | 4 +- src/components/FeedbackRequestsList/index.tsx | 4 +- src/components/TreesMap/index.tsx | 31 +++---- src/components/TreesMap/treesLayer.ts | 48 ++++------- src/components/WaterSupplyLegend/index.tsx | 21 +++++ src/lib/hooks/useFeedbackData/index.tsx | 17 ++-- src/lib/hooks/useForecastData/index.tsx | 5 +- src/lib/hooks/useGdkTreeId/index.ts | 15 +--- src/lib/hooks/useNowcastData/index.ts | 54 +++++++++--- src/lib/hooks/useShadingData/index.ts | 5 +- src/lib/hooks/useTreeData/index.tsx | 84 ++++++++++++++++-- src/lib/hooks/useWateringData/index.ts | 5 +- src/lib/requests/getForecastData.ts | 86 ++++++++++--------- src/lib/requests/getIssueTypesData.ts | 18 +--- src/lib/requests/getShading.ts | 62 ++++--------- src/lib/requests/getTreeRainAmount.ts | 36 +++----- src/lib/requests/getWateringValue/index.ts | 55 ++---------- .../mapRowsToDepths/mapRowsToDepths.test.ts | 77 ----------------- .../utils/mapRowsToDepths/mapRowsToDepths.ts | 41 +++------ 24 files changed, 351 insertions(+), 386 deletions(-) create mode 100644 public/issue_types.json create mode 100644 public/trees.geojson delete mode 100644 src/lib/utils/mapRowsToDepths/mapRowsToDepths.test.ts diff --git a/README.md b/README.md index fc6aafa2..59ba324c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,42 @@ # Quantified Trees – Baumblick > The app **Baumblick** is part of a federally funded project called [**Quantified Trees**](https://qtrees.ai/) (QTrees). It is thus part of the German adoption strategy to climate change with focus on how to help city trees to not suffer and die because of rising temperatures and more and more frequent droughts. The app tells the story of each Berlin city tree by using a vast amount of open data like location and tree specific data. On an interactive map users can see how thirsty city trees of Berlin are. More precisely, it visualizes the trees' ground suction tension. This suction tension represents the amount of energy tree roots need in order to suck out water from the soil. Using open data as well as sensors distributed under the ground, an AI developed by [Birds on Mars](https://www.birdsonmars.com/) is able to generate nowcasts and a 14-days forecasts for each tree – even for those that are not equipped with sensors! The app is oriented towards the public and should inform in a simple and intuitive way. +## Attention: This project is kept in "Demo Mode"(2024-07-11) +This project is kept in "Demo Mode" since 2024-07-11. To keep it working without backend, database and vector tiles, we have prepared static Demo data that is used instead. Please see https://github.com/technologiestiftung/baumblick-frontend/pull/124 for reference. The snapshot data has the following format and contains all data needed for the frontend to work properly with ~47k trees. Please not that the forecast values and watering values are generated randomly. The features "Missnutzung der Baumscheibe melden" and "Baumschäden melden" are not connected to any backend anymore and have no effect. + +``` +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 13.502675622307946, + 52.40368675459003 + ] + }, + "properties": { + "trees_id": "00008100:0011dd1c", + "nowcast_values_30cm": 105.91428, + "nowcast_values_60cm": 108.66888, + "nowcast_values_90cm": 41.63244, + "nowcast_values_stamm": 85.4052, + "trees_lat": 52.40368675459003, + "trees_lng": 13.502675622307946, + "baumscheibe_m2": null, + "shading_spring": 0.63, + "shading_summer": 0.5, + "shading_fall": 0.63, + "shading_winter": 0.8, + "rainfall_in_mm": 1.210145 + } + } + ] +} +``` + ## Context Climate change is causing increasingly hot, dry weather in many places. In recent years, Berlin has also experienced more hot days than ever before. Determining whether trees are in need of water isn't as easy as looking at the ground on the surface level. Many factors such as the tree's age, specie, plate size or ground quality play an important role. Old trees, for instance, tend to have deep roots and thereby be less dependent on additional watering. Overwatering can in fact be more detrimental to a tree than helpful. diff --git a/locales/de/common.json b/locales/de/common.json index 6a07a092..1ebad320 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -82,6 +82,8 @@ "contentsLink": "Inhalte" }, "legend": { + "demoDataTitle": "DEMO:", + "demoDataHint": "Diese Karte zeigt exemplarische Daten.", "map": { "title": "Wasserversorgung", "levels": { @@ -178,7 +180,9 @@ "modalConfirm": "Melden", "modalCancel": "Abbrechen", "confirmationTitle": "Danke! Erfolgreich gemeldet!", + "demoTitle": "Demo-Modus aktiv", "confirmationDescription": "Ab morgen kannst du wieder etwas melden", + "demoDescription": "Diese Funktion ist im Demo-Modus nicht aktiv, es wurde nichts gemeldet.", "loading": "Infos werden geladen...", "error": "Es ist beim Laden der Infos ein Fehler aufgetreten:" } diff --git a/pages/trees/[id].tsx b/pages/trees/[id].tsx index 059f9e1b..0b494626 100644 --- a/pages/trees/[id].tsx +++ b/pages/trees/[id].tsx @@ -97,17 +97,16 @@ const TreePage: TreePageWithLayout = ({ treeId, csrfToken }) => { data: nowcastData, error: nowcastError, isLoading: nowcastIsLoading, - } = useNowcastData(treeData?.id, csrfToken) + } = useNowcastData(treeData?.id) const { data: shadingData, error: shadingError, isLoading: shadingIsLoading, - } = useShadingData(treeData?.id, csrfToken) + } = useShadingData(treeData?.id) const { data: forecastData, error: forecastError } = useForecastData( - treeData?.id, - csrfToken + treeData?.id ) const { data: gdkTreeId, isLoading: gdkTreeIdIsLoading } = useGdkTreeId( @@ -124,7 +123,7 @@ const TreePage: TreePageWithLayout = ({ treeId, csrfToken }) => { data: wateringData, error: wateringDataError, isLoading: wateringDataIsLoading, - } = useWateringData(treeData?.id, csrfToken) + } = useWateringData(treeData?.id) if (treeDataError) { void push('/404') @@ -185,7 +184,8 @@ const TreePage: TreePageWithLayout = ({ treeId, csrfToken }) => { 'md:border-l md:border-r border-gray-200', 'row-start-2 row-span-1', 'grid grid-cols-1 grid-rows-auto', - 'motion-safe:animate-slide-up' + 'motion-safe:animate-slide-up', + 'mt-12 lg:mt-0' )} >
{ >
) diff --git a/src/components/FeedbackRequestsList/index.tsx b/src/components/FeedbackRequestsList/index.tsx index a6173714..c0838b0d 100644 --- a/src/components/FeedbackRequestsList/index.tsx +++ b/src/components/FeedbackRequestsList/index.tsx @@ -15,12 +15,10 @@ interface FeedbackRequestsListPropType { export const FeedbackRequestsList: FC = ({ treeData, - csrfToken, }) => { const { t } = useTranslation('common') const { issues, reportIssue, isLoading, error } = useFeedbackData( - treeData?.id, - csrfToken + treeData?.id ) const [openedIssueModal, setOpenedIssueModal] = useState(null) diff --git a/src/components/TreesMap/index.tsx b/src/components/TreesMap/index.tsx index 406b4bd2..1f904bd3 100644 --- a/src/components/TreesMap/index.tsx +++ b/src/components/TreesMap/index.tsx @@ -1,4 +1,11 @@ -import { FC, useCallback, useEffect, useRef, useState } from 'react' +import { TreeDataType } from '@lib/requests/getTreeData' +import { ViewportProps } from '@lib/types/map' +import { + NEXT_PUBLIC_MAPTILER_BASEMAP_URL, + NEXT_PUBLIC_MAPTILER_KEY, +} from '@lib/utils/envUtil' +import { mapRawQueryToState } from '@lib/utils/queryUtil' +import classNames from 'classnames' import maplibregl, { AttributionControl, GeolocateControl, @@ -7,25 +14,17 @@ import maplibregl, { MapGeoJSONFeature, NavigationControl, } from 'maplibre-gl' -import { mapRawQueryToState } from '@lib/utils/queryUtil' import { useRouter } from 'next/router' +import { FC, useCallback, useEffect, useRef, useState } from 'react' import { useDebouncedCallback } from 'use-debounce' -import { ViewportProps } from '@lib/types/map' +import { MapTilerLogo } from './MapTilerLogo' import { - TREES_LAYER_ID, + TREES_ID_KEY, TREES_LAYER, + TREES_LAYER_ID, TREES_SOURCE, TREES_SOURCE_ID, - TREES_SOURCE_LAYER_ID, - TREES_ID_KEY, } from './treesLayer' -import { MapTilerLogo } from './MapTilerLogo' -import classNames from 'classnames' -import { - NEXT_PUBLIC_MAPTILER_BASEMAP_URL, - NEXT_PUBLIC_MAPTILER_KEY, -} from '@lib/utils/envUtil' -import { TreeDataType } from '@lib/requests/getTreeData' interface OnSelectOutput { id: string @@ -247,7 +246,6 @@ export const TreesMap: FC = ({ map.current.setFeatureState( { source: TREES_SOURCE_ID, - sourceLayer: TREES_SOURCE_LAYER_ID, id: currentSelectedTreeId, }, { selected: true } @@ -263,7 +261,6 @@ export const TreesMap: FC = ({ map.current.setFeatureState( { source: TREES_SOURCE_ID, - sourceLayer: TREES_SOURCE_LAYER_ID, id: hoveredTreeId, }, { hover: false } @@ -276,7 +273,6 @@ export const TreesMap: FC = ({ map.current.setFeatureState( { source: TREES_SOURCE_ID, - sourceLayer: TREES_SOURCE_LAYER_ID, id: e.features[0].id, }, { hover: true } @@ -291,7 +287,6 @@ export const TreesMap: FC = ({ map.current.setFeatureState( { source: TREES_SOURCE_ID, - sourceLayer: TREES_SOURCE_LAYER_ID, id: hoveredTreeId, }, { hover: false } @@ -345,7 +340,6 @@ export const TreesMap: FC = ({ map.current.setFeatureState( { source: TREES_SOURCE_ID, - sourceLayer: TREES_SOURCE_LAYER_ID, id: treeIdToSelect, }, { selected: true } @@ -360,7 +354,6 @@ export const TreesMap: FC = ({ map.current.setFeatureState( { source: TREES_SOURCE_ID, - sourceLayer: TREES_SOURCE_LAYER_ID, id: currentSelectedTreeId, }, { selected: false } diff --git a/src/components/TreesMap/treesLayer.ts b/src/components/TreesMap/treesLayer.ts index 8ce29b15..d552fb7f 100644 --- a/src/components/TreesMap/treesLayer.ts +++ b/src/components/TreesMap/treesLayer.ts @@ -1,5 +1,4 @@ import { WATER_SUPPLY_STATUSES } from '@lib/utils/mapSuctionTensionToStatus' -import { startOfYesterday } from 'date-fns' import { LayerSpecification, SourceSpecification } from 'maplibre-gl' import colors from '../../style/colors' @@ -20,12 +19,15 @@ export const TREES_SOURCE_LAYER_ID = 'treesgeo' const NOWCAST_AVERAGE_PROPERTY = 'nowcast_values_stamm' +// 2024-07-11: This project is about to be archived. +// For archiving purposes, we render a selection of static trees on the map and make the the frontend independent of backend, database and vector tiles. export const TREES_SOURCE: SourceSpecification = { - type: 'vector', - tiles: [process.env.NEXT_PUBLIC_TREE_TILES_URL as string], - maxzoom: 14, - minzoom: 0, + type: 'geojson', + data: '/trees.geojson', promoteId: TREES_ID_KEY, + cluster: true, + clusterMaxZoom: 14, + clusterRadius: 5, } const CIRCLE_STROKE_WIDTH = { @@ -47,22 +49,10 @@ const getColorScale = (idSuffix = ''): (string | number)[] => { }).slice(0, -1) // Removes the last number value because it not needed anymore } -/** - * Maplibre expression to check whether a nowcast timestamp is older than the start of this day (compares the string values which is not ideal, but there doesn't seem to be a way to cast to dates via expressions). - */ -const IS_OUTDATED_NOWCAST = [ - '<=', - ['get', 'nowcast_timestamp_stamm'], - startOfYesterday().toISOString(), -] - -const PARTICIPATING_DISTRICTS = ['Mitte', 'Neukölln'] - export const TREES_LAYER: LayerSpecification = { id: TREES_LAYER_ID, type: 'circle', source: TREES_SOURCE_ID, - 'source-layer': TREES_SOURCE_LAYER_ID, maxzoom: 24, minzoom: 0, layout: { @@ -71,9 +61,13 @@ export const TREES_LAYER: LayerSpecification = { paint: { 'circle-color': [ 'case', - ['all', ['has', NOWCAST_AVERAGE_PROPERTY], ['!', IS_OUTDATED_NOWCAST]], + [ + 'all', + ['has', NOWCAST_AVERAGE_PROPERTY], + ['!=', ['get', NOWCAST_AVERAGE_PROPERTY], null], + ], ['step', ['get', NOWCAST_AVERAGE_PROPERTY], ...getColorScale()], - 'rgba(255,255,255,0)', + 'rgba(0, 0, 0, 0)', ], 'circle-stroke-width': [ 'case', @@ -88,22 +82,12 @@ export const TREES_LAYER: LayerSpecification = { ], 'circle-stroke-color': [ 'case', - ['all', ['has', NOWCAST_AVERAGE_PROPERTY], ['!', IS_OUTDATED_NOWCAST]], + ['has', NOWCAST_AVERAGE_PROPERTY], ['step', ['get', NOWCAST_AVERAGE_PROPERTY], ...getColorScale('-dark')], colors.gray[400], ], - 'circle-opacity': [ - 'case', - ['in', ['get', 'trees_bezirk'], ['literal', PARTICIPATING_DISTRICTS]], - 1, - 0.2, - ], - 'circle-stroke-opacity': [ - 'case', - ['in', ['get', 'trees_bezirk'], ['literal', PARTICIPATING_DISTRICTS]], - 1, - 0.4, - ], + 'circle-opacity': 1, + 'circle-stroke-opacity': 1, 'circle-radius': [ 'interpolate', ['exponential', 0.5], diff --git a/src/components/WaterSupplyLegend/index.tsx b/src/components/WaterSupplyLegend/index.tsx index 480766d9..c39ea4f1 100644 --- a/src/components/WaterSupplyLegend/index.tsx +++ b/src/components/WaterSupplyLegend/index.tsx @@ -108,6 +108,27 @@ export const WaterSupplyLegend: FC = ({ {children} + { + // 2024-07-11: This project is about to be archived. + // For archiving purposes, we render a hint for the user that the data is demo data. + } +
+
+ {t('legend.demoDataTitle')}{' '} + {t('legend.demoDataHint')} +
+
, bodyNode ) diff --git a/src/lib/hooks/useFeedbackData/index.tsx b/src/lib/hooks/useFeedbackData/index.tsx index 3c2f4327..19792b01 100644 --- a/src/lib/hooks/useFeedbackData/index.tsx +++ b/src/lib/hooks/useFeedbackData/index.tsx @@ -1,8 +1,6 @@ -import { reportIssue } from '@lib/requests/reportIssue' - +import { getIssueTypesData } from '@lib/requests/getIssueTypesData' import { useEffect, useState } from 'react' import useSWR from 'swr' -import { getIssueTypesData } from '@lib/requests/getIssueTypesData' const LOCAL_STORAGE_PREFIX = 'issue' @@ -14,14 +12,11 @@ export interface IssueTypeType { alreadyReported: boolean } -type UseFeedbackDataType = ( - treeId: string | undefined, - csrfToken: string -) => { +type UseFeedbackDataType = (treeId: string | undefined) => { issues: IssueTypeType[] | null isLoading: boolean error: string | null - reportIssue: (issueTypeId: number) => Promise + reportIssue: (issueTypeId: number) => void } const getIssueTypes = async (treeId: string): Promise => { @@ -83,7 +78,7 @@ const getIfAlreadyReported = ( return false } -export const useFeedbackData: UseFeedbackDataType = (treeId, csrfToken) => { +export const useFeedbackData: UseFeedbackDataType = (treeId) => { const { data, error: sdkError, @@ -110,11 +105,11 @@ export const useFeedbackData: UseFeedbackDataType = (treeId, csrfToken) => { })) || null, isLoading: data === null, error: issueError || sdkError?.message || null, - reportIssue: async (issueTypeId: number): Promise => { + reportIssue: (issueTypeId: number): void => { if (!treeId) return setIssueError(null) try { - await reportIssue({ issueTypeId, treeId, csrfToken }) + // await reportIssue({ issueTypeId, treeId, csrfToken }) window.localStorage.setItem( getLocalStorageKey(treeId, issueTypeId), new Date().toISOString() diff --git a/src/lib/hooks/useForecastData/index.tsx b/src/lib/hooks/useForecastData/index.tsx index dcebf258..987e04e0 100644 --- a/src/lib/hooks/useForecastData/index.tsx +++ b/src/lib/hooks/useForecastData/index.tsx @@ -11,13 +11,12 @@ interface useForecastDataReturnType { } export const useForecastData = ( - treeId: string | undefined, - csrfToken: string + treeId: string | undefined ): useForecastDataReturnType => { const params = [`Forecast - Tree ID - ${treeId || 'nodata'}`] const { data, error } = useSWR( params, - () => (treeId ? getForecastData(treeId, csrfToken) : undefined) + () => (treeId ? getForecastData(treeId) : undefined) ) return { diff --git a/src/lib/hooks/useGdkTreeId/index.ts b/src/lib/hooks/useGdkTreeId/index.ts index e3a3924a..59a9a9cd 100644 --- a/src/lib/hooks/useGdkTreeId/index.ts +++ b/src/lib/hooks/useGdkTreeId/index.ts @@ -1,5 +1,4 @@ -import { GdkTreeIdReturnType, getGdkTreeId } from '@lib/requests/getGdkTreeId' -import useSWR from 'swr' +import { GdkTreeIdReturnType } from '@lib/requests/getGdkTreeId' export interface useGdkTreeIdReturnType { isLoading: boolean @@ -10,15 +9,9 @@ export interface useGdkTreeIdReturnType { export const useGdkTreeId = ( treeId: string | undefined ): useGdkTreeIdReturnType => { - const params = [`GDK Tree ID - Tree ID - ${treeId || 'nodata'}`] - const { data, error } = useSWR< - useGdkTreeIdReturnType['data'] | undefined, - Error - >(params, () => (treeId ? getGdkTreeId(treeId) : undefined)) - return { - isLoading: !data && !error, - data: data || null, - error: error || null, + isLoading: false, + data: treeId || null, + error: null, } } diff --git a/src/lib/hooks/useNowcastData/index.ts b/src/lib/hooks/useNowcastData/index.ts index 8dd21061..c5a6e3bb 100644 --- a/src/lib/hooks/useNowcastData/index.ts +++ b/src/lib/hooks/useNowcastData/index.ts @@ -1,9 +1,6 @@ -import { getNowcastData, NowcastDataType } from '@lib/requests/getNowcastData' -import { - MappedNowcastRowsType, - mapRowsToDepths, -} from '@lib/utils/mapRowsToDepths' +import { MappedNowcastRowsType } from '@lib/utils/mapRowsToDepths' import useSWR from 'swr' +import { TreeGeoJsonFeature, TreesGeoJson } from '../useTreeData' interface useNowcastDataReturnType { isLoading: boolean @@ -11,19 +8,56 @@ interface useNowcastDataReturnType { error: Error | null } +/** + * Fetches the current nowcast data for a tree. + * 2024-07-11: This project is about to be archived. + * For archiving purposes, we make the project independent of the backend, database and vector tiles. + * The nowcast data is taken from the static trees.geojson file. + * @param treeId string + * @returns Promise + */ export const useNowcastData = ( - treeId: string | undefined, - csrfToken: string + treeId: string | undefined ): useNowcastDataReturnType => { + async function fetchData(): Promise { + const response = await fetch(`/trees.geojson`) + const trees = (await response.json()) as TreesGeoJson + const foundTree = trees.features.find( + (tree: TreeGeoJsonFeature) => tree.properties.trees_id === treeId + ) + if (!foundTree) { + return { + depth30Row: { value: undefined }, + depth60Row: { value: undefined }, + depth90Row: { value: undefined }, + depthAverageRow: { value: undefined }, + } as MappedNowcastRowsType + } + return { + depth30Row: { + value: foundTree.properties.nowcast_values_30cm, + }, + depth60Row: { + value: foundTree.properties.nowcast_values_60cm, + }, + depth90Row: { + value: foundTree.properties.nowcast_values_90cm, + }, + depthAverageRow: { + value: foundTree?.properties.nowcast_values_stamm, + }, + } as MappedNowcastRowsType + } + const params = [`Nowcast - Tree ID - ${treeId || 'nodata'}`] - const { data, error } = useSWR( + const { data, error } = useSWR( params, - () => (treeId ? getNowcastData(treeId, csrfToken) : undefined) + () => (treeId ? fetchData() : undefined) ) return { isLoading: !data && !error, - data: data && data.length > 0 ? mapRowsToDepths(data) : null, + data: data || null, error: error || null, } } diff --git a/src/lib/hooks/useShadingData/index.ts b/src/lib/hooks/useShadingData/index.ts index f821ff96..838db451 100644 --- a/src/lib/hooks/useShadingData/index.ts +++ b/src/lib/hooks/useShadingData/index.ts @@ -8,12 +8,11 @@ export interface useShadingDataReturnType { } export const useShadingData = ( - treeId: string | undefined, - csrfToken: string + treeId: string | undefined ): useShadingDataReturnType => { const params = [`Shading - Tree ID - ${treeId || 'nodata'}`] const { data, error } = useSWR(params, () => - treeId ? getShading(treeId, csrfToken) : undefined + treeId ? getShading(treeId) : undefined ) return { diff --git a/src/lib/hooks/useTreeData/index.tsx b/src/lib/hooks/useTreeData/index.tsx index 8dc93a45..be37e47a 100644 --- a/src/lib/hooks/useTreeData/index.tsx +++ b/src/lib/hooks/useTreeData/index.tsx @@ -1,4 +1,4 @@ -import { getTreeData, TreeDataType } from '@lib/requests/getTreeData' +import { TreeDataType } from '@lib/requests/getTreeData' import useSWR from 'swr' type UseTreeDataType = (treeid: string | undefined) => { @@ -7,15 +7,89 @@ type UseTreeDataType = (treeid: string | undefined) => { error: Error | null } +export interface TreeGeoJsonFeature { + type: string + geometry: { + type: string + coordinates: number[] + } + properties: { + trees_id: string + nowcast_values_stamm: number + nowcast_values_30cm: number + nowcast_values_60cm: number + nowcast_values_90cm: number + trees_lat: number + trees_lng: number + baumscheibe_m2: number + trees_stammumfg: number + shading_spring: number + shading_summer: number + shading_fall: number + shading_winter: number + rainfall_in_mm: number + } +} + +export interface TreesGeoJson { + type: string + features: TreeGeoJsonFeature[] +} + +/** + * 2024-07-11: This project is about to be archived. + * For archiving purposes, we make the project independent of the backend, database and vector tiles. + * The tree data here is taken from the static trees.geojson file. + * @param treeId + * @returns + */ export const useTreeData: UseTreeDataType = (treeId) => { - const { data, error } = useSWR( + async function fetchData(): Promise { + const response = await fetch(`/trees.geojson`) + + const trees = (await response.json()) as TreesGeoJson + + const foundTree = trees.features.find( + (tree: TreeGeoJsonFeature) => tree.properties.trees_id === treeId + ) + + return { + baumscheibe: foundTree?.properties.baumscheibe_m2, + id: foundTree?.properties.trees_id, + lat: foundTree?.properties.trees_lat, + lng: foundTree?.properties.trees_lng, + stammumfg: foundTree?.properties.trees_stammumfg, + street_tree: true, + art_bot: null, + art_dtsch: null, + baumhoehe: null, + bezirk: null, + created_at: null, + eigentuemer: null, + gattung: null, + gattung_deutsch: null, + geometry: null, + hausnr: null, + kennzeich: null, + kronedurch: null, + namenr: null, + pflanzjahr: null, + standalter: null, + standortnr: null, + strname: null, + updated_at: null, + zusatz: null, + } as TreeDataType + } + + const { data, error } = useSWR( `tree_data${treeId ? treeId : 'nodata'}`, - () => (treeId ? getTreeData(treeId) : undefined) + () => (treeId ? fetchData() : undefined) ) return { - data: data && data.length > 0 ? data[0] : null, - isLoading: !data && !error, + data: data as TreeDataType, + isLoading: false, error: error || null, } } diff --git a/src/lib/hooks/useWateringData/index.ts b/src/lib/hooks/useWateringData/index.ts index 775ed800..e5aae1e4 100644 --- a/src/lib/hooks/useWateringData/index.ts +++ b/src/lib/hooks/useWateringData/index.ts @@ -8,12 +8,11 @@ export interface useWateringDataReturnType { } export const useWateringData = ( - treeId: string | undefined, - csrfToken: string + treeId: string | undefined ): useWateringDataReturnType => { const params = [`Watering - Tree ID - ${treeId || 'nodata'}`] const { data, error } = useSWR(params, () => - treeId ? getWateringValue(treeId, csrfToken) : undefined + treeId ? getWateringValue(treeId) : undefined ) return { diff --git a/src/lib/requests/getForecastData.ts b/src/lib/requests/getForecastData.ts index c86760d6..b4364ee3 100644 --- a/src/lib/requests/getForecastData.ts +++ b/src/lib/requests/getForecastData.ts @@ -1,6 +1,3 @@ -import { getBaseUrl } from '@lib/utils/urlUtil' -import { startOfYesterday } from 'date-fns' - /** * According to the database schema all values except id are nullable. */ @@ -22,52 +19,63 @@ export type ForecastDataType = { model_id?: string } -const TABLE_NAME = 'forecast' -const TREE_ID_COLUMN_NAME = 'tree_id' -const TYPE_ID_COLUMN_NAME = 'type_id' -const TYPE_ID_FOR_AVERAGE = '4' - -const TIMESTAMP_COLUMN = 'timestamp' -const TODAY = startOfYesterday().toISOString() -const FORECAST_MAX_ROWS = 13 +// export const WATER_SUPPLY_STATUSES: WaterSupplyStatusType[] = [ +// { +// suctionTensionRange: [0, 33], +// label: 'Gut', +// id: 'good', +// }, +// { +// suctionTensionRange: [33, 81], +// label: 'Mäßig', +// id: 'medium', +// }, +// { +// suctionTensionRange: [81, 270], +// label: 'Kritisch', +// id: 'critical', +// }, +// ] /** * Fetches the forecast data for a tree (maximum 14 days). + * 2024-07-11: This project is about to be archived. + * For archiving purposes, we make the project independent of the backend, database and vector tiles. + * The forecast data generated here is random. * @param treeId string * @returns Promise */ -export const getForecastData = async ( - treeId: string, - csrfToken: string -): Promise => { - if (!treeId) return +export const getForecastData = (treeId: string): ForecastDataType[] => { + if (!treeId) return [] - const REQUEST_URL = `${getBaseUrl()}/api/ml-api-passthrough/${TABLE_NAME}` + const today = new Date() + const forecasts: ForecastDataType[] = [] - const REQUEST_PARAMS = new URLSearchParams({ - [TREE_ID_COLUMN_NAME]: `eq.${treeId}`, - [TYPE_ID_COLUMN_NAME]: `eq.${TYPE_ID_FOR_AVERAGE}`, - [TIMESTAMP_COLUMN]: `gte.${TODAY}`, - order: `${TIMESTAMP_COLUMN}`, - limit: `${FORECAST_MAX_ROWS}`, - offset: '1', - }) + for (let i = 0; i < 14; i++) { + const timestamp = new Date( + today.getTime() + i * 24 * 60 * 60 * 1000 + ).toISOString() - const response = await fetch(`${REQUEST_URL}?${REQUEST_PARAMS.toString()}`, { - method: 'POST', - headers: { - 'CSRF-Token': csrfToken, - 'Content-Type': 'application/json', - }, - }) + let randomValue = Math.random() * 270 + if (i > 0) { + const last = forecasts[i - 1] + const lastValue = last.value || 0 + if (lastValue <= 33) { + randomValue = Math.random() * 81 + } + } - if (!response.ok) { - const txt = await response.text() - console.error(txt) - throw new Error(txt) + const forecast: ForecastDataType = { + id: 0, + timestamp: timestamp, + tree_id: treeId, + type_id: 4, + value: randomValue, + created_at: new Date().toISOString(), + model_id: 'Random Forest (simple)', + } + forecasts.push(forecast) } - const { data } = (await response.json()) as { data: ForecastDataType[] } - - return data + return forecasts } diff --git a/src/lib/requests/getIssueTypesData.ts b/src/lib/requests/getIssueTypesData.ts index edb064a4..46bb649b 100644 --- a/src/lib/requests/getIssueTypesData.ts +++ b/src/lib/requests/getIssueTypesData.ts @@ -1,22 +1,10 @@ import { Database } from '@lib/types/database' -import { getBaseUrl } from '@lib/utils/urlUtil' type IssueTypesDataType = Database['public']['Tables']['issue_types']['Row'] -const TABLE_NAME = 'issue_types' export const getIssueTypesData = async (): Promise< IssueTypesDataType[] | undefined > => { - const REQUEST_URL = `${getBaseUrl()}/api/ml-api-passthrough/${TABLE_NAME}?order=id` - - const response = await fetch(REQUEST_URL, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }) - if (!response.ok) { - const txt = await response.text() - console.error(txt) - throw new Error(txt) - } - const { data } = (await response.json()) as { data: IssueTypesDataType[] } - return data + const response = await fetch(`/issue_types.json`) + const issueTypes = (await response.json()) as IssueTypesDataType[] + return issueTypes } diff --git a/src/lib/requests/getShading.ts b/src/lib/requests/getShading.ts index efe17bc8..bb696059 100644 --- a/src/lib/requests/getShading.ts +++ b/src/lib/requests/getShading.ts @@ -1,57 +1,33 @@ -import { Database } from '@lib/types/database' +import { TreeGeoJsonFeature, TreesGeoJson } from '@lib/hooks/useTreeData' import { getCurrentSeason } from '@lib/utils/getCurrentSeason' -import { getBaseUrl } from '@lib/utils/urlUtil' -const TABLE_NAME = 'shading' -const TREE_ID_COLUMN_NAME = 'tree_id' - -export type ShadingType = Database['public']['Tables']['shading']['Row'] +export type ShadingType = { + fall: number | null + spring: number | null + summer: number | null + winter: number | null +} /** * Fetches the current shading data for a tree. + * 2024-07-11: This project is about to be archived. + * For archiving purposes, we make the project independent of the backend, database and vector tiles. + * The shading data generated here is taken from the static trees.geojson file. * @param treeId string * @param csrfToken string * @returns Promise */ export const getShading = async ( - treeId: string, - csrfToken: string + treeId: string ): Promise => { - if (!treeId) return - - const REQUEST_URL = `${getBaseUrl()}/api/ml-api-passthrough/${TABLE_NAME}` - - const REQUEST_PARAMS = new URLSearchParams({ - [TREE_ID_COLUMN_NAME]: `eq.${treeId}`, - limit: '1', - offset: '0', - }) - - const response = await fetch(`${REQUEST_URL}?${REQUEST_PARAMS.toString()}`, { - method: 'POST', - headers: { - 'CSRF-Token': csrfToken, - 'Content-Type': 'application/json', - }, - }) - - if (!response.ok) { - const txt = await response.text() - console.error(txt) - throw new Error(txt) - } - - const { - // We destructure a lot because PostgREST always returns an array: - data: [shadingData], - } = (await response.json()) as { data: ShadingType[] } - const currentSeason = getCurrentSeason() - - if (!currentSeason) { - console.error('Unable to find shading data for current season') - return + const response = await fetch(`/trees.geojson`) + const trees = (await response.json()) as TreesGeoJson + const foundTree = trees.features.find( + (tree: TreeGeoJsonFeature) => tree.properties.trees_id === treeId + ) + if (foundTree && currentSeason !== undefined) { + return foundTree.properties[`shading_${currentSeason}`] } - - return shadingData && (shadingData[currentSeason] as number) + return undefined } diff --git a/src/lib/requests/getTreeRainAmount.ts b/src/lib/requests/getTreeRainAmount.ts index 73a38972..6b080294 100644 --- a/src/lib/requests/getTreeRainAmount.ts +++ b/src/lib/requests/getTreeRainAmount.ts @@ -1,15 +1,12 @@ -import { getBaseUrl } from '@lib/utils/urlUtil' - -type RawTreeRainAmountType = { - sum_rainfall_in_mm: number -} +import { TreeGeoJsonFeature, TreesGeoJson } from '@lib/hooks/useTreeData' export type TreeRainAmountType = number -const TREE_ID_COLUMN_NAME = 'tree_id' - /** * Fetches the rain data for a tree (in mm for the current day). + * 2024-07-11: This project is about to be archived. + * For archiving purposes, we make the project independent of the backend, database and vector tiles. + * The rain data is taken from the static trees.geojson file. * @param treeId string * @returns Promise */ @@ -17,22 +14,13 @@ export const getTreeRainAmount = async ( treeId: string ): Promise => { if (!treeId) return - - const REQUEST_URL = `${getBaseUrl()}/api/trees/rainfall` - - const REQUEST_PARAMS = new URLSearchParams({ - [TREE_ID_COLUMN_NAME]: `${treeId}`, - }) - - const response = await fetch(`${REQUEST_URL}?${REQUEST_PARAMS.toString()}`) - - if (!response.ok) { - const txt = await response.text() - console.error(txt) - throw new Error(txt) + const response = await fetch(`/trees.geojson`) + const trees = (await response.json()) as TreesGeoJson + const foundTree = trees.features.find( + (tree: TreeGeoJsonFeature) => tree.properties.trees_id === treeId + ) + if (foundTree) { + return foundTree.properties.rainfall_in_mm } - - const { data } = (await response.json()) as { data: RawTreeRainAmountType } - - return data.sum_rainfall_in_mm + return undefined } diff --git a/src/lib/requests/getWateringValue/index.ts b/src/lib/requests/getWateringValue/index.ts index c026f862..08d7fec4 100644 --- a/src/lib/requests/getWateringValue/index.ts +++ b/src/lib/requests/getWateringValue/index.ts @@ -1,60 +1,17 @@ -import { getBaseUrl } from '@lib/utils/urlUtil' - -const VIEW_NAME = 'watering' -const TREE_ID_COLUMN_NAME = 'tree_id' - -// Note that we have to define this type ourselves because the type generator -// is currently not able to type database views. -interface WateringType { - tree_id: string - sum: number - date: Date -} - /** * Fetches the watering value in liters for a tree. * Currently this fetches the waterings of the past 60 days. + * 2024-07-11: This project is about to be archived. + * For archiving purposes, we make the project independent of the backend, database and vector tiles. + * For some trees, we return a random value as mocked demo data. * @param treeId string * @param csrfToken string * @returns number | undefined */ -export const getWateringValue = async ( - treeId: string, - csrfToken: string -): Promise => { +export const getWateringValue = (treeId: string): number | undefined => { if (!treeId) return - const REQUEST_URL = `${getBaseUrl()}/api/ml-api-passthrough/${VIEW_NAME}` - - const REQUEST_PARAMS = new URLSearchParams({ - [TREE_ID_COLUMN_NAME]: `eq.${treeId}`, - }) - - const response = await fetch(`${REQUEST_URL}?${REQUEST_PARAMS.toString()}`, { - method: 'POST', - headers: { - 'CSRF-Token': csrfToken, - 'Content-Type': 'application/json', - }, - }) - - if (!response.ok) { - const txt = await response.text() - console.error(txt) - throw new Error(txt) - } - - const { data: waterings } = (await response.json()) as { - data: WateringType[] + if (Math.random() < 0.5) { + return Math.round(50 + Math.random() * 500) } - - if (!waterings || waterings.length === 0) return - - const wateringValue = waterings - .map((watering) => watering.sum) - .reduce((accumulator, currentValue) => { - return accumulator + currentValue - }) - - return wateringValue } diff --git a/src/lib/utils/mapRowsToDepths/mapRowsToDepths.test.ts b/src/lib/utils/mapRowsToDepths/mapRowsToDepths.test.ts deleted file mode 100644 index 5b47f103..00000000 --- a/src/lib/utils/mapRowsToDepths/mapRowsToDepths.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { NowcastDataType } from '@lib/requests/getNowcastData' -import { mapRowsToDepths } from '.' - -describe('mapRowsToDepths', () => { - test('maps one row to one depth', () => { - const ROWS_WITH_COMPLETE_DATA: NowcastDataType[] = [ - { - id: 35223, - type_id: 1, - }, - { - id: 89262, - type_id: 2, - }, - { - id: 41729, - type_id: 3, - }, - { - id: 15292, - type_id: 4, - }, - ] - const mappedRows = mapRowsToDepths(ROWS_WITH_COMPLETE_DATA) - expect(mappedRows.depth30Row).toHaveProperty('type_id', 1) - expect(mappedRows.depth60Row).toHaveProperty('type_id', 2) - expect(mappedRows.depth90Row).toHaveProperty('type_id', 3) - expect(mappedRows.depthAverageRow).toHaveProperty('type_id', 4) - }) - test('maps only the rows that match', () => { - const ROWS_WITH_COMPLETE_DATA: NowcastDataType[] = [ - { - id: 35223, - type_id: 1, - }, - { - id: 15292, - type_id: 4, - }, - ] - const mappedRows = mapRowsToDepths(ROWS_WITH_COMPLETE_DATA) - expect(mappedRows.depth30Row).toHaveProperty('type_id', 1) - expect(mappedRows.depth60Row).toBeUndefined() - expect(mappedRows.depth90Row).toBeUndefined() - expect(mappedRows.depthAverageRow).toHaveProperty('type_id', 4) - }) - test('uses the latest timestamp if there are duplicates', () => { - const EARLIEST_TIMESTAMP = '2022-08-21T11:00:00.000Z' - const LATEST_TIMESTAMP = '2022-08-21T12:00:00.000Z' - const ROWS_WITH_DUPLICATE_TYPES: NowcastDataType[] = [ - { - id: 35223, - type_id: 1, - }, - { - id: 89262, - type_id: 2, - timestamp: LATEST_TIMESTAMP, - }, - { - id: 41729, - type_id: 2, - timestamp: EARLIEST_TIMESTAMP, - }, - { - id: 15292, - type_id: 2, - }, - ] - const mappedRows = mapRowsToDepths(ROWS_WITH_DUPLICATE_TYPES) - expect(mappedRows.depth30Row).toHaveProperty('type_id', 1) - expect(mappedRows.depth60Row).toHaveProperty('type_id', 2) - expect(mappedRows.depth60Row).toHaveProperty('timestamp', LATEST_TIMESTAMP) - expect(mappedRows.depth90Row).toBeUndefined() - expect(mappedRows.depthAverageRow).toBeUndefined() - }) -}) diff --git a/src/lib/utils/mapRowsToDepths/mapRowsToDepths.ts b/src/lib/utils/mapRowsToDepths/mapRowsToDepths.ts index 7fc9c695..8506befd 100644 --- a/src/lib/utils/mapRowsToDepths/mapRowsToDepths.ts +++ b/src/lib/utils/mapRowsToDepths/mapRowsToDepths.ts @@ -1,33 +1,14 @@ -import { NowcastDataType } from '@lib/requests/getNowcastData' - export interface MappedNowcastRowsType { - depth30Row: NowcastDataType | undefined - depth60Row: NowcastDataType | undefined - depth90Row: NowcastDataType | undefined - depthAverageRow: NowcastDataType | undefined -} - -/** - * Maps raw nowcast rows to named fields according to their type (type_id 1 -> -30cm, type_id 2 -> 60cm, type_id 3 -> 90cm, type_id 4 -> average) - * @param nowcastRows NowcastDataType[] - * @returns MappedNowcastRowsType - */ -export const mapRowsToDepths = ( - nowcastRows: NowcastDataType[] -): MappedNowcastRowsType => { - const descendingRows = nowcastRows.sort((a, b) => { - if ( - typeof a.timestamp === 'undefined' || - typeof b.timestamp === 'undefined' - ) { - return 0 - } - return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() - }) - return { - depth30Row: descendingRows.find((row) => row.type_id === 1), - depth60Row: descendingRows.find((row) => row.type_id === 2), - depth90Row: descendingRows.find((row) => row.type_id === 3), - depthAverageRow: descendingRows.find((row) => row.type_id === 4), + depth30Row: { + value: number | undefined + } + depth60Row: { + value: number | undefined + } + depth90Row: { + value: number | undefined + } + depthAverageRow: { + value: number | undefined } }