Project longitude and latitude to planar coordinates system - r

Disclaimer: I am currently using the ppp branch version of the {sf} package, because new features for converting objects between {sf} and {spatstat} are available in it (see https://github.com/r-spatial/sf/issues/1233). For it to work properly, I had to manually delete the {sf} package from my hard drive and then reinstall it from Github. I am also using the development version of {spatstat} for no particular reason.
# install.packages("remotes")
install_github("r-spatial/sf#ppp")
install_github("spatstat/spatstat")
I have two geospatial objects: area_one, which is the union of the polygons of several counties in Texas and vz, which is the point locations of several stores in Texas and they are both objects of the sf family. vz was created using longitude and latitude coordinates scraped from the internet. My goal is to create a ppp object with the locations in vz as the points and the polygon in area_one as the window. The issue is that I cannot find the correct coordinate reference system (CRS), for my points to lie inside the polygon. I get an error telling me that the points lie outside the window. Here are the two files to make the code below reproducible:
area_one: download here
vz: download here
# Load packages
library(sf) # Development version in the ppp branch
library(spatstat) # Development version in the master branch
library(tmap)
library(here)
# Read the geospatial data (CRS = 4326)
area_one <- st_read(dsn = here("area_one/area_one.shp"), layer = "area_one")
vz <- st_read(dsn = here("vz/vz.shp"), layer = "vz")
# Plot a quick map
tm_shape(area_one) +
tm_borders() +
tm_shape(vz) +
tm_dots(col = "red", size = 0.1)
# Create a planar point pattern
vz_lonlat <- st_coordinates(vz)
area_one_flat <- st_transform(area_one, crs = 6345)
p <- ppp(x = vz_lonlat[, "X"], y = vz_lonlat[, "Y"], window = as.owin(area_one_flat)) # Note that the reason why this line of code does not throw an error (especially despite the window argument being an sf object) is because of the version of the {sf} package I am using
Warning message:
49 points were rejected as lying outside the specified window
plot(p)

As #spacedman points out you should first transform vz to the same coordinate system as the observation region. I guess you could do something like (untested):
vz_flat <- st_coordinates(st_transform(vz, crs = 6345))
area_one_flat <- st_transform(area_one, crs = 6345)
p <- ppp(x = vz_flat[, "X"], y = vz_flat[, "Y"], window = as.owin(area_one_flat))

Related

How to truly calculate a spherical voronoi diagram using sf?

I want to make a world map with a voronoi tessellation using the spherical nature of the world (not a projection of it), similar to this using D3.js, but with R.
As I understand ("Goodbye flat Earth, welcome S2 spherical geometry") the sf package is now fully based on the s2 package and should perform as I needed. But I don't think that I am getting the results as expected. A reproducible example:
library(tidyverse)
library(sf)
library(rnaturalearth)
library(tidygeocoder)
# just to be sure
sf::sf_use_s2(TRUE)
# download map
world_map <- rnaturalearth::ne_countries(
scale = 'small',
type = 'map_units',
returnclass = 'sf')
# addresses that you want to find lat long and to become centroids of the voronoi tessellation
addresses <- tribble(
~addr,
"Juneau, Alaska" ,
"Saint Petersburg, Russia" ,
"Melbourne, Australia"
)
# retrive lat long using tidygeocoder
points <- addresses %>%
tidygeocoder::geocode(addr, method = 'osm')
# Transform lat long in a single geometry point and join with sf-base of the world
points <- points %>%
dplyr::rowwise() %>%
dplyr::mutate(point = list(sf::st_point(c(long, lat)))) %>%
sf::st_as_sf() %>%
sf::st_set_crs(4326)
# voronoi tessellation
voronoi <- sf::st_voronoi(sf::st_union( points ) ) %>%
sf::st_as_sf() %>%
sf::st_set_crs(4326)
# plot
ggplot2::ggplot() +
geom_sf(data = world_map,
mapping = aes(geometry = geometry),
fill = "gray95") +
geom_sf(data = points,
mapping = aes(geometry = point),
colour = "red") +
geom_sf(data = voronoi,
mapping = aes(geometry = x),
colour = "red",
alpha = 0.5)
The whole Antarctica should be closer to Melbourne than to the other two points. What am I missing here? How to calculate a voronoi on a sphere using sf?
(This answer doesn't tell you how to do it, but does tell you what's going wrong.)
When I ran this code I got
Warning message:
In st_voronoi.sfc(sf::st_union(points)) :
st_voronoi does not correctly triangulate longitude/latitude data
From digging into the code it looks like this is a known limitation. Looking at the C++ code for CPL_geos_voronoi, it looks like it directly calls a GEOS method for building Voronoi diagrams. It might be worth opening an sf issue to indicate that this is a feature you would value (if no-one tells the developer that particular features would be useful, they don't get prioritized ...) It doesn't surprise me that GEOS doesn't automatically do computations that account for spherical geometry. Although the S2 code base mentions Voronoi diagrams in a variety of places, it doesn't look like there is a drop-in replacement for the GEOS algorithm ... there are a variety of implementations in other languages for spherical Voronoi diagrams (e.g. Python), but someone would probably have to port them to R (or C++) ...
If I really needed to do this I would probably try to figure out how to call the Python code from within R (exporting the data from sf format to whatever Python needs, then re-importing the results into an appropriate sf format ...)
Printing the code for sf:::st_voronoi.sfc:
function (x, envelope = st_polygon(), dTolerance = 0, bOnlyEdges = FALSE)
{
if (compareVersion(CPL_geos_version(), "3.5.0") > -1) {
if (isTRUE(st_is_longlat(x)))
warning("st_voronoi does not correctly triangulate longitude/latitude data")
st_sfc(CPL_geos_voronoi(x, st_sfc(envelope), dTolerance = dTolerance,
bOnlyEdges = as.integer(bOnlyEdges)))
}
else stop("for voronoi, GEOS version 3.5.0 or higher is required")
}
In other words, if the GEOS version is less than 3.5.0, the operation fails completely. If it is >= 3.5.0 (sf:::CPL_geos_version() reports that I have version 3.8.1), and long-lat data is being used, a warning is supposed to be issued (but the computation is done anyway).
The first time I ran this I didn't get the warning; I checked and options("warn") was set to -1 (suppressing warnings). I'm not sure why — running from a clean session did give me the warning. Maybe something in the pipeline (e.g. rnaturalearth telling me I needed to install the rnaturalearthdata package) accidentally set the option?

How to add location points onto a shapefile imported into R from QGIS using ggplot2

I'm working on a project which involves GPS coordinates from offshore locations. I'm looking to measure the distance from shore for each of my points. I have created a shapefile of the shoreline in question in QGIS and I have successfully imported it into R using the st_read() function (named "biminishore" in this example).
With the following code, I'm able to plot my shapefile in ggplot2:
bplot = ggplot() +
geom_sf(data = biminishore, size = 0.1, color = "black", fill = "green1") +
ggtitle("Bimini, The Bahamas") +
coord_sf() +
theme_classic()
plot(bplot)
Now, I would like to add the location coordinates (imported into R as a .csv with separate columns for Lat and Lon) as a layer over the imported shapefile. Can anyone suggest how to go about doing this in a way that will allow me to calculate the distance between each point and the nearest shoreline point?
My currents attempts are giving the error: Error in st_transform.sfc(st_geometry(x), crs, ...) : cannot transform sfc object with missing crs
I assume this means my coordinate systems are incompatible but haven't found a way around this yet. So far, I have tried combining my point columns using SpatialPoints(). I've also tried using multiple forms of st_set_crs() and st_transform() but I haven't had any luck yet. Any help is greatly appreciated! Thanks!
Read your points file as a csv & then transform it to an sf object:
library(tidyverse)
library(sf)
points <- read_csv('path_to_points.csv')
#make it an sf object, change Long and Lat to the correct column name
points_sf <- st_as_sf(points, coords = c("Long", "Lat"))
# set crs of points_sf to same as biminishore object
points_sf <- st_set_crs(points_sf, st_crs(biminishore))
Then you should be able to plot them together by adding:
+ geom_sf(data = points_sf)
to your ggplot2 call.
Finding the nearest feature between the two can be done with sf::st_nearest_feature(points_sf, biminishore).
A good post on nearest features & distances: https://gis.stackexchange.com/questions/349955/getting-a-new-column-with-distance-to-the-nearest-feature-in-r

Failing to assign projection to sp object SpatialPointsDataFrame

I have a SpatialPointsDataFrame called johnny, created from a vanilla dataframe by assigning coordinates. These coordinates are in coordinate system EPSG 4326 (the standard GPS geographic coordinate system), but johnny does not know that. So ,I am trying to assign EPSG 4326 to johnny, essentially as in this earlier question data projection in R using package SP . I, too, am using sp. My ultimate goal is to project johnny to projected_johnny. However, I can't seem to assign the existing projection correctly first. Who sees my mistake?
library(sp)
x <- seq(80,90,by=1)
y <- seq(40,50,by=1)
value <- seq(10,20,by=1)
johnny <- data.frame(cbind(x,y,value))
coordinates(johnny) <- ~x+y
class(johnny)
[1] "SpatialPointsDataFrame"
attr(,"package")
[1] "sp"
proj4string(johnny) <- CRS("+init=epsg:4326")
Error in if (is.na(get("has_proj_def.dat", envir = .RGDAL_CACHE))) { :
argument is of length zero
I have considered and rejected the following possible solutions after trying them out:
Adding library rdgal directly
using CRS("+proj=longlat +datum=WGS84") instead of CRS("+init=epsg:4326")
I am using R 3.6.0 and sp 1.3-1. The rgdal version loaded via sp is 1.5-15. Any ideas are welcome. This should be such a simple action...
I looked over your code and guessed what you are probably trying to accomplish. But the way you are going about things is more different than it needs to be. There is a simple way to accomplished this. By far, the easiest way to accomplish this is by using those tools found in the R, sf package. Know that the sf package is a newer package than the sp package. And the sf package provides easy to use tools for accomplishing these tasks.
The code below is somewhat different from your code. A two column matrix was used instead of your three column data frame.
The simple feature geometry points were created from the matrix. Then the simple feature column object was created from the geometry points. Then the plot was created.
Code:
# Create matrix
x <- seq(80,90,by=1)
y <- seq(40,50,by=1)
# value <- seq(10,20,by=1)
#johnny <- data.frame(cbind(x,y))
jm <- matrix(data = c(x,y), nrow = 11, ncol = 2)
# coordinates(johnny) <- ~x+y
# class(johnny)
# johnny
Create sf multipoint geometry:
jm.sfg <- st_multipoint(jm)
jm.sfg
Create sf column object:
jm.sfc <- st_sfc(jm.sfg, crs = 4326)
jm.sfc
Plot
plot(jm.sfc, axes = TRUE)
The plot can be viewed from link below.

Incorrect NA return when converting Lat/Long Coordinates to location in R

I am trying to use a modified version of the R code found in the following link:
Latitude Longitude Coordinates to State Code in R
To test the code, I created the following formal arguments:
mapping = "state"
pointsDF = data.frame(x = c(-88.04607, -83.03579), y = c(42.06907, 42.32983))
latlong2state(pointsDF, mapping)
The code returned the following:
[1] "Illinois" NA
The first coordinate set returns a correct answer, i.e. "Illinois". However, when I input the 2nd coordinate set (i.e. -83.03579, 42.32983) into an online converter, I get the following:
Downtown, Detroit, MI, USA
(http://www.latlong.net/Show-Latitude-Longitude.html)
Running the code again but changing the second coordinate from 42.32983 to 43.33 puts the point in the state of Michigan.
When using the "world" map as my formal argument for the "mapping" variable, the code returns "USA". I have been struggling for days to figure this out and have had no luck. I have played around with SpatialPointDataFrames, various projections, and looked into the state polygon objects themselves. I am using R version 3.3.1 on a Windows 7 system. I think the data point in question may be falling on a border line. In which case, I think an "NA" would be expected. The code I used is below.
Code Used:
library(sp)
library(maps)
library(maptools)
library(rgdal)
latlong2state = function(pointsDF, mapping) {
local.map = map(database = mapping, fill = TRUE, col = "transparent", plot = FALSE)
IDs = sapply(strsplit(local.map$names, ":"), function(x) x[1])
maps_sp = map2SpatialPolygons(map = local.map, ID = IDs,
proj4string = CRS("+proj=longlat +datum=WGS84"))
pointsSP = SpatialPoints(pointsDF,
proj4string = CRS("+proj=longlat +datum=WGS84"))
indices = over(x = pointsSP, y = maps_sp)
mapNames = sapply(maps_sp#polygons, function(x) {x#ID})
mapNames[indices]
}
I am only two months in to learning R and love the language thus far. This has been the first time I could not find an answer. I would really appreciate an help provided on the matter!!!
Firstly, the issue is not due to the point lying on a border. In fact, over() would not return NA for a point on a border, but rather "if a point falls in multiple polygons, the last polygon is recorded."
NA denotes a point that does not fall in a polygon. We can zoom in on your map to see this is the case
plot(local.map, xlim = c(-83.2, -82.8), ylim=c(42.2,42.6), type="l")
polygon(local.map, col="grey60")
points(local.map)
points(pointsDF[2,], col="red")
The point falls outside the contiguous USA in Canada, according to the polygons provided by maps::map(). Why would this be the case when other maps, as you say, locate this point on the USA side of the border? I do not think this is a projection issue, because we are using the same WGS84 geographic coordinates for the polygons and the points. It seems, therefore, that the polygons themselves that are provided by maps::map() may be wrong.
We can check this by comparing to polygons from another source. I downloaded the US census departments highest resolution state boundaries from http://www2.census.gov/geo/tiger/GENZ2015/shp/cb_2015_us_state_500k.zip. Then,
shp.path <- "C:/Users/xxx/Downloads/cb_2015_us_state_500k/cb_2015_us_state_500k.shp"
states <- readOGR(path.expand(shp.path), "cb_2015_us_state_500k")
plot(states, xlim = c(-83.2, -82.8), ylim=c(42.2,42.6))
points(pointsDF[2,], col="red")
gets us this map in which we see that the point is inside the US boundary:
The solution I recommend therefore, is to use these better resolution, more reliable boundary polygons, particularly if you are interested to accurately resolve points close to borders.

Buffer (geo)spatial points in R with gbuffer

I'm trying to buffer the points in my dataset with a radius of 100km. I'm using the function gBuffer from the package rgeos. Here's what I have so far:
head( sampledf )
# postalcode lat lon city province
#1 A0A0A0 47.05564 -53.20198 Gander NL
#4 A0A1C0 47.31741 -52.81218 St. John's NL
coordinates( sampledf ) <- c( "lon", "lat" )
proj4string( sampledf ) <- CRS( "+proj=longlat +datum=WGS84" )
distInMeters <- 1000
pc100km <- gBuffer( sampledf, width=100*distInMeters, byid=TRUE )
I get the following warning:
In gBuffer(sampledf, width = 100 * distInMeters, byid = TRUE) :
Spatial object is not projected; GEOS expects planar coordinates
From what I understand/read, I need to change the Coordinate Reference System (CRS),
in particular the projection, of the dataset from 'geographic' to 'projected'.
I'm not sure sure how to change this. These are all Canadian addresses, I might add.
So NAD83 seems to me a natural projection to choose but I may be wrong.
Any/all help would be greatly appreciated.
With a little bit more digging, it turns out that using a 'projected' coordinates reference system is as simple as
# To get Statscan CRS, see here:
# http://spatialreference.org/ref/epsg/3347/
pc <- spTransform( sampledf, CRS( "+init=epsg:3347" ) )
EPSG3347, used by STATSCAN (adequate for Canadian addresses), uses a lambert conformal conic projection. Note that NAD83 is inappropriate: it is a 'geographic', rather than a 'projected' CRS. To buffer the points
pc100km <- gBuffer( pc, width=100*distm, byid=TRUE )
# Add data, and write to shapefile
pc100km <- SpatialPolygonsDataFrame( pc100km, data=pc100km#data )
writeOGR( pc100km, "pc100km", "pc100km", driver="ESRI Shapefile" )
As #MichaelChirico pointed out, projecting your data to usergeos::gBuffer() should be applied with care. I am not an expert in geodesy, but as far I understood from this ESRI article (Understanding Geodesic Buffering), projecting and then applying gBuffer means actually producing Euclidean buffers as opposed to Geodesic ones. Euclidean buffers are affected by the distortions introduced by projected coordinate systems. These distortions might be something to worry about if your analysis involves wide buffers especially with a wider range of latitudes across big areas (I presume Canada is a good candidate).
I came across the same issue some time ago and I targeted my question towards gis.stackexchange - Euclidean and Geodesic Buffering in R. I think the R code that I proposed then and also the given answer are relevant to this question here as well.
The main idea is to make use of geosphere::destPoint(). For more details and a faster alternative, see the mentioned gis.stackexchange link above. Here is my older attempt applied on your two points:
library(geosphere)
library(sp)
pts <- data.frame(lon = c(-53.20198, -52.81218),
lat = c(47.05564, 47.31741))
pts
#> lon lat
#> 1 -53.20198 47.05564
#> 2 -52.81218 47.31741
make_GeodesicBuffer <- function(pts, width) {
# A) Construct buffers as points at given distance and bearing ---------------
dg <- seq(from = 0, to = 360, by = 5)
# Construct equidistant points defining circle shapes (the "buffer points")
buff.XY <- geosphere::destPoint(p = pts,
b = rep(dg, each = length(pts)),
d = width)
# B) Make SpatialPolygons -------------------------------------------------
# Group (split) "buffer points" by id
buff.XY <- as.data.frame(buff.XY)
id <- rep(1:dim(pts)[1], times = length(dg))
lst <- split(buff.XY, id)
# Make SpatialPolygons out of the list of coordinates
poly <- lapply(lst, sp::Polygon, hole = FALSE)
polys <- lapply(list(poly), sp::Polygons, ID = NA)
spolys <- sp::SpatialPolygons(Srl = polys,
proj4string = CRS("+proj=longlat +ellps=WGS84 +datum=WGS84"))
# Disaggregate (split in unique polygons)
spolys <- sp::disaggregate(spolys)
return(spolys)
}
pts_buf_100km <- make_GeodesicBuffer(as.matrix(pts), width = 100*10^3)
# Make a kml file and check the results on Google Earth
library(plotKML)
#> plotKML version 0.5-9 (2019-01-04)
#> URL: http://plotkml.r-forge.r-project.org/
kml(pts_buf_100km, file.name = "pts_buf_100km.kml")
#> KML file opened for writing...
#> Writing to KML...
#> Closing pts_buf_100km.kml
Created on 2019-02-11 by the reprex package (v0.2.1)
And to toy around, I wrapped the function in a package - geobuffer
Here is an example:
# install.packages("devtools") # if you do not have devtools, then install it
devtools::install_github("valentinitnelav/geobuffer")
library(geobuffer)
pts <- data.frame(lon = c(-53.20198, -52.81218),
lat = c(47.05564, 47.31741))
pts_buf_100km <- geobuffer_pts(xy = pts, dist_m = 100*10^3)
Created on 2019-02-11 by the reprex package (v0.2.1)
Others might come up with better solutions, but for now, this worked well for my problems and hopefully can solve other's problems as well.

Resources