It may often be desirable to separately colour two or more portions of a map
that are separated by a line object. This is not directly possible because only
polygons can be filled with colour, not lines. The osm_line2poly()
function
comes to the rescue here by converting a line to a polygon surrounding a given
plotting region. The classic example where this arises is with coastlines. These
are always represented in OpenStreetMap as line objects, preventing any ability
to simply colour land and ocean separately.
This vignette illustrates the general principles of the osm_line2poly()
function, along with several ancillary issues such as plotting coastal islands.
Although this functionality has been primarily developed with coastlines in
mind, the osm_line2poly()
function has been designed in a sufficiently general
manner to be readily adaptable to any other cases where such line-to-polygon
conversion may be desirable.
This vignette explores the example of the coastline around Greater Melbourne, Australia, first demonstrating how to extract coastline and convert it to land and sea polygons, and then demonstrating how these areas may be delineated on a plot.
We use osmdata
to extract the
coastline within the bounding box of Greater Melbourne.
map_dpi <- 72 # dpi res for all maps fetch_osm <- FALSE
library (osmplotr) library (osmdata) library (magrittr) bbox <- osmdata::getbb ("greater melbourne, australia") coast <- opq (bbox = bbox) %>% add_osm_feature (key = "natural", value = "coastline") %>% osmdata_sf (quiet = FALSE)
This coastline object consists of several types of structure
coast
message (paste0 ( "Object of class 'osmdata' with:\n", " $bbox : -38.49937,144.44405,-37.40175,146.1925\n", " $overpass_call : The call submitted to the overpass API\n", " $timestamp : [ Thurs 5 Oct 2017 10:23:18 ]\n", " $osm_points : 'sf' Simple Features Collection with 13635 points\n", " $osm_lines : 'sf' Simple Features Collection with 73 linestrings\n", " $osm_polygons : 'sf' Simple Features Collection with 12 polygons\n", " $osm_multilines : 'sf' Simple Features Collection with 0 multilinestrings\n", " $osm_multipolygons : 'sf' Simple Features Collection with 0 multipolygons" ))
Because OpenStreetMap represents coastline as line objects, all coastline data
is contained within the $osm_lines
object. The osm_line2poly()
function can
then convert these lines to polygons which can be used to plot filled areas.
coast_poly <- osm_line2poly (coast$osm_lines, bbox) names (coast_poly)
c ("sea", "land", "islands")
Note that reflecting its envisioned primary usage, the function always returns
objects named "sea"
, "land"
, and "islands"
. For usages other than
coastline, these names will of course reflect other kinds of object. The
"islands"
item contains any polygons which are separate to those originally
queried. Each item of this list is an sf::data.frame
object:
class (coast_poly$sea)
c ("sf", "data.frame")
The list
items returned by osm_line2poly()
may then be used to provide a map
background which distinguishes ocean from land. Here we first colour the entire
map using the background colour for the ocean, and overlay the land and island
polygons on top of that.
map <- osm_basemap (bbox = bbox, bg = "cadetblue2") %>% add_osm_objects (coast_poly$land, col = "lightyellow1") %>% add_osm_objects (coast_poly$islands, col = "orange") %>% add_osm_objects (coast$osm_polygons, col = "purple", border = "black") %>% add_osm_objects (coast$osm_lines, col = "black") %>% print_osm_map ()
print_osm_map (map, filename = "melb_a1.png", width = 600, units = "px", dpi = map_dpi )
The gaudy colours differentiate the source of polygons. Purple islands were
returned by the original osm query, while the orange ones were constructed from
fragments by osm_line2poly
.
The osm_line2poly()
function works by identifying lines which extend at at
least two points beyond a given bounding box. For coastline, OpenStreetMap is
designed so that land always lies to the left side in the direction of the
line, enabling water and land to be systematically distinguished. The following
test cases demonstrate the reliability of this distinction.
getCoast <- function (bbox) { qry <- opq (bbox) qry <- add_osm_feature (qry, key = "natural", value = "coastline") return (osmdata_sf (qry)) } testPlot <- function (coast, bbox) { if (!dev.cur ()) dev.off () map <- osm_basemap (bbox = bbox) map <- add_osm_objects (map, coast$osm_lines) print_osm_map (map) } testPlotPoly <- function (coast, bbox, fname) { ## trouble doing this check properly on Travis if (nrow (coast$osm_lines) > 0) { coastp <- osm_line2poly (coast$osm_lines, bbox = bbox) map <- osm_basemap (bbox = bbox) map <- add_osm_objects (map, coastp$sea, col = "cadetblue2") map <- add_osm_objects (map, coastp$land, col = "sienna2") print_osm_map (map, filename = fname, width = 200, units = "px", dpi = map_dpi ) } else { warning ("osm query probably failed - not plotting") invisible (NULL) } }
test_plot <- function (bbox) { dat <- opq (bbox) %>% add_osm_feature (key = "natural", value = "coastline") %>% osmdata_sf (quiet = FALSE) coast <- osm_line2poly (dat$osm_lines, bbox) osm_basemap (bbox = bbox) %>% add_osm_objects (coast$sea, col = "cadetblue2") %>% add_osm_objects (coast$land, col = "sienna2") }
test_plot <- function (bbox, filename, map_dpi) { dat <- opq (bbox) %>% add_osm_feature (key = "natural", value = "coastline") %>% osmdata_sf (quiet = FALSE) coast <- osm_line2poly (dat$osm_lines, bbox) osm_basemap (bbox = bbox) %>% add_osm_objects (coast$sea, col = "cadetblue2") %>% add_osm_objects (coast$land, col = "sienna2") %>% print_osm_map ( file = filename, width = 200, units = "px", dpi = map_dpi ) }
Fetch the test data. A variable name with WE
indicates that the coast enters the bounding box
on the western side and exits on the east. The land is on the left when following that path.
bbWE <- get_bbox (c (142.116906, -38.352713, 142.205162, -38.409661)) coastWE <- getCoast (bbWE) bbEW <- get_bbox (c (144.603127, -38.104003, 144.685557, -38.135596)) coastEW <- getCoast (bbEW) bbNS <- get_bbox (c (143.807998, -39.770986, 143.906494, -39.918643)) coastNS <- getCoast (bbNS) bbSN <- get_bbox (c (144.073544, -39.854586, 144.149318, -39.960047)) coastSN <- getCoast (bbSN) bbWW <- get_bbox (c (144.904865, -37.858295, 144.923679, -37.874367)) coastWW <- getCoast (bbWW) bbEE <- get_bbox (c (144.643383, -38.294671, 144.692197, -38.336022)) coastEE <- getCoast (bbEE) bbNN <- get_bbox (c (145.856321, -38.831642, 146.050920, -38.914031)) coastNN <- getCoast (bbNN) bbSS <- get_bbox (c (146.363768, -38.770345, 146.486389, -38.837287)) coastSS <- getCoast (bbSS) bbEN <- get_bbox (c (144.738212, -38.337690, 144.758053, -38.346966)) coastEN <- getCoast (bbEN) bbEWWS <- get_bbox (c (144.693077, -38.307526, 144.729113, -38.343997)) coastEWWS <- getCoast (bbEWWS) bbWS <- get_bbox (c (143.164906, -38.704885, 143.2075563, -38.7462058)) coastWS <- getCoast (bbWS)
testPlotPoly (coastWE, bbWE, "testWE.png")
testPlotPoly (coastEW, bbEW, "testEW.png")
testPlotPoly (coastNS, bbNS, "testNS.png")
testPlotPoly (coastSN, bbSN, "testSN.png")
testPlotPoly (coastWW, bbWW, "testWW.png")
testPlotPoly (coastEE, bbEE, "testEE.png")
testPlotPoly (coastNN, bbNN, "testNN.png")
testPlotPoly (coastSS, bbSS, "testSS.png")
testPlotPoly (coastEN, bbEN, "testEN.png")
testPlotPoly (coastEWWS, bbEWWS, "testEWWS.png")
testPlotPoly (coastWS, bbWS, "testWS.png")
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.