Skip to content

Commit

Permalink
feat: use snapshotted demo data, make independent of backend (#124)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Jaszkowic authored Jul 17, 2024
1 parent 4f5a049 commit e1b57ad
Show file tree
Hide file tree
Showing 24 changed files with 351 additions and 386 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
"contentsLink": "Inhalte"
},
"legend": {
"demoDataTitle": "DEMO:",
"demoDataHint": "Diese Karte zeigt exemplarische Daten.",
"map": {
"title": "Wasserversorgung",
"levels": {
Expand Down Expand Up @@ -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:"
}
Expand Down
14 changes: 7 additions & 7 deletions pages/trees/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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')
Expand Down Expand Up @@ -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'
)}
>
<div
Expand Down Expand Up @@ -297,7 +297,7 @@ const TreePage: TreePageWithLayout = ({ treeId, csrfToken }) => {
>
<Button
primary
href={`https://giessdenkiez.de/tree/${gdkTreeId}`}
href={`https://giessdenkiez.de/map?treeId=${gdkTreeId}`}
>
<GdkIcon
color1={colors.white}
Expand Down
14 changes: 14 additions & 0 deletions public/issue_types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"id": 1,
"title": "Missnutzung der Baumscheibe",
"description": "Die Baumscheibe (nicht versiegelte Fläche) am Standort eines Baums wir oft durch falsch parkende Autos, illegalen Müll-Entsorgungen, wie bspw.: alte Waschmaschinen oder Bauschutt, missbraucht. Melde uns bitte, wenn die Baumscheibe seit längerem zugestellt ist.",
"image_url": "/images/issues/missnutzung-baumscheibe.jpg"
},
{
"id": 2,
"title": "Baumschäden",
"description": "Baumschäden, wie bspw. abgeknickte Äste, stellen eine Gefahr für den Straßenraum dar und können die Vitalität eines Baumes nachhaltig einschränken. Melde uns bitte, wenn dieser Baum einen Schaden hat.",
"image_url": "/images/issues/baumschaeden.jpg"
}
]
1 change: 1 addition & 0 deletions public/trees.geojson

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/components/FeedbackRequestsList/FeedbackConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ export const FeedbackConfirmation: FC = () => {
'mt-4'
)}
>
<h4 className="font-bold">{t('feedback.confirmationTitle')}</h4>
<h4 className="font-bold">{t('feedback.demoTitle')}</h4>
<div className="row-span-2 flex items-center">
<CheckIcon color1={colors.scale['good']} />
</div>
<p className="font-serif text-gray-500">
{t('feedback.confirmationDescription')}
{t('feedback.demoDescription')}
</p>
</div>
)
Expand Down
4 changes: 1 addition & 3 deletions src/components/FeedbackRequestsList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ interface FeedbackRequestsListPropType {

export const FeedbackRequestsList: FC<FeedbackRequestsListPropType> = ({
treeData,
csrfToken,
}) => {
const { t } = useTranslation('common')
const { issues, reportIssue, isLoading, error } = useFeedbackData(
treeData?.id,
csrfToken
treeData?.id
)
const [openedIssueModal, setOpenedIssueModal] =
useState<IssueTypeType | null>(null)
Expand Down
31 changes: 12 additions & 19 deletions src/components/TreesMap/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -247,7 +246,6 @@ export const TreesMap: FC<MapProps> = ({
map.current.setFeatureState(
{
source: TREES_SOURCE_ID,
sourceLayer: TREES_SOURCE_LAYER_ID,
id: currentSelectedTreeId,
},
{ selected: true }
Expand All @@ -263,7 +261,6 @@ export const TreesMap: FC<MapProps> = ({
map.current.setFeatureState(
{
source: TREES_SOURCE_ID,
sourceLayer: TREES_SOURCE_LAYER_ID,
id: hoveredTreeId,
},
{ hover: false }
Expand All @@ -276,7 +273,6 @@ export const TreesMap: FC<MapProps> = ({
map.current.setFeatureState(
{
source: TREES_SOURCE_ID,
sourceLayer: TREES_SOURCE_LAYER_ID,
id: e.features[0].id,
},
{ hover: true }
Expand All @@ -291,7 +287,6 @@ export const TreesMap: FC<MapProps> = ({
map.current.setFeatureState(
{
source: TREES_SOURCE_ID,
sourceLayer: TREES_SOURCE_LAYER_ID,
id: hoveredTreeId,
},
{ hover: false }
Expand Down Expand Up @@ -345,7 +340,6 @@ export const TreesMap: FC<MapProps> = ({
map.current.setFeatureState(
{
source: TREES_SOURCE_ID,
sourceLayer: TREES_SOURCE_LAYER_ID,
id: treeIdToSelect,
},
{ selected: true }
Expand All @@ -360,7 +354,6 @@ export const TreesMap: FC<MapProps> = ({
map.current.setFeatureState(
{
source: TREES_SOURCE_ID,
sourceLayer: TREES_SOURCE_LAYER_ID,
id: currentSelectedTreeId,
},
{ selected: false }
Expand Down
48 changes: 16 additions & 32 deletions src/components/TreesMap/treesLayer.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 = {
Expand All @@ -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: {
Expand All @@ -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',
Expand All @@ -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],
Expand Down
21 changes: 21 additions & 0 deletions src/components/WaterSupplyLegend/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,27 @@ export const WaterSupplyLegend: FC<WaterSupplyLegendType> = ({
{children}
</div>
</div>
{
// 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.
}
<div className="w-full mt-2">
<div
className={classNames(
className,
'text-xs',
'ml-2 lg:ml-4 pointer-events-auto',
'w-[176px] min-w-[80px]',
'py-2 px-3',
'bg-white',
'rounded border border-gray-300',
'shadow-md'
)}
>
<span className="font-bold">{t('legend.demoDataTitle')}</span>{' '}
{t('legend.demoDataHint')}
</div>
</div>
</div>,
bodyNode
)
Expand Down
Loading

0 comments on commit e1b57ad

Please sign in to comment.