Nearest locations to multiple routes - r

I'd like to get the nearest location from each of a list of routes and the distance from the route to the place. I think I can do this via SF but am not sure how. In the sample data there are 19 separate routes.
install.packages("sf")
install.packages("sfheaders")
library(sf)
routeData <- read.csv("https://www.dropbox.com/s/vtj8wvcqxj52pbl/SpainActivityRoutes.csv?dl=1")
# Convert routes to SF
sfheaders::sf_multipolygon(
obj = spainActivityRoutes
, multipolygon_id = "id"
, x = "lon"
, y = "lat"
)
# Read in locations
locations <- data.frame(id = c(1,2,3),
place = c('Alcudia', 'Puerto de Pollensa', 'Alaro'),
latitude = c(39.85327712, 39.9024565, 39.704459175469395),
longitude = c(3.123974802, 3.080426926, 2.7919874776545694))

Starting with the data:
routeData <- read.csv("https://www.dropbox.com/s/vtj8wvcqxj52pbl/SpainActivityRoutes.csv?dl=1")
split on id, apply a function to create linestring objects, join the list of linestrings using st_sfc to make a spatial vector. Assume these are "GPS" coordinates with EPSG code 4326:
routes = do.call(st_sfc, lapply(split(routeData, routeData$id) , function(d){st_linestring(cbind(d$lon, d$lat))}))
st_crs(routes)=4326
Convert points data frame to spatial points data frame with same coordinate system:
pts = st_as_sf(locations, coords=c("longitude","latitude"), crs=4326)
Now we can get the nearest route to each point:
> nearf = st_nearest_feature(pts, routes)
> nearf
[1] 1 15 19
So the first point is nearest to route 1, the second point route 15, the third point route 19. Now the distances we get by computing the distance from each point in turn to each of those route lines in turn by using st_distance with by_element=TRUE (otherwise it computes the distances from all points to all three routes as a matrix):
> st_distance(pts, routes[st_nearest_feature(pts, routes)], by_element=TRUE)
Units: [m]
[1] 7.888465 27.046029 44.175458
If you want the point on the route nearest to the point data then use st_nearest_points with pairwise=TRUE:
> st_nearest_points(pts, routes[st_nearest_feature(pts, routes)], pairwise=TRUE)
Geometry set for 3 features
Geometry type: LINESTRING
Dimension: XY
Bounding box: xmin: 2.791987 ymin: 39.70412 xmax: 3.124058 ymax: 39.90256
Geodetic CRS: WGS 84
LINESTRING (3.123975 39.85328, 3.124058 39.85331)
LINESTRING (3.080427 39.90246, 3.080143 39.90256)
LINESTRING (2.791987 39.70446, 2.792247 39.70412)
which returns 2-point lines from the test point to the line. You can use functions like st_cast(...,"POINT") to split those into points and get the locations as points.

Related

Spatial Points to Polygons query

I have a question regarding converting spatial data in R and bringing it from R into QGIS.
I have a GeoTiff of Antarctic sea ice concentration, downloaded from the link below:
https://seaice.uni-bremen.de/databrowser/#day=13&month=10&year=2022&img=%7B%22image%22%3A%22image-1%22%2C%22product%22%3A%22AMSR%22%2C%22type%22%3A%22visual%22%2C%22region%22%3A%22Antarctic3125%22%7D
I want to extract the contour of the sea ice edge (defined as 15%), and then have this contour in a file type that I can open in QGIS and reproject for use in making other maps. My current understanding is that to do this, I would need to convert the contour to a spatial points df, and then convert that to a spatial polygons df which I would then be able to open as a shapefile in QGIS. However, I think I'm going wrong here as I cannot make the conversion with the below code - any suggestions?
**This is my current workflow:**
library(raster)
library(tidyverse)
library(sp)
library(sf)
#Load in sea ice geotiff
sic <- raster('Environmental_Data/SIC/AMSR2/asi-AMSR2-s3125-20220107-v5.4.tif')/1
plot(sic)
#Make all values over land NA
sic[sic>100] = NA
#Crop to make area smaller (I have a specific area of interest)
sic = crop(sic, extent(sic)*c(0.5,0.5,0,1))
plot(sic)
#Pull out the sea ice edge (15% contour) (this makes it a spatial lines df)
ie = rasterToContour(sic, levels=15)
#Convert to spatial points
ie.pt = as(ie, "SpatialPointsDataFrame") plot(ie.pt, add=T, pch=16, cex=0.4)
#Convert to spatial polygons
ie.pt_poly <-as(ie.pt, "SpatialPolygons")
#Then I get this error:
Error in as(ie.pt, "SpatialPolygons"):
no method or default for coercing “SpatialPointsDataFrame” to “SpatialPolygons”
reworking your process to terra and sf...
library(terra)
library(sf)
sic <- rast('~/Downloads/asi-AMSR2-s3125-20221113-v5.4.tif')
sic[sic>100] = NA
sic2 = crop(sic, ext(sic)*c(0.5,0.5,0,1))
sic2_contour <- terra::contour(sic2, maxcells=100000, filled = TRUE) # plot side effect
sic2_cont <- as.contour(sic2)
sic2_cont_disagg <- disagg(sic2_cont)
y <- sf::st_as_sf(sic2_cont_disagg)
y
Simple feature collection with 6519 features and 1 field
Geometry type: LINESTRING
Dimension: XY
Bounding box: xmin: -1968608 ymin: 206384.6 xmax: 1968608 ymax: 3660462
Projected CRS: WGS 84 / NSIDC Sea Ice Polar Stereographic South
First 10 features:
level geometry
1 10 LINESTRING (-1968608 340765...
2 10 LINESTRING (-1955825 232458...
3 10 LINESTRING (-1968608 259539...
4 10 LINESTRING (-1968608 262189...
5 10 LINESTRING (-1960827 264530...
6 10 LINESTRING (-1968608 265308...
7 10 LINESTRING (-1968608 278293...
8 10 LINESTRING (-1943042 251270...
9 10 LINESTRING (-1943042 275001...
10 10 LINESTRING (-1930259 331948...
unique(y$level)
[1] 10 20 30 40 50 60 70 80 90 100
y10 <- y[which(y$level == 10),]
plot(sic2)
plot(y10, col = 'pink', lwd =3, add = TRUE)
I can't think why one would go to points, except perhaps to then buffer and fill one's contour. But terra::writeRaster(sic2..., and terra::writeVector(y,..., or y10 pull into QGIS and see.
There are pink(s) interior to sic2 as these presumably are holes in sea ice that have the same value as northernmost contour that could perhaps be further removed by testing for within.
I think this is what you are looking for.
library(terra)
r <- rast("asi-AMSR2-s3125-20221113-v5.4.tif")
# crop to the area of interest
e <- ext(-1975000, 1975000, 2e+05, 4350000)
re <- crop(r, e)
# get contour and save to file
v <- as.contour(re, levels=15)
writeVector(v, "contour_lines.shp")
Contours are normally lines (neither points nor polygons). But if you wanted a polygon you could do
x <- ifel(x <15 | x>100, NA, 1)
p <- as.polygons(x)
writeVector(p, "contour_polygons.shp")
Or, more generally, use terra::classify to create regions before using as.polygons.

Smoothing polygons on map with ggplot2 and sf

How can you smooth the polygons of a map produced with ggplot and sf?
I have used the sf package to extract the polygons from a shapefile
geomunicipios <- st_read("ruta/archivo.shp")
Reading layer `archivo' from data source
`ruta\archivo.shp'
using driver `ESRI Shapefile'
Simple feature collection with 45 features and 10 fields
Geometry type: MULTIPOLYGON
Dimension: XY
Bounding box: xmin: -2.344411 ymin: 37.37375 xmax: -0.647983 ymax: 38.75509
Geodetic CRS: WGS 84
And ggplot2 to plot the map:
rmurcia <- ggplot(data = geomunicipios) +
geom_sf(aes(fill=columna),color="#FFFFFF",size=1)
To perform the smoothing of the polygons I have analyzed three alternatives:
i. package "smoothr":
geosmunicipios <- smooth(geomunicipios, method = "ksmooth", smoothness = 12)
ii. package "rmapshaper": geosmunicipios <- ms_simplify(geomunicipios, keep = 0.02500, weighting = 12)
iii. package "sf": geosmunicipios <- st_simplify(geomunicipios, dTolerance = 50, preserveTopology = TRUE)
You have to try different values of the parameters to adjust to the needs and obtain the desired result.
To reproduce the case, the download can be done from: centrodedescargas.cnig.es/CentroDescargas/index.jsp
And follow the links:
Información geográfica de referencia - Límites municipales, provinciales y autonómicos - Descargar: lineas_limite.zip.
And the path in the uncompressed folder:
SIGLIM_Publico_INSPIRE - SHP_ETRS89 - recintos_municipales_inspire_peninbal_etrs89 - recintos_municipales_inspire_peninbal_etrs89.shp
Finally, for this case I have chosen to use rmapshaper, it produces a satisfactory result with a reduced size of the .pdf file, where I include the graphic.

Spatial join longitude and latitude points to multipolygon shapefile

I have a spatial data frame of the longitude and latitude of wildfire origins that I am trying to perform a spatial join on to a US Census TIGER/Line shapefile (places) to see if/where there is spatial intersection of fire origins and places.
I am converting the longitude and latitude to coordinate geometry using st_as_sf then attempting to st_join this to the places file, but am encountering an error as the CRS are different. The shapefile is in NAD83 projection, so I am attempting to match that.
library(tidyverse)
library(sf)
> head(fires)
# Longitude Latitude FireName
#1 -106.46667 34.66000 TRIGO
#2 -81.92972 35.87111 SUNRISE
#3 -103.76944 37.52694 BRIDGER
#4 -122.97556 39.37500 BACK
#5 -121.15611 39.62778 FREY
#6 -106.38306 34.77056 BIG SPRING
#convert df to sf
fires_sf <- st_as_sf(fires, coords = c("Longitude", "Latitude"), crs = 4269, agr = "constant")
head(fires_sf$geometry)
#Geometry set for 6 features
#Geometry type: POINT
#Dimension: XY
#Bounding box: xmin: -122.9756 ymin: 34.66 xmax: -81.92972 ymax: 39.62778
#Geodetic CRS: NAD83
#POINT (-106.4667 34.66)
#POINT (-81.92972 35.87111)
#POINT (-103.7694 37.52694)
#POINT (-122.9756 39.375)
#POINT (-121.1561 39.62778)
head(places$geometry)
#Geometry set for 6 features
#Geometry type: MULTIPOLYGON
#Dimension: XY
#Bounding box: xmin: -1746916 ymin: -395761.6 xmax: -1655669 ymax: -212934.8
#Projected CRS: USA_Contiguous_Albers_Equal_Area_Conic
#First 5 geometries:
#MULTIPOLYGON (((-1657066 -233757.7, -1657192 -2...
#MULTIPOLYGON (((-1668181 -273428.5, -1669420 -2...
#MULTIPOLYGON (((-1735046 -389578.2, -1735146 -3...
#MULTIPOLYGON (((-1732841 -376703.9, -1732642 -3...
#MULTIPOLYGON (((-1693749 -377716, -1693286 -377..
joined <- st_join(places, fires_sf)
Error in st_geos_binop("intersects", x, y, sparse = sparse, prepared = prepared, :
st_crs(x) == st_crs(y) is not TRUE
To work around this, I have tried st_transform to change the projection to longitude and latitude coordinates, as the places shapefile may be using UTM coordinates, and the datum to NAD83 in both spatial frames. I am getting an error for this as well.
#transform CRS projections
places_transform <- st_transform(places, "+proj=longlat +datum=NAD83")
fires_sf_transform <- st_transform(fires_sf, "+proj=longlat +datum=NAD83")
joined_new <- st_join(places_transform, fires_sf_transform)
Error in s2_geography_from_wkb(x, oriented = oriented, check = check) :
Evaluation error: Found 1045 features with invalid spherical geometry.
[3] Loop 0 is not valid: Edge 280 has duplicate vertex with edge 306
I have attempted to convert the geometry from longitude and latitude coordinates in the fires dataset to UTM coordinates to match the places shapefile, but this was also unsuccessful.
Any advice on how I can properly perform the spatial join of these points and multipolygons would be greatly appreciated.

Cleaning Geocode data with r

I am cleaning my dataset and I don't know how to clean GPS data.
when I use the table function I find that they are entered in different shapes.
"547140",
"35.6997",
"251825.7959",
"251470.43",
"54/4077070001",
and "54/305495"
I don't know how to clean this variable with this great difference.
I would be thankful if help me or suggest me a website for training.
Your main issue is standardizing the GPS by projecting GPS to a coordinate system of choice. Say we have the GPS of amsterdam in two different coordinate systems, one in amersfoort/rd new (espg 28992) and one in wsg1984 (espg 4326):
x y location espg
1: 1.207330e+05 486632.35593 amsterdam 28992
2: 4.884088e+00 52.36651 amsterdam 4326
structure(list(x = c(120733.012428048, 4.88408811380055), y = c(486632.355933105,
52.3665054922233), location = c("amsterdam", "amsterdam"), espg = c(28992,
4326)), row.names = c(NA, -2L), class = "data.frame")
What we want to do is reproject our coordinates to one geographic coordinate system of choice. In this case I used WSG1984 (espg 4326).
library(sf)
#here I tell R which columns contain the coordinates
coordinates(dt) <- ~x+y
#I now convert the table to a spatial object
dt <- st_as_sf(dt)
#here I split by the different ESPG's present
dt <- split(dt, dt$espg)
#here I loop through every individual espg present in the dataset
for(i in 1:length(dt)){
#here I say in which coordinate system (espg) the GPS data is in
st_crs(dt[[i]]) <- unique(dt[[i]]$espg)
#here I transform the coordinates to another projection (in this case WSG1984, espg 4326)
dt[[i]] <- dt[[i]] %>% st_transform(4326)
}
#here I bind the items of the list together
dt <- do.call(rbind, dt)
head(dt)
Simple feature collection with 2 features and 2 fields
Geometry type: POINT
Dimension: XY
Bounding box: xmin: 4.884088 ymin: 52.36651 xmax: 4.884088 ymax: 52.36651
Geodetic CRS: WGS 84
location espg geometry
4326 amsterdam 4326 POINT (4.884088 52.36651)
28992 amsterdam 28992 POINT (4.884088 52.36651)
In the geometry column you now see that the coordinates are equal to one another.
Bottom line is that you need to know the geographic coordinate system the GPS data is in. Then you can convert your data from a table to a spatial object and transform the GPS data to a projection of choice.
In addition, it is always a good idea to check if your assumption on the original ESPG is good by for example plotting the data.
library(ggplot2)
library(ggspatial)
ggplot(dt) + annotation_mape_tile() + geom_sf(size = 4) + theme(text = element_text(size = 15) + facet_wrap(~espg)
In the figurebelow we see that the projection went well for both espg's.

R - nested loop for list of SpatialLinesDataFrame intersected with SpatialPolygonsDataFrame objects

I have a series of steps I need to complete on a list of SpatialLinesDataFrame ('lines' herein) objects based on their relationships with individual features within a multi-feature SpatialPolygonsDataFrame ('polygons') object. In short, each line list element originates inside a single polygon feature, and may or may not pass through one or more other polygon features. I want to update each line element to connect origin polygons to the first point of contact for each individual polygon intersected by the line element. So, each line element may become multiple new line features (n=number of intersected polygons).
I would like to do this efficiently as my lines lists and polygon features are numerous. I have provided example data and STEP-wise description of what I am trying to do below. I am new to R and not a programmer, so I don't know if any of what I propose is valid.
library(sp)
library(rgdal)
library(raster)
###example data prep START
#example 'RDCO Regional Parks' data can be downloaded here:
https://data-rdco.opendata.arcgis.com/datasets group_ids=1950175c56c24073bb5cef3900e19460
parks <- readOGR("/path/to/example/data/RDCO_Regional_Parks/RDCO_Regional_Parks.shp")
plot(parks)
#subset watersheds for example
parks_sub <- parks[parks#data$Shapearea > 400000,]
plot(parks_sub, col='green', axes = T)
#create SpatialLines from scratch
pts_line1 <- cbind(c(308000, 333000), c(5522000, 5530000))
line1 <- spLines(pts_line1, crs = crs(parks_sub))
plot(line1, axes=T, add=T) #origin polygon = polyl[[4]] = OBJECTID 181
pts_line2 <- cbind(c(308000, 325000), c(5524000, 5537000))
line2 <- spLines(pts_line2, crs = crs(parks_sub))
plot(line2, axes=T, add=T) #origin polygon = polyl[[8]] = OBJECTID 1838
linel <- list()
linel[[1]] <- line1
linel[[2]] <- line2
#convert to SpatialLinesDataFrame objects
lineldf <- lapply(1:length(linel), function(i) SpatialLinesDataFrame(linel[[i]], data.frame(id=rep(i, length(linel[[i]]))), match.ID = FALSE))
#match id field value with origin polygon
lineldf[[1]]#data$id <- 181
lineldf[[2]]#data$id <- 1838
###example data prep END
#initiate nested for loop
for (i in 1:length(lineldf)) {
for (j in 1:length(parks_sub[j,])) {
#STEP 1:for each line list feature (NB: with ID matching origin polygon ID)
#identify whether it intersects with a polygon list feature
if (tryCatch(!is.null(intersect(lineldf[[i]], parks_sub[j,])), error=function(e) return(FALSE)) == 'FALSE'){
next
}
#if 'FALSE', go on to check intersect with next polygon in list
#if 'TRUE', go to STEP 2
#STEP 2: add intersected polygon OBJECTID value to SLDF new column in attribute table
#i.e., deal with single intersected polygon at a time
else {
lineldf[[i]]#data["id.2"] = parks_sub[j,]#data$OBJECTID
#STEP 3: erase portion of line overlapped by intersected SPDF
line_erase <- erase(lineldf[[i]],parks_sub[j,])
#STEP 4: erase line feature(s) that no longer intersect with the origin polygon
#DO NOT KNOW HOW TO SELECT feature (i.e., line segment) within 'line_erase' object
if (tryCatch(!is.null(intersect(line_erase[???], parks_sub[j,])), error=function(e) return(FALSE)) == 'FALSE'){
line_erase[???] <- NULL}
else {
#STEP 5: erase line features that only intersect with origin polygon
if (line_erase[???]#data$id.2 = parks_sub[j,]#data$OBJECTID){
line_erase[???] <- NULL
}
else {
#STEP 6: write valid line files to folder
writeOGR(line_erase,
dsn = paste0("path/to/save/folder", i, ".shp"),
layer = "newline",
driver = 'ESRI Shapefile',
overwrite_layer = T)
}}}}}
Here's a solution using the sf package. I'll work with the objects you create and convert them to sf, although you can read shapefiles into sf objects and create them from scratch so if there's no other reason to use sp objects I'd recommend that.
Use these packages:
library(sf)
library(dplyr)
Convert polygons. I'm dropping a load of columns from parks_sub just so it can print neater - if you need them don't drop them:
p = st_as_sf(parks_sub)
p = p[,c("OBJECTID","PARK_NAME")]
Convert lines. I'm only going to work with your first line object, write a loop over your list to do a whole set:
l1 = st_as_sf(lineldf[[1]])
Next step is to compute all the intersection points between your line and your polygons. You have to: a) convert the polygons to linestrings otherwise the intersection of a polygon and a line is a line, and b) convert the "MULTIPOINT" intersections when a line crosses a polygon more than once into a set of POINT objects using st_cast:
pts = st_cast(st_cast(st_intersection(l1,
st_cast(p,"MULTILINESTRING")
),"MULTIPOINT"),"POINT")
Next we need the first point of the line. For the data you create in the example, the line end in the polygon is actually the second point, so I'll extract that here.
first_point = st_cast(l1$geom,"POINT")[2]
If in your real data its the first point then put [1] in there. If it depends then that's another little problem.
Now compute the distances from that first point to all the intersection points:
pts$d_first = st_distance(first_point, pts)[1,]
So what we want now is the nearest intersection point in each group of points defined by the same polygon ID.
near_pts = pts %>% group_by(OBJECTID) %>% filter(d_first==min(d_first))
Then the desired lines are constructed from the first point to those nearest points:
nlines = st_cast(st_union(near_pts, first_point),"LINESTRING")
Plot the polygons and the lines in decreasing width to show the overlap:
plot(p$geom)
plot(nlines$geom, lwd=c(10,7,4), col=c("black","grey","white"), add=TRUE)
Note the three lines include a short one (in white) from the first point to the boundary of the polygon it is in - if you don't want this you can filter out the point with the nearest distance from the data frame before constructing the lines - but that assumes the first point is inside a polygon...
nlines retains the attributes of the polygons the line intersects, as well as the ID of the line:
> nlines
Simple feature collection with 3 features and 4 fields
geometry type: LINESTRING
dimension: XY
bbox: xmin: 310276 ymin: 5522728 xmax: 333000 ymax: 5530000
epsg (SRID): 26911
proj4string: +proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs
id OBJECTID PARK_NAME d_first geometry
1 181 2254 Mission Creek 6794.694 m LINESTRING (326528.6 552792...
2 181 1831 Glen Canyon 23859.161 m LINESTRING (310276 5522728,...
3 181 1838 Black Mountain 1260.329 m LINESTRING (331799.6 552961...
so now wrap all that into a function and loop that over your lines and job done!?

Resources