Maanmittauslaitoksen geocoding-rajapinnan käyttö R-kielellä

geocoding
spatial
data
R
mml
Tekijä
Julkaistu

27. tammikuuta 2023

Maanmittauslaitoksen geokoodauspalvelua kuvataan verkkosivuilla seuraavasti:

Geokoodauspalvelu mahdollistaa sijainnin löytämisen hakusanoilla, jotka sisältävät paikannimiä, osoitteita, kiinteistötunnuksia (kiinteistötunnus ja palstan tunnuspisteen sijainti) tai karttalehtiä koko maasta. Palvelu tarjoaa myös käänteistä geokoodausta, eli lähimpien kohteiden hakemista annetuilla koordinaateilla.

Tarkempi dokumentaatio ja käyttöesimerkit löytyvät taas sivulta Tekninen kuvaus Geokoodauspalvelu (REST) v2. Tässä blogissa näytetään miten palvelua voi käyttää R-kielellä.

Maanmittauslaitoksen rajapinnat edellyttävät api-avaimen käyttöä. Api-avaimia ei pidä kirjoittaa suoraan skripteihin. Yksi parempi tapa on tallentaa api-avaimet ympäristömuuttujiksi ja lukea arvo ympäristömuuttujasta Sys.getenv()-funktiolla.

# Tallenna ympäristömuuttuja esim .Rprofile -tiedostoon.
Sys.setenv("MML_API_KEY"="4b5ff7a19789-4a95-85bb-ddbb-936f5fb4")
# lue avain objektiksi sessioon
mml_api_key <- Sys.getenv("MML_API_KEY")

Suora geokoodaus

Tutustu aluksi suoran geokoodauksen logiikkaan https://github.com/pelias/documentation/blob/master/search.md.

Määritellään funktio mml_api_key.

mml_api_key <- Sys.getenv("MML_API_KEY")

#' Geocode a place name or an address
#'
#' @param search_string plane name or an address
#' @param apikey MML api key
#' @param string source of data. One of geographic-names, interpolated-road-addresses, addresses, 
#' @return sf data
#' @export

geocode <- function(search_string = "", 
                        source = "interpolated-road-addresses", 
                        output_crs = "EPSG:3067", 
                        lang = "fi",
                        size = NULL,
                        options = NULL,
                        api_key = NULL) {
  
  if (!source %in% c("interpolated-road-addresses",
                     "geographic-names", "addresses",
                     "mapsheets-tm35","cadastral-units")){
    stop("source must be one of 'interpolated-road-addresses','geographic-names', 'addresses','mapsheets-tm35','cadastral-units'")
  }
  if (!output_crs %in% c("EPSG:3067","EPSG:4326")){
    stop("output_crs must be one of 'EPSG:3067','EPSG:4326'")
  }
  if (!lang %in% c("fi","sv","en")){
    stop("lang must be one of 'fi','se' or 'en'")
  }
  if (is.null(api_key)){
    stop("api_key must be provided")
  }
  
  base_url = "https://avoin-paikkatieto.maanmittauslaitos.fi/geocoding/v2/pelias/search"
  queries <- paste0("?text=", 
                           URLencode(search_string), 
                 "&sources=",source,
                 "&crs=",output_crs,
                 "&lang=",lang,
                 "&api-key=", api_key)
  if (!is.null(size)){
    queries <- paste0(queries, "&size=", size)
  }
  if (!is.null(options)){
    queries <- paste0(queries, "&options=", options)
  }

  # Set the user agent
  ua <- httr::user_agent("https://github.com/rOpenGov/geofi")
  # Construct the query URL
  urls <- paste0(base_url, queries)
  
  # Print out the URL
  # message("Requesting response from: ", url)
  
  query_geocode <- function(x, query_ua = ua, crs1=output_crs){
    #dat <- suppressWarnings(sf::st_read(x, crs = as.integer(sub(".+:", "", crs1))))
    # Get the response and check the response.
  resp <- httpcache::GET(x, query_ua)
  
  # Strip the namespace as it will be only trouble
  # xml2::xml_ns_strip(content)
  
  if (httr::http_error(resp)) {
    status_code <- httr::status_code(resp)
    # If status code is 400, there might be more information available
    # exception_texts <- ""
    # if (status_code == 400) {
    #   # exception_texts <- xml2::xml_text(xml2::xml_find_all(content, "//ExceptionText"))
    #   # Remove URI since full URL is going to be displayed
    #   #exception_texts <- exception_texts[!grepl("^(URI)", exception_texts)]
    #   #exception_texts <- c(exception_texts, paste("URL: ", url))
    # } else if (status_code == 403) {
    #   
    # }
    stop(
      sprintf(
        "OGC API %s request failed\n[%s]",
        paste(x),
        httr::http_status(status_code)$message#,
        # paste0(exception_texts, collapse = "\n ")
      ),
      call. = FALSE
    )
  }
  resp_sf <- suppressWarnings(sf::st_read(resp, crs = as.integer(sub(".+:", "", crs1)), quiet = TRUE))
  if (nrow(resp_sf) == 0){
    return()
  } else {
  resp_sf$query <- x  
  return(resp_sf)
  }
  }
  dat <- lapply(urls, query_geocode) %>%
    do.call("rbind", .)
  return(dat)
}

Testataan funktiota ensin hakemalla paikannimistä kaikki paikannimet, joissa esiintyy sana kainu.

library(sf)
library(mapview)
library(dplyr)
library(leaflet)

kainut <- geocode(search_string = "kainu", 
                      source = "geographic-names", 
                      output_crs = "EPSG:4326", # koska tehdään leaflet-kartta
                      api_key = Sys.getenv("MML_API_KEY"))

leaflet(kainut) %>%
  addTiles() %>%
  addMarkers() %>%
  addLabelOnlyMarkers(label =  ~label, 
                      labelOptions = labelOptions(noHide = T,
                                                  direction = 'top',
                                                  textOnly = T,
                                                  textsize = "20px"))

Sitten yritetään hakea pistekoordinaatti katuosoitteelle. Kokeillaan tehdä se Pelkosennniemen kunnanvirastolle osoitteessa Sodankyläntie 1 A 98500 Pelkosenniemi.

pelko <- geocode(search_string = "Sodankyläntie 1 A 98500 Pelkosenniemi", 
                      source = "interpolated-road-addresses",
                      api_key = Sys.getenv("MML_API_KEY"))

leaflet(pelko %>% st_transform(4326)) %>%
  addTiles() %>%
  addMarkers() %>% 
  addLabelOnlyMarkers(label =  ~label, 
                      labelOptions = labelOptions(noHide = T,
                                                  direction = 'top',
                                                  textOnly = T,
                                                  textsize = "20px"))

Voit myös hakea useamman osoitteen samalla kertaa, kuten yliopistosairaalat tässä tapauksessa

yo_sairaala_osoitteet <- c("haartmaninkatu 4 Helsinki", "Kiinamyllynkatu 4 Turku", "Kajaanintie 50 Oulu", "Puijonlaaksontie 2 Kuopio", "Kuntokatu 2 Tampere")

yo_sairaalat <- geocode(search_string = yo_sairaala_osoitteet, 
                            source = "interpolated-road-addresses",
                            api_key = Sys.getenv("MML_API_KEY"))


ggplot() + 
  geom_sf(data = geofi::get_municipalities() %>% st_union()) +
  geom_sf_label(data = yo_sairaalat, aes(label = label)) 

Käänteinen geokoodaus

Tutustu aluksi käänteisen geokoodauksen logiikkaan https://github.com/pelias/documentation/blob/master/reverse.md.

  • /geocoding/v2/pelias/reverse

Paikannimen, osoitteen tai muun paikkatietokohteen haku annetun paikan (esim. sijaintipisteellä määriteltynä) läheisyydestä.

#' @param location string location plane name or an address
#' @param api_key string MML api key
#' @param boundary_circle_radius numeric 
#' @param size numeric 
#' @param layers string
#' @param sources string
#' @param boundary_country string
#' @param api_key string  MML api key
#' @return sf of list
#' @export

#' @examples
#' 
#' geocode_reverse(point = geofi::municipality_central_localities[1:3,], 
#'             source = "geographic-names", 
#'             apikey = Sys.getenv("MML_API_KEY"))
geocode_reverse <- function(point, 
                                boundary_circle_radius = NULL,
                                size = NULL,
                                layers = NULL,
                                sources = NULL,
                                boundary_country = NULL,
                                return = "sf", # json
                                api_key = Sys.getenv("MML_API_KEY")) {
  
  if (!all(sf::st_is_valid(point))) stop("location is not a valid sf-object")

  create_queries <- function(x){
    coords <- sf::st_coordinates(x)
    query <- paste0("?point.lat=",coords[[2]], 
                   "&point.lon=",coords[[1]],
                   "&api-key=", api_key)
    
    if (!is.null(boundary_circle_radius)){
      query <- paste0(query, "&boundary.circle.radius=", boundary_circle_radius)
    }
    if (!is.null(size)){
      query <- paste0(query, "&size=", size)
    }
    if (!is.null(layers)){
      query <- paste0(query, "&layers=", layers)
    }
    if (!is.null(sources)){
      query <- paste0(query, "&sources=", sources)
    }
      if (!is.null(boundary_country)){
      query <- paste0(query, "&boundary.country=", boundary_country)
    }
    return(query)
  }
  
  lst <- list()
  for (i in 1:nrow(point)){
    lst[[i]] <- create_queries(x = point[i,])
  }
  queries <- do.call("c", lst) 
  
  ua <- httr::user_agent("https://github.com/rOpenGov/geofi")
  # Construct the query URL
  base_url = "https://avoin-paikkatieto.maanmittauslaitos.fi/geocoding/v2/pelias/reverse"
  urls <- paste0(base_url, queries)
  
  # Print out the URL
  # message("Requesting response from: ", url)
  
  query_geocode <- function(x, query_ua = ua){
    # Get the response and check the response.
    resp <- httpcache::GET(x, query_ua)
    if (return == "sf"){
      ddat <- sf::st_read(resp, quiet = TRUE)
    } else {
      ddat <- httr::content(resp, as = "parsed")
    }
  }
  if (return == "sf"){
  dat <- lapply(urls, query_geocode) %>%
    do.call("rbind", .)    
  } else {
    dat <- lapply(urls, query_geocode)
  }
  return(dat)
}

Tee käänteinen geokoodaus Eduskuntatalolle sf-objektina 300 metrin säteellä pisteestä ja että palautetaan ensimmäiset 10 osumaa.

eduskuntatalo <- tibble(lon = 24.933333, lat = 60.1725) %>% 
  sf::st_as_sf(coords = c("lon","lat"), crs = 4326)

res <- geocode_reverse(point = eduskuntatalo, 
                       size = 10,
                       boundary_circle_radius = 300, 
                       return = "sf")

leaflet(res) %>%
  addTiles() %>%
  addMarkers() %>% 
  addLabelOnlyMarkers(label =  ~label, 
                      labelOptions = labelOptions(noHide = T,
                                                  direction = 'top',
                                                  textOnly = T,
                                                  textsize = "20px"))

Hakusanojen tulkinta

  • /geocoding/v2/searchterm/decode

Hakusanan tunnistus vertailemalle hakusanaa luettelopalvelussa saatavilla oleviin koodilistojen koodien selitteisiin. Jos sopiva selite löytyy, palautetaan vastaava koodiarvo.

Samankaltaiset hakusanat

  • /geocoding/v2/searchterm/similar

Samankaltaisten termien haku vertaamalla hakusanaa paikannimiin ja palauttamalla parhaiten saatuun syötteeseen sopivat.

Uudelleenkäyttö

Viittaus

BibTeX-viittaus:
@online{kainu2023,
  author = {Kainu, Markus},
  title = {Maanmittauslaitoksen geocoding-rajapinnan käyttö R-kielellä},
  date = {2023-01-27},
  url = {https://markuskainu.fi/posts/2023-01-27-mml-geocoding},
  langid = {fi}
}
Viitatkaa tähän teokseen seuraavasti:
Kainu, Markus. 2023. “Maanmittauslaitoksen geocoding-rajapinnan käyttö R-kielellä.” January 27, 2023. https://markuskainu.fi/posts/2023-01-27-mml-geocoding.