class: center, middle, inverse, title-slide .title[ # Introduction to Geospatial Techniques for Social Scientists in R ] .subtitle[ ## Advanced Data Import & Processing ] .author[ ### Stefan Jünger & Anne-Kathrin Stroppe ] .institute[ ### GESIS Workshop ] .date[ ### April 24, 2024 ] --- layout: true --- ## Now <table class="table" style="color: black; margin-left: auto; margin-right: auto;"> <thead> <tr> <th style="text-align:left;"> Day </th> <th style="text-align:left;"> Time </th> <th style="text-align:left;"> Title </th> </tr> </thead> <tbody> <tr> <td style="text-align:left;color: gray !important;"> April 23 </td> <td style="text-align:left;color: gray !important;"> 10:00-11:30 </td> <td style="text-align:left;font-weight: bold;"> Introduction to GIS </td> </tr> <tr> <td style="text-align:left;color: gray !important;"> April 23 </td> <td style="text-align:left;color: gray !important;"> 11:45-13:00 </td> <td style="text-align:left;font-weight: bold;"> Vector Data </td> </tr> <tr> <td style="text-align:left;color: gray !important;color: gray !important;"> April 23 </td> <td style="text-align:left;color: gray !important;color: gray !important;"> 13:00-14:00 </td> <td style="text-align:left;font-weight: bold;color: gray !important;"> Lunch Break </td> </tr> <tr> <td style="text-align:left;color: gray !important;"> April 23 </td> <td style="text-align:left;color: gray !important;"> 14:00-15:30 </td> <td style="text-align:left;font-weight: bold;"> Mapping </td> </tr> <tr> <td style="text-align:left;color: gray !important;border-bottom: 1px solid"> April 23 </td> <td style="text-align:left;color: gray !important;border-bottom: 1px solid"> 15:45-17:00 </td> <td style="text-align:left;font-weight: bold;border-bottom: 1px solid"> Raster Data </td> </tr> <tr> <td style="text-align:left;color: gray !important;background-color: yellow !important;"> April 24 </td> <td style="text-align:left;color: gray !important;background-color: yellow !important;"> 09:00-10:30 </td> <td style="text-align:left;font-weight: bold;background-color: yellow !important;"> Advanced Data Import & Processing </td> </tr> <tr> <td style="text-align:left;color: gray !important;"> April 24 </td> <td style="text-align:left;color: gray !important;"> 10:45-12:00 </td> <td style="text-align:left;font-weight: bold;"> Applied Data Wrangling & Linking </td> </tr> <tr> <td style="text-align:left;color: gray !important;color: gray !important;"> April 24 </td> <td style="text-align:left;color: gray !important;color: gray !important;"> 12:00-13:00 </td> <td style="text-align:left;font-weight: bold;color: gray !important;"> Lunch Break </td> </tr> <tr> <td style="text-align:left;color: gray !important;"> April 24 </td> <td style="text-align:left;color: gray !important;"> 13:00-14:30 </td> <td style="text-align:left;font-weight: bold;"> Investigating Spatial Autocorrelation </td> </tr> <tr> <td style="text-align:left;color: gray !important;"> April 24 </td> <td style="text-align:left;color: gray !important;"> 14:45-16:00 </td> <td style="text-align:left;font-weight: bold;"> Spatial Econometrics & Outlook </td> </tr> </tbody> </table> --- ## More on the Geospatial Data Landscape Geospatial data tend to be quite big - pressure to distribute data efficiently Data dumps (on the internet) may not be helpful - when resources are low - time's a factor - the data have a large geographic extent Often a *Programming Application Interfaces* (API) is used --- ## What Is an API? An Application Programming Interface (API) serves as an entry point to data distributed over the internet to - get data - push data Standardized mechanisms - to query data - often just a simple text string to enter in your URL address bar - it gets complicated when login data are required --- ## Data Providers Offering Geospatial Data APIs - [OpenStreetMap](https://wiki.openstreetmap.org/wiki/API) - [Google](https://developers.google.com/maps/documentation/geolocation/overview) - [Bing](https://docs.microsoft.com/en-us/bingmaps/rest-services/locations/) - [Copernicus Climate Data Store](https://cds.climate.copernicus.eu/) - ... - [Cologne's Open Data Portal](https://www.offenedaten-koeln.de/dataset/taxonomy/term/44/field_tags/Geo-44) - Specialized `R` packages, such as the [`wiesbaden` package](https://cran.r-project.org/web/packages/wiesbaden/index.html) or the [`tidycensus` package](https://cran.r-project.org/web/packages/tidycensus/index.html) --- ## Example: Access to Public Transport Say, we're interested in the accessibility of public transport in Cologne - bus, tram, etc. - all platforms and vehicles should be wheel-chair accessible **We can gather this information using OpenStreetMap!** --- ## Accessing OSM Data: The Overpass API > The Overpass API (formerly known as OSM Server Side Scripting, or OSM3S before 2011) is a read-only API that serves up custom selected parts of the OSM map data. It acts as a database over the web: the client sends a query to the API and returns the data set that corresponds to the query. .tinyisher[Source: https://wiki.openstreetmap.org/wiki/Overpass_API] --- ## Starting With a Geographic Area to Query Many geospatial API requests start with a bounding box or another geographical extent to define which area should be accessed. .pull-left[ ```r cologne_pt_stops <- osmdata::getbb( "Köln" ) ``` ] -- .pull-right[ <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/cologne-polygon-plot-1.png" style="display: block; margin: auto;" /> ] --- ## Defining Some Technical Details The Overpass API also requires a timeout parameter that repeats the request for a while if anything fails. ```r cologne_pt_stops <- cologne_pt_stops |> osmdata::opq(timeout = 25*100) cologne_pt_stops ``` ``` ## $bbox ## [1] "50.8304399,6.7725303,51.0849743,7.162028" ## ## $prefix ## [1] "[out:xml][timeout:2500];\n(\n" ## ## $suffix ## [1] ");\n(._;>;);\nout body;" ## ## $features ## NULL ## ## $osm_types ## [1] "node" "way" "relation" ## ## attr(,"class") ## [1] "list" "overpass_query" ## attr(,"nodes_only") ## [1] FALSE ``` --- ## Turning to the Content The content we aim to request is defined with key/value pairs. It's best to learn them by looking them up in the [official documentation](https://wiki.openstreetmap.org/wiki/Map_features). ```r cologne_pt_stops <- cologne_pt_stops |> osmdata::add_osm_feature(key = "public_transport", value = "stop_position") cologne_pt_stops ``` ``` ## $bbox ## [1] "50.8304399,6.7725303,51.0849743,7.162028" ## ## $prefix ## [1] "[out:xml][timeout:2500];\n(\n" ## ## $suffix ## [1] ");\n(._;>;);\nout body;" ## ## $features ## [1] "[\"public_transport\"=\"stop_position\"]" ## ## $osm_types ## [1] "node" "way" "relation" ## ## attr(,"class") ## [1] "list" "overpass_query" ## attr(,"nodes_only") ## [1] FALSE ``` --- ## Conduct Actual Request/Download Data is finally downloaded in the `osmdata` package, e.g., using the `osmdata::osmdata_sf()` function. ```r cologne_pt_stops <- cologne_pt_stops |> osmdata::osmdata_sf() cologne_pt_stops ``` --- ## Filter and Transform The data comprises a list that can be accessed as any list in `R`. Here, we extract the points and wrangle them spatially. .pull-left[ ```r cologne_pt_stops <- cologne_pt_stops$osm_points |> tibble::as_tibble() |> sf::st_as_sf() |> sf::st_transform(3035) |> dplyr::filter(wheelchair == "yes") cologne_pt_stops ``` ``` ## Simple feature collection with 597 features and 89 fields ## Geometry type: POINT ## Dimension: XY ## Bounding box: xmin: 4094554 ymin: 3084876 xmax: 4121662 ymax: 3112024 ## Projected CRS: ETRS89-extended / LAEA Europe ## # A tibble: 597 × 90 ## osm_id name FIXME `VRS:gemeinde` `VRS:name` `VRS:old_ref` `VRS:ortsteil` ## * <chr> <chr> <chr> <chr> <chr> <chr> <chr> ## 1 361716 Eifel… <NA> KÖLN <NA> <NA> Innenstadt ## 2 388550 Brahm… <NA> KÖLN <NA> <NA> Lindenthal ## 3 21033643 Weins… <NA> KÖLN Weinsberg… <NA> Ehrenfeld ## 4 27042401 Markt <NA> BERGISCH GLAD… Bergisch … <NA> Mitte ## 5 28122005 Heuma… <NA> KÖLN <NA> <NA> Innenstadt ## 6 28301370 Kalke… <NA> KÖLN Kalker Fr… <NA> Merheim ## 7 28301373 Merhe… <NA> KÖLN <NA> <NA> Merheim ## 8 28301416 Refra… <NA> BERGISCH GLAD… <NA> <NA> Refrath ## 9 30145575 Köln-… <NA> KÖLN Holweide … <NA> Holweide ## 10 30388742 Mülhe… <NA> <NA> <NA> <NA> <NA> ## # ℹ 587 more rows ## # ℹ 83 more variables: `VRS:ref` <chr>, alt_name <chr>, ashtray <chr>, ## # bench <chr>, bin <chr>, bus <chr>, bus_bay <chr>, bus_lines <chr>, ## # check_date <chr>, `check_date:crossing` <chr>, covered <chr>, ## # crossing <chr>, departures_board <chr>, description <chr>, ## # direction <chr>, disused <chr>, fixme <chr>, `gtfs:name` <chr>, ## # `gtfs:stop_id` <chr>, highway <chr>, image <chr>, internet_access <chr>, … ``` ] --- ## The Data Indeed Are Mappable .pull-left[ ```r tm_shape(cologne_pt_stops) + tm_dots() ``` ] -- .pull-right[ <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/cologne-pt-stops-map-1.png" style="display: block; margin: auto;" /> ] --- ## Fiddling With the Data: Creating a Quick 'Heatmap' OpenStreetMap points data are nice for analyzing urban infrastructure. Let's draw a quick 'heatmap' using kernel densities. .pull-left[ ```r cologne_pt_stops_densities <- cologne_pt_stops |> sf::as_Spatial() |> as("ppp") |> spatstat.explore::density.ppp(sigma = 500) |> terra::rast() terra::crs(cologne_pt_stops_densities) <- "epsg:3035" ``` ] -- .pull-right[ <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/cologne-pt-stops-densities-plot-1.png" style="display: block; margin: auto;" /> ] --- class: middle ## Exercise 2_1_1: Working with OSM Data [Exercise](https://stefanjuenger.github.io/gesis-workshop-geospatial-techniques-R-2024/exercises/2_1_1_Working_with_OSM_Data.html) [Solution](https://stefanjuenger.github.io/gesis-workshop-geospatial-techniques-R-2024/solutions/2_1_1_Working_with_OSM_Data.html) --- ## Accessing Unpackaged (Vector) Data Not all data come as pretty as OpenStreetMap data in, e.g., the `osmdata` package. Don't worry. There are methods to import data from a source that - only provide a URL - not yet prepared for analysis --- ## Example: GeoJSON Files JSON files are pretty popular - standardized and well-structured - similar to XML-files, but human readability is a bit better - also easy to parse for editors and browser There's also a JSON flavor for geospatial data... --- ## GeoJSON Snippet ``` { "type": "FeatureCollection", "features": [ { "type": "Feature", "id": 12, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.957362270020273, 50.94308762750329 ] ... ``` .tinyisher[Source: https://www.offenedaten-koeln.de/] --- ## An Application From Cologne’s Open Data Portal ```r park_and_ride <- glue::glue( "https://geoportal.stadt-koeln.de/arcgis/rest/services/verkehr/", "verkehrskalender/MapServer/8/query?where=objectid+is+not+null&text=&", "objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&", "spatialRel=esriSpatialRelIntersects&distance=&units=esriSRUnit_Foot&", "relationParam=&outFields=*&returnGeometry=true&returnTrueCurves=false&", "maxAllowableOffset=&geometryPrecision=&outSR=4326&havingClause=&", "returnIdsOnly=false&returnCountOnly=false&orderByFields=&", "groupByFieldsForStatistics=&outStatistics=&returnZ=false&returnM=false&", "gdbVersion=&historicMoment=&returnDistinctValues=false&resultOffset=&", "resultRecordCount=&returnExtentOnly=false&datumTransformation=&", "parameterValues=&rangeValues=&quantizationParameters=&featureEncoding=", "esriDefault&f=pjson" ) |> sf::st_read(as_tibble = TRUE) ``` ``` ## Reading layer `file' from data source ## `https://geoportal.stadt-koeln.de/arcgis/rest/services/verkehr/verkehrskalender/MapServer/8/query?where=objectid+is+not+null&text=&objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&distance=&units=esriSRUnit_Foot&relationParam=&outFields=*&returnGeometry=true&returnTrueCurves=false&maxAllowableOffset=&geometryPrecision=&outSR=4326&havingClause=&returnIdsOnly=false&returnCountOnly=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&returnZ=false&returnM=false&gdbVersion=&historicMoment=&returnDistinctValues=false&resultOffset=&resultRecordCount=&returnExtentOnly=false&datumTransformation=¶meterValues=&rangeValues=&quantizationParameters=&featureEncoding=esriDefault&f=pjson' ## using driver `ESRIJSON' ## Simple feature collection with 24 features and 9 fields ## Geometry type: POINT ## Dimension: XY ## Bounding box: xmin: 6.814912 ymin: 50.84826 xmax: 7.16169 ymax: 51.05211 ## Geodetic CRS: WGS 84 ``` .tinyisher[Source: https://www.offenedaten-koeln.de/dataset/park-and-ride-anlagen-koeln] --- ## Park & Ride Parking Spaces With just two 'simple' commands, we can retrieve geospatial data about Cologne's Park & Ride parking spaces in `R`. Not too bad, right? .pull-left[ ```r tm_shape(park_and_ride) + tm_dots() ``` ] -- .pull-right[ <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/trash-cologne-plot-exec-1.png" style="display: block; margin: auto;" /> ] --- ## Raster Data Access It is not only vector data that can be processed through these mechanisms. The idea is the same for raster data - accessing the information through URLs - just the downloaded data formats differ --- ## Example: Downloading German Weather Data The German Weather Service provides excellent weather data in its Climate Data Center (CDC): https://opendata.dwd.de/climate_environment/CDC/ Let's download some summer temperature data using a custom function. ```r download_dwd_data <- function(url, path) { download.file(url, dest = paste0(path, "/", "lyr.1.asc.gz")) R.utils::gunzip( paste0(path, "/", "lyr.1.asc.gz"), overwrite = TRUE, remove = TRUE ) raster_file <- terra::rast(paste0(path, "/", "lyr.1.asc")) unlink(paste0(path, "/", "lyr.1.asc.gz")) raster_file } ``` --- ## Voilà .pull-left[ ```r paste0( "https://opendata.dwd.de/climate_environment/CDC/grids_germany/seasonal/", "air_temperature_max/14_JJA/grids_germany_seasonal_air_temp_max_202314.", "asc.gz") |> download_dwd_data(path = "./data/") |> terra::plot() ``` ] -- .pull-right[ <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/dwd-map-exec-1.png" style="display: block; margin: auto;" /> ] --- ## OSM Data Can Be Gathered As Raster Data, Too The `tmaptools` package is handy for downloading OpenStreetMap data (tiles). ```r cologne_raster <- tmaptools::read_osm(park_and_ride) |> terra::rast() cologne_raster ``` ``` ## class : SpatRaster ## dimensions : 939, 1006, 3 (nrow, ncol, nlyr) ## resolution : 38.37281, 38.36172 (x, y) ## extent : 758632.6, 797235.6, 6594496, 6630517 (xmin, xmax, ymin, ymax) ## coord. ref. : WGS 84 / Pseudo-Mercator ## source(s) : memory ## names : red, green, blue ## min values : 3, 2, 2 ## max values : 254, 254, 254 ``` --- ## Mapped OSM Raster Data The resulting data can be packed in a `tmap` workflow. .pull-left[ ```r tm_shape(cologne_raster) + tm_rgb() ``` ] -- .pull-right[ <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/cologne-raster-map-exec-1.png" style="display: block; margin: auto;" /> ] --- ## Use It As a Background Map These data are images, so they are perfect for use as background maps when mapping other geospatial attributes, such as our park and ride data. .pull-left[ ```r tm_shape(cologne_raster) + tm_rgb() + tm_shape(park_and_ride) + tm_dots(size = .3) ``` ] -- .pull-right[ <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/cologne-raster-map-bg-exec-1.png" style="display: block; margin: auto;" /> ] --- ## Playing With Different Map Types A list of available type names can be seen with the function call `OpenStreetMap::getMapInfo()`. -- .pull-left[ ```r tmaptools::read_osm( park_and_ride, type = "bing" ) |> tm_shape() + tm_rgb() + tm_shape(park_and_ride) + tm_dots(size = .3, col = "red") ``` <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/esri-topo-1.png" width="70%" style="display: block; margin: auto;" /> ] -- .pull-right[ ```r tmaptools::read_osm( park_and_ride, type = "esri-topo" ) |> tm_shape() + tm_rgb() + tm_shape(park_and_ride) + tm_dots(size = .3) ``` <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/stamen-watercolor-1.png" width="70%" style="display: block; margin: auto;" /> ] --- ## Shameless Advertising: The `z11` Package There are times when no APIs are available to download geospatial data. The German Census 2011 is a prime example - only a data dump of Gigabytes of data exists for hundreds of attributes - when you often work with these data, it's a pain **Thus, I created my own (pseudo-)API for these data hosted over Github.** --- ## Accessing Data Details on using the package can be found [here](https://stefanjuenger.github.io/z11/articles/using-z11.html), but it's straightforward. For example, if you want to download data on immigrant rates on a 1 km² grid, you can use the following function. ```r immigrants_germany <- z11::z11_get_1km_attribute(Auslaender_A) immigrants_germany[immigrants_germany <= -1] <- NA immigrants_germany ``` ``` ## class : SpatRaster ## dimensions : 867, 641, 1 (nrow, ncol, nlyr) ## resolution : 1000, 1000 (x, y) ## extent : 4031500, 4672500, 2684500, 3551500 (xmin, xmax, ymin, ymax) ## coord. ref. : ETRS89-extended / LAEA Europe (EPSG:3035) ## source(s) : memory ## name : Auslaender_A ## min value : 0 ## max value : 100 ``` --- ## It’s a Raster As it is a raster file, you can plot it easily. -- .pull-left[ ```r tm_shape(immigrants_germany) + tm_raster(palette = "viridis") ``` ] -- .pull-right[ <img src="data:image/png;base64,#2_1_Advanced_Data_Import_Processing_files/figure-html/census-immigrants-map-execution-1.png" style="display: block; margin: auto;" /> ] --- class: middle ## Exercise 2_1_2: Wrangling the German Census [Exercise](https://stefanjuenger.github.io/gesis-workshop-geospatial-techniques-R-2024/exercises/2_1_2_Wrangling_the_German_Census.html) [Solution](https://stefanjuenger.github.io/gesis-workshop-geospatial-techniques-R-2024/solutions/2_1_2_Wrangling_the_German_Census.html) --- class: middle ## Add-on Slides --- ## OGC Web Services OSM and others provide a standardized interface to their data. The Open Geospatial Consortium developed a more broadly used interface design for more diverse data sources - often used by public authorities all over the world - support of well-supported data formats - good documentation --- ## Displaying vs. Downloading These web services can broadly be divided into services to - display data - download data Let's briefly focus on download services! --- ## Download Services Web Feature Service (WFS) - vector data **Web Coverage Service (WCS)** - raster data Unfortunately, as of today, no ready-to-play packages are providing full access to OWS services in `R` - The [`ows4R` package](https://cran.r-project.org/web/packages/ows4R/index.html) lets you only use WFS services - But you could establish an interface with `Python` and use its `OWSLib` to access WCS data (see add-on slides. <!-- STOP WITH SLIDES HERE --> --- layout: false class: center background-image: url(data:image/png;base64,#../assets/img/the_end.png) background-size: cover .left-column[ </br> <img src="data:image/png;base64,#../img/Stefan.png" width="75%" style="display: block; margin: auto;" /> ] .right-column[ .left[.small[<svg viewBox="0 0 512 512" style="height:1em;position:relative;display:inline-block;top:.1em;" xmlns="http://www.w3.org/2000/svg"> <path d="M464 64H48C21.49 64 0 85.49 0 112v288c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V112c0-26.51-21.49-48-48-48zm0 48v40.805c-22.422 18.259-58.168 46.651-134.587 106.49-16.841 13.247-50.201 45.072-73.413 44.701-23.208.375-56.579-31.459-73.413-44.701C106.18 199.465 70.425 171.067 48 152.805V112h416zM48 400V214.398c22.914 18.251 55.409 43.862 104.938 82.646 21.857 17.205 60.134 55.186 103.062 54.955 42.717.231 80.509-37.199 103.053-54.947 49.528-38.783 82.032-64.401 104.947-82.653V400H48z"></path> </svg> [stefan.juenger@gesis.org](mailto:stefan.juenger@gesis.org)] .small[<svg viewBox="0 0 512 512" style="height:1em;position:relative;display:inline-block;top:.1em;" xmlns="http://www.w3.org/2000/svg"> <path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path> </svg> [`@StefanJuenger`](https://twitter.com/StefanJuenger)] .small[<svg viewBox="0 0 496 512" style="height:1em;position:relative;display:inline-block;top:.1em;" xmlns="http://www.w3.org/2000/svg"> <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path> </svg> [`StefanJuenger`](https://github.com/StefanJuenger)] .small[<svg viewBox="0 0 576 512" style="height:1em;position:relative;display:inline-block;top:.1em;" xmlns="http://www.w3.org/2000/svg"> <path d="M280.37 148.26L96 300.11V464a16 16 0 0 0 16 16l112.06-.29a16 16 0 0 0 15.92-16V368a16 16 0 0 1 16-16h64a16 16 0 0 1 16 16v95.64a16 16 0 0 0 16 16.05L464 480a16 16 0 0 0 16-16V300L295.67 148.26a12.19 12.19 0 0 0-15.3 0zM571.6 251.47L488 182.56V44.05a12 12 0 0 0-12-12h-56a12 12 0 0 0-12 12v72.61L318.47 43a48 48 0 0 0-61 0L4.34 251.47a12 12 0 0 0-1.6 16.9l25.5 31A12 12 0 0 0 45.15 301l235.22-193.74a12.19 12.19 0 0 1 15.3 0L530.9 301a12 12 0 0 0 16.9-1.6l25.5-31a12 12 0 0 0-1.7-16.93z"></path> </svg> [`https://stefanjuenger.github.io`](https://stefanjuenger.github.io)]] ]