4.3. Einfügen Karte#
In dieser Übung erstellen wir eine interaktive Karte, auf der alle Bäume (stand 2025) dargestellt sind. Dabei können verschiedene Filter gesetzt werden, z. B. nach Bezirk, Jahr, Jahreszeit oder Baumart.
Ziel ist es, mit Hilfe der Leaflet-Bibliothek eine Karte zu generieren, die:
Bewässerte Bäume als farbige Kreise darstellt
Informationen zur Bewässerung beim Anklicken anzeigt
Eine Legende zur Farbskala der Wassermenge enthält
Funktionstüchtige Pumpen ab einem bestimmten Zoom-Level anzeigen
4.3.1. Benutzeroberfläche (UI)#
Die Benutzeroberfläche besteht aus zwei Teilen:
einer Seitenleiste (
sidebarMenu
) mit der Navigationeinem Inhaltsbereich (
tabItem
) mit:Karte mit Baumbestands anzeige
Dropdowns zur Auswahl des Zeitraums, des Bezirks, des lor und der Baumgattung
Navigation in der Seitenleiste
dashboardSidebar(
sidebarMenu(
menuItem("Karte", tabName = "map", icon = icon("map"))
)
)
sidebarMenu(...)
ist die Hauptnavigation des Dashboards.menuItem(...)
erzeugt einen Menüpunkt:"Karte"
ist der angezeigte Name.tabName = "map"
verbindet den Menüpunkt mit dem Tab.icon("map")
zeigt ein kleines Karten Symbol an.
Merke:
Mit menuItem(...)
wird ein weiterer Navigationspunkt eingebunden. “map” als tabName verknüpft ihn mit dem Kartentab.
4.3.2. UI: Karte mit Filter-Boxen#
tabItem(tabName = "map",
fluidRow(
box(title = "Filter", status = "primary", solidHeader = TRUE, width = 12,
column(width = 6, selectInput("map_bezirk", "Bezirk auswählen:", choices = c("Alle", unique(df_merged_clean$bezirk)), selected = "Alle", multiple = TRUE)),
column(width = 6, selectInput("map_lor", "Lebensweltlich orientierte Räume auswählen:", choices = c("Alle", unique(df_merged_sum_distanz_umkreis_pump_ok_lor$bzr_name)), selected = "Alle", multiple = TRUE)),
column(width = 6, selectInput("map_year", "Jahr auswählen:", choices = c("2020-2024", unique(year(df_merged_clean$timestamp))), selected = "2020-2024", multiple = TRUE)),
column(width = 6, selectInput("map_saison", "Saison auswählen:", choices = c("Alle", "Winter", "Frühling", "Sommer", "Herbst"), selected = "Alle", multiple = TRUE)),
column(width = 6, selectInput("map_baumgattung", "Baumgattung auswählen:", choices = c("Alle", unique(df_merged_clean$gattung_deutsch)), selected = "Alle", multiple = TRUE))
)
),
leafletOutput("map", height = "800px")
)
box(...)
ist ein Container mit:title
(Überschrift)status = "primary"
(Farbe)solidHeader = TRUE
(fester Rand)width = 12
(volle Breite – 12 ist die maximale Spaltenanzahl)
fluidRow(...)
sorgt für eine horizontale Anordnung (z. B. nebeneinander statt untereinander).multiple = TRUE
bedeutet, dass man mehrere Optionen gleichzeitig auswählen kann.
Merke:
fluidRow()
ordnet Inhalte nebeneinander. box(...)
gruppiert UI-Elemente visuell und funktional.
4.3.3. Zoom Javascript#
dashboardBody(
tags$script(HTML("
$(document).ready(function() {
var map = $('#map').find('div.leaflet-container')[0];
if (map) {
var leafletMap = $(map).data('leaflet-map');
leafletMap.on('zoomend', function() {
Shiny.setInputValue('map_zoom', leafletMap.getZoom());
});
}
});
")),
Diese Funktion überwacht die Zoomstufe der Karte. Wenn die Nutzer*innen herein- oder herauszoomen, wird die aktuelle Zoomstufe (map_zoom
) an die Shiny-App zurückgemeldet, sodass darauf reagiert werden kann.
4.3.4. Daten filtern im Server: filtered_data_map#
filtered_data_map <- reactive({
data <- df_merged_sum_distanz_umkreis_pump_ok_lor
# 1. Nach Bezirk
if (!is.null(input$map_bezirk) && !("Alle" %in% input$map_bezirk)) {
data <- data %>% filter(bezirk %in% input$map_bezirk)
}
# 2. Nach LOR (Lebensweltlich orientierte Räume)
if (!is.null(input$map_lor) && !("Alle" %in% input$map_lor)) {
data <- data %>% filter(bzr_name %in% input$map_lor)
}
# 3. Nach Baumgattung
if (!is.null(input$map_baumgattung) && !("Alle" %in% input$map_baumgattung)) {
data <- data %>% filter(gattung_deutsch %in% input$map_baumgattung)
}
# 4. Nach Jahr
if (!is.null(input$map_year) && !("2020-2024" %in% input$map_year)) {
data$timestamp <- as.Date(data$timestamp)
data <- data %>% filter(lubridate::year(timestamp) %in% as.numeric(input$map_year))
}
# 5. Nach Saison
if (!is.null(input$map_saison) && !("Alle" %in% input$map_saison)) {
data$monat <- lubridate::month(as.Date(data$timestamp))
data$saison <- case_when(
data$monat %in% c(12, 1, 2) ~ "Winter",
data$monat %in% c(3, 4, 5) ~ "Frühling",
data$monat %in% c(6, 7, 8) ~ "Sommer",
data$monat %in% c(9, 10, 11) ~ "Herbst",
TRUE ~ "Unbekannt"
)
data <- data %>% filter(saison %in% input$map_saison)
}
# 6. Nur mit Koordinaten
data <- data %>% filter(!is.na(lat), !is.na(lng))
data
})
Wichtige Begriffe erklärt:
reactive(...)
: erzeugt eine reaktive Funktion, die automatisch neu berechnet wird, wenn sich Eingaben ändern.NA-Werte (
NA
) Steht für “Not Available” und bedeutet, dass ein Wert in den Daten fehlt oder unbekannt ist. Zum Beispiel, wenn für einen Baum die Koordinaten nicht bekannt sind.is.na(x)
prüft, ob x ein fehlender Wert ist.!is.na(x)
prüft, ob x nicht fehlt.filter(!is.na(lat), !is.na(lng))
hier werden nur Datensätze berücksichtigt, bei denen sowohl lat (Breitengrad) als auch lng (Längengrad) vorhanden sind.
filter(...)
: Filtert die Daten so, dass nur diejenigen Zeilen erhalten bleiben, die bestimmte Bedingungen erfüllen.%>%
(Pipe-Operator) Leitet das Ergebnis von links an die Funktion rechts weiter. Er sorgt für eine lesbare Verkettung von Operationen.
Operatoren
%in%
: prüft, ob ein Wert in einer Liste enthalten ist. Zum Beispiel:bezirk %in% input$map_bezirk
<-
: weist einer Variable einen Wert zu (z. B.x <- 3
).|
= ODER,&
= UND
Merke:
reactive()
ist wie ein intelligenter Beobachter: Er reagiert automatisch auf Eingaben und aktualisiert die Daten.
if- und else-Anweisungen
if (Bedingung) {
# wird ausgeführt, wenn die Bedingung wahr ist
} else {
# wird ausgeführt, wenn die Bedingung falsch ist
}
Merke:
Die Filter arbeiten unabhängig voneinander – so können beliebige Kombinationen gewählt werden.
Beispiel:
Ein Filter „Bezirk: Friedrichshain-Kreuzberg“ + „Baumgattung: Ahorn“ ergibt nur Ahornbäume im gewählten Bezirk.
4.3.5. Pumpen Icon hinzufügen#
icon_pumpe <- makeIcon(
iconUrl = "icons/water-pump-icon-14.jpg",
iconWidth = 15,
iconHeight = 15
)
Wichtige Begriffe erklärt:
makeIcon
: Erstellt das Icon was man später nutzen kanniconUrl
: Gibt dem Pfad zum Pumpen Bild angibticonWidth
: Gibt die Breite des Icons aniconHeight
: Gibt die Höhe des Icons an
4.3.6. 6Karte zeichnen mit Leaflet#
output$map <- renderLeaflet({
data <- filtered_data_map()
# Farbpalette für Marker
color_palette <- colorNumeric(
palette = colorRampPalette(c("#FFA500", "#0000FF"))(100),
domain = c(0, 2500),
na.color = "#CCCCCC"
)
# Marker hinzufügen
leaflet(data = data) %>%
addTiles() %>%
addCircleMarkers(
lng = ~lng,
lat = ~lat,
radius = 4,
stroke = FALSE,
fillOpacity = 0.7,
color = ~color_palette(pmin(gesamt_bewaesserung, 2500)),
popup = ~paste0(
"<strong>Baumart: </strong>", art_dtsch, "<br>",
"<strong>Gattung: </strong>", gattung_deutsch, "<br>",
"<strong>Standort: </strong>", strname, " ", hausnr, "<br>",
"<strong>Gesamtbewässerung: </strong>", round(as.numeric(gesamt_bewaesserung), 1), " Liter", "<br>",
"Ø <strong>Bewässerungsintervall: </strong>",
ifelse(is.infinite(durchschnitts_intervall), "Keine Daten",
paste(round(durchschnitts_intervall, 1), " Tage")), "<br><br>"
)
) %>%
# Pumpenmaker
addMarkers(
data = pumpen_mit_bezirk,
icon = icon_pumpe,
group = "Pumpen"
) %>%
addLayersControl(
overlayGroups = c("Pumpen"),
options = layersControlOptions(collapsed = FALSE)
) %>%
# Legende hinzufügen
addLegend(
position = "bottomright",
pal = color_palette,
values = c(0, 2500),
title = "Gesamtbewässerung (Liter)",
labFormat = labelFormat(suffix = " L", digits = 0),
opacity = 1
) %>%
hideGroup("Pumpen") # Anfangs ausblenden
})
# Observer für Zoomlevel-Änderungen
observe({
req(input$map_zoom) # Sicherstellen, dass Zoomlevel verfügbar ist
zoom_level <- input$map_zoom
if (!is.null(zoom_level)) {
leafletProxy("map") %>%
clearGroup("Pumpen") %>%
{
if (zoom_level >= 14) { # Nur anzeigen ab Zoomlevel 14
addMarkers(., data = pumpen_mit_bezirk, icon = icon_pumpe, group = "Pumpen")
} else {
.
}
}
}
})
leaflet(...)
: Erstellt eine neue Leaflet-Karte mit den gefilterten Daten.filtered_data_map()
enthält die gefilterten Baumdaten, die zuvor im reactive()-Block erstellt wurden.addTiles()
: Fügt die Hintergrundkarte hinzu (OpenStreetMap).addCircleMarkers(...)
: Zeichnet Punkte für jeden Baum:Zeichnet Kreise (Marker) für jeden Baum:
lng, lat: Die Koordinaten (Längengrad, Breitengrad)
radius = 4: Größe des Kreises
fillOpacity = 0.7: Durchsichtigkeit des Kreises
color = …: Farbe hängt von der Bewässerungsmenge ab. Werte über 2500 werden abgeschnitten (pmin(…))
popup = …: Beim Anklicken erscheint ein Infofenster mit:
Baumart & Gattung
Standort (Straße + Hausnummer)
Gesamtbewässerung
Durchschnittliches Intervall in Tagen
addMarkers
: Fügt Pumpen-Standorte als klassische Marker hinzu. Diese Marker gehören zur Gruppe “Pumpen”, damit man sie ein- und ausblenden kann.Mit
colorNumeric
wird eine lineare Farblegende erzeugt. Je höher der Bewässerungswert, desto dunkler der Punkt.addLayersControl
:Fügt eine Kontrollleiste auf der Karte hinzu.
Nutzer*innen können die Gruppe “Pumpen” manuell ein- oder ausblenden.
collapsed = FALSE: Die Leiste ist standardmäßig ausgeklappt.
Mit
addLegend(...)
wird eine Farblegende eingeblendet. Der Nutzer sieht, welche Farben welchen Bewässerungswerten entsprechen.hideGroup(...)
:Diese Zeile sorgt dafür, dass die Pumpen beim ersten Laden nicht sofort angezeigt werden.
Nutzer*innen können sie über die Ebenensteuerung bei Bedarf aktivieren.
observe({...})
: Beobachtet laufend eine Eingabe – hier die Zoomstufe der Karte.input$map_zoom
: Die aktuelle Zoomstufe der Karte.req(...)
: Stellt sicher, dass ein gültiger Wert vorliegt.leafletProxy("map")
: Greift auf eine bestehende Karte zu, ohne sie neu zu laden.clearGroup("Pumpen")
: Löscht alle aktuellen Pumpen-Marker von der Karte.
Dann:
Wenn der Zoomwert ≥ 14 ist, werden die Pumpenmarker neu hinzugefügt.
Sonst bleibt die Karte ohne Pumpen.
Warum ist das sinnvoll? Pumpenmarker sollen nur dann angezeigt werden, wenn man weit genug hineingezoomt hat. So bleibt die Karte bei niedriger Zoomstufe übersichtlich und schneller.
Merke:
Mit leafletProxy()
kann man gezielt Inhalte einer bestehenden Karte ändern – z. B. Marker hinzufügen oder löschen – ohne dass die ganze Karte neu aufgebaut werden muss.
Gesamter Code
# UI-Definition
ui <- dashboardPage(
dashboardHeader(title = "Gieß den Kiez Dashboard"),
dashboardSidebar(
sidebarMenu(
menuItem("Karte", tabName = "map", icon = icon("map")),
)
),
dashboardBody(
tags$script(HTML("
$(document).ready(function() {
var map = $('#map').find('div.leaflet-container')[0];
if (map) {
var leafletMap = $(map).data('leaflet-map');
leafletMap.on('zoomend', function() {
Shiny.setInputValue('map_zoom', leafletMap.getZoom());
});
}
});
")),
tabItems(
tabItem(tabName = "map",
fluidRow(
box(title = "Filter", status = "primary", solidHeader = TRUE, width = 12,
column(width = 6,
selectInput("map_bezirk", "Bezirk auswählen:", choices = c("Alle", unique(df_merged_clean$bezirk)), selected = "Alle", multiple = TRUE),
),
column(width = 6,
selectInput("map_lor", "Lebensweltlich orientierte Räume auswählen:", choices = c("Alle", unique(df_merged_sum_distanz_umkreis_pump_ok_lor$bzr_name)), selected = "Alle", multiple = TRUE)
),
column(width = 6,
selectInput("map_year", "Jahr auswählen:", choices = c("2020-2024",unique(year(df_merged_clean$timestamp))), selected = "2020-2024", multiple = TRUE),
),
column(width = 6,
selectInput("map_saison", "Saison auswählen:", choices = c("Alle", "Winter", "Frühling", "Sommer", "Herbst"), selected = "Alle", multiple = TRUE),
),
column(width = 6,
selectInput("map_baumgattung", "Baumgattung auswählen:", choices = c("Alle", unique(df_merged_clean$gattung_deutsch)), selected = "Alle", multiple = TRUE)
),
)
),
leafletOutput("map", height = "800px")
),
)
# Server-Logik
server <- function(input, output, session) {
convert_units <- function(liters) {
if (liters >= 1e6) {
return(list(value = round(liters / 1e6, 2), unit = "ML"))
} else if (liters >= 1e3) {
return(list(value = round(liters / 1e3, 2), unit = "m³"))
} else {
return(list(value = round(liters, 2), unit = "L"))
}
}
# Funktion zum Umrechnen von Vektoren
convert_unit_vector <- function(liters_vector) {
sapply(liters_vector, function(liters) {
conversion_result <- convert_units(liters)
return(list(value = conversion_result$value, unit = conversion_result$unit))
})
}
full_unit <- function(unit) {
if(length(unit) == 1) {
switch(unit,
"ML" = "Mega Liter",
"L" = "Liter",
"m³" = "Kubikmeter",
"kL" = "Kilo Liter",
unit) # Default
} else {
return("Unknown unit")
}
}
filteredData <- reactive({
req(input$stats_baumvt_year)
df <- df_merged %>%
mutate(year = lubridate::year(timestamp))
# Basisfilter nach Auswahl
df_filtered <- df %>%
filter(
("Baumbestand Stand 2025" %in% input$start_year &
(is.na(timestamp) | year %in% 2020:2024)) |
("2020-2024" %in% input$start_year &
!is.na(timestamp) & year %in% 2020:2024) |
(any(!input$start_year %in% c("2020-2024", "Baumbestand Stand 2025")) &
year %in% as.numeric(input$start_year))
)
# Wenn NUR "2020-2024" ausgewählt ist, dann NA-Drop forcieren
if (all(input$start_year == "2020-2024")) {
df_filtered <- df_filtered %>% filter(!is.na(timestamp))
}
if (!is.null(input$bezirk) && input$bezirk != "Alle") {
df_filtered <- df_filtered %>% filter(bezirk %in% input$bezirk)
}
df_filtered
})
output$total_trees <- renderValueBox({
valueBox(
formatC(n_distinct(df_merged$gisid), format = "d", big.mark = "."),
"Gesamtzahl der Bäume",
icon = icon("tree"),
color = "green"
)
})
output$total_tree_watered <- renderValueBox({
valueBox(
formatC(n_distinct(filteredData()$gisid), format = "d", big.mark = "."),
"Gesamtzahl der gegossenen Bäume",
icon = icon("tree"),
color = "green"
)
})
# Dynamische Auswahl: welche Box zeigen?
output$dynamic_tree_box <- renderUI({
if ("Baumbestand Stand 2025" %in% input$start_year) {
valueBoxOutput("total_trees")
} else {
valueBoxOutput("total_tree_watered")
}
})
output$total_water <- renderValueBox({
# Umrechnung des Werts und Ermittlung der Einheit
conversion_result <- convert_units(sum(filteredData()$bewaesserungsmenge_in_liter, na.rm = TRUE))
# Der umgerechnete Wert und die Einheit
converted_value <- conversion_result$value
unit <- conversion_result$unit
valueBox(
paste(format(converted_value, decimal.mark = ",", big.mark = "."), unit),
paste("Gesamtbewässerung (", full_unit(unit), ")", sep=""),
icon = icon("tint"),
color = "blue"
)
})
output$avg_water <- renderValueBox({
valueBox(
formatC(mean(filteredData()$bewaesserungsmenge_in_liter, na.rm = TRUE), digits = 2),
"Durchschnittliche Bewässerung pro gegossenen Baum (Liter)",
icon = icon("chart-line"),
color = "aqua"
)
})
Server Code:
filtered_data_map <- reactive({
# data <- df_merged_sum_pump_ok
data <- df_merged_sum_distanz_umkreis_pump_ok_lor
# Bezirk
if (!is.null(input$map_bezirk) && !("Alle" %in% input$map_bezirk)) {
data <- data %>% filter(bezirk %in% input$map_bezirk)
}
if (!is.null(input$map_lor) && !("Alle" %in% input$map_lor)) {
data <- data %>% filter(bzr_name %in% input$map_lor)
}
# Baumgattung
if (!is.null(input$map_baumgattung) && !("Alle" %in% input$map_baumgattung)) {
data <- data %>% filter(gattung_deutsch %in% input$map_baumgattung)
}
# Jahr (nur wenn timestamp vorhanden und als Date/Year formatiert)
if (!is.null(input$map_year) && !("2020-2024" %in% input$map_year)) {
data$timestamp <- as.Date(data$timestamp)
data <- data %>%
filter(lubridate::year(timestamp) %in% as.numeric(input$map_year))
}
# Saison
if (!is.null(input$map_saison) && !("Alle" %in% input$map_saison)) {
data$monat <- lubridate::month(as.Date(data$timestamp))
data$saison <- case_when(
data$monat %in% c(12, 1, 2) ~ "Winter",
data$monat %in% c(3, 4, 5) ~ "Frühling",
data$monat %in% c(6, 7, 8) ~ "Sommer",
data$monat %in% c(9, 10, 11) ~ "Herbst",
TRUE ~ "Unbekannt"
)
data <- data %>% filter(saison %in% input$map_saison)
}
# Koordinaten check
data <- data %>% filter(!is.na(lat), !is.na(lng))
return(data)
})
icon_pumpe <- makeIcon(
iconUrl = "icons/water-pump-icon-14.jpg",
iconWidth = 15,
iconHeight = 15
)
output$map <- renderLeaflet({
data <- filtered_data_map()
color_palette <- colorNumeric(
palette = colorRampPalette(c("#FFA500", "#0000FF"))(100),
domain = c(0, 2500),
na.color = "#CCCCCC"
)
leaflet(data = data) %>%
addTiles() %>%
addCircleMarkers(
lng = ~lng,
lat = ~lat,
radius = 4,
stroke = FALSE,
fillOpacity = 0.7,
color = ~color_palette(pmin(gesamt_bewaesserung, 2500)),
popup = ~paste0(
"<strong>Baumart: </strong>", art_dtsch, "<br>",
"<strong>Gattung: </strong>", gattung_deutsch, "<br>",
"<strong>Standort: </strong>", strname, " ", hausnr, "<br>",
"<strong>Gesamtbewässerung: </strong>", round(as.numeric(gesamt_bewaesserung), 1), " Liter", "<br>",
"Ø <strong>Bewässerungsintervall: </strong>",
ifelse(is.infinite(durchschnitts_intervall), "Keine Daten",
paste(round(durchschnitts_intervall, 1), " Tage")), "<br><br>"
)
) %>%
addMarkers(
data = pumpen_mit_bezirk,
icon = icon_pumpe,
group = "Pumpen"
) %>%
addLayersControl(
overlayGroups = c("Pumpen"),
options = layersControlOptions(collapsed = FALSE)
) %>%
addLegend(
position = "bottomright",
pal = color_palette,
values = c(0, 2500),
title = "Gesamtbewässerung (Liter)",
labFormat = labelFormat(suffix = " L", digits = 0),
opacity = 1
) %>%
hideGroup("Pumpen") # Anfangs ausblenden
})
# Observer für Zoomlevel-Änderungen
observe({
req(input$map_zoom) # Sicherstellen, dass Zoomlevel verfügbar ist
zoom_level <- input$map_zoom
if (!is.null(zoom_level)) {
leafletProxy("map") %>%
clearGroup("Pumpen") %>%
{
if (zoom_level >= 14) { # Nur anzeigen ab Zoomlevel 14
addMarkers(., data = pumpen_mit_bezirk, icon = icon_pumpe, group = "Pumpen")
} else {
.
}
}
}
})
}