R: Heading/ Direction of Travel/ bearing from .gpx file - tmaptools - r

Using tmaptools package in R - How can I extract the 'Bearing' information from a .GPX track file. This appears in Garmin Basecamp but does not appear using tmaptools::read_GPX. Currently I use the below code. But surely there is a simpler way? Link to GPS Track: https://www.dropbox.com/s/02p3yyjkv9fmrni/Barron_Thomatis_2019_EOD.gpx?dl=0
library(tmaptools)
library(tmap)
library(sf)
library(tidyverse)
library(geosphere)
GPSTrack <- read_GPX("Barron_Thomatis_2019_EOD.gpx", layers = "track_points", as.sf = TRUE)
#
#Adjust GPS Track Data
#
#Extract Lat & Lon from Track geometery (c(lat, Lon))
GPSTrack_Pts <- st_coordinates(GPSTrack)
#Add X, Y Columns to Track
GPSTrack2 <- cbind(GPSTrack, GPSTrack_Pts)
#Create a coordinate vector by combining X & Y
coords <- cbind(GPSTrack2$X,GPSTrack2$Y)
#Convert GPS Track into SpatialPoints format for calculating Bearing
GPSTrack_SpPts <- SpatialPoints(coords)
#Create GPS Point Bearing, GPP point distance & GPS Time interval columns
empty <- st_as_sfc("POINT(EMPTY)")
GPSTrack2 <- GPSTrack2 %>%
st_set_crs(4326) %>% # will use great circle distance
mutate(
Bearing = bearing(coords))
#Convert Bearing to Course and Add as column
GPSTrack2 <- GPSTrack2 %>%
mutate(course = (Bearing + 360) %% 360) # add full circle, i.e. +360, and determine modulo for 360

I suggest you use lwgeom::st_geod_azimuth() for this task - it makes for somewhat more concise code.
Note that there is a challenge when adding the vector of bearings back to the spatial dataframe of points; it has by definition one element less than is the number of rows (you need two points to define a bearing).
One possibility of achieving that - if required - is by concatenating the vector with a single NA value representing the bearing of the very last point. By definition it has no azimuth, as there is no following point.
The azimuth values are objects of class units, originally in radians. Should the class create a problem (as it does with concatenating with the NA) you can easily convert it to a plain number via units::drop_units().
library(sf)
library(dplyr)
library(lwgeom)
points <- st_read("Barron_Thomatis_2019_EOD.gpx",
layer = "track_points",
quiet = T,
stringsAsFactors = F)
points <- points %>%
mutate(bearing = c(lwgeom::st_geod_azimuth(.) %>% units::drop_units(), NA))

Related

Filling road network data gaps

I have a motorway network with count points that can be matched to road links. However, they only match around half the osm links. The network is uni directional and it should be possible to assign data from joining links to the missing links.
I currently have a rather ugly and long solution based on a WHILE loop that sequentially fills the connecting links. However, I think a more elegant solution might be possible by using an sfnetwork or spatial lines network. The packages stplanr, sfnetwork and dodger closely match what I want to do, but all seem to focus on routing and origin destination data.
Below is a reproducible example that uses a small area of UK motorway network and removes a random sample of half the links and generates flow and speed data for the half remaining.
How do I fill in the missing links with data from either end of the missing links?
library(tidyverse)
library(mapview)
library(sf)
library(osmdata)
## define area to import osm data
x_max <- -2.31
x_min <- -2.38
y_max <- 51.48
y_min <- 51.51
##create a data frame to setup a polygon generation
df <- data.frame(X = c(x_min, x_max, x_max, x_min),
Y = c(y_max, y_max, y_min, y_min))
##generate a polygon of the area
rd_area <- df %>%
st_as_sf(coords = c("X", "Y"), crs = 4326) %>%
dplyr::summarise(geometry = st_combine(geometry)) %>%
st_cast("POLYGON")
##get osm geometry for motorway links for defined area
x <- opq(bbox = rd_area) %>%
add_osm_feature(key = c('highway'), value = c('motorway',
'motorway_link')) %>% osmdata_sf()
## extract line geometry, generate a unique segment ID and get rid of excess columns
rdz <- x$osm_lines %>%
mutate(seg_id = paste0("L", sprintf("%02d", 1:NROW(bicycle)))) %>%
select(seg_id)
## pretend we only have traffic counts and speeds for half the links
osm_dat <- rdz[c(3,4,5,7,11,14,15),]
## links without data
osm_nodat <- filter(rdz, !seg_id %in% osm_dat$seg_id)
## visualise links with data and without
mapview(osm_dat, color = "green")+mapview(osm_nodat, color = "red")
## make up some data to work with
pretend_counts <- st_centroid(osm_dat)
## assign some random annual average daily flow and speed averages
pretend_counts$aadt <- sample(200:600, nrow(pretend_counts))
pretend_counts$speed <- sample(40:80, nrow(pretend_counts))
Here is one quick and elegant solution from the Cyipt project https://github.com/cyipt/cyipt/blob/master/scripts/prep_data/get_traffic.R
It uses the code from the get.aadt.class function and uses Voroni polygons to give the flows and speeds to the nearest roads. However, it doesn't distribute, i.e. split the flows where one links meets two and it sometimes results in opposing directions having the same flows and speeds.
library(dismo) ## dismo package for voroni polygon generation
#Make voronoi polygons and convert to SF
voronoi <- dismo::voronoi(x = st_coordinates(pretend_counts))
voronoi <- as(voronoi, "sf")
st_crs(voronoi) <- st_crs(pretend_counts)
#Find Intersections of roads with vernoi polygons
inter <- st_intersects(osm_nodat,voronoi)
#Get aadt and ncycle values
osm_nodat$aadt <- as.numeric(lapply(1:nrow(osm_nodat),function(x){as.numeric(round(mean(pretend_counts$aadt[inter[[x]]])),0)}))
osm_nodat$speed <- as.numeric(lapply(1:nrow(osm_nodat),function(x){as.numeric(round(mean(pretend_counts$speed[inter[[x]]])),0)}))
#Remove Unneded Data
all_osm <- as.data.frame(rbind(osm_dat, osm_nodat))
st_geometry(all_osm) <- all_osm$geometry
flows <- dplyr::select(all_osm, aadt)
mapview(flows)

How to convert "im" pixel image to raster?

I am trying to convert an "im" pixel image I've produced into a raster image. The "im" was created with the following code:
library(sf)
library(spatstat)
library(rgeos)
library(raster)
# read ebird data
ebd_species <- ("ebd_hooded.txt") %>%
read_ebd()
# extracting coordinates
latitude_species <- ebd_species$latitude
longitude_species <- ebd_species$longitude
#convert to spatial object
coordinates1 <- data.frame(x = longitude_species, y = latitude_species) %>% st_as_sf(coords = c("x", "y"))
# converting to point pattern data
coordinates <- as.ppp(coordinates1)
# density image
a <- density(coordinates,2)
plot(a)
This is the plot I get:
plot
What I want to do is convert this into a raster. I wanna then use the coordinates of the ebird data to extract the values of density from the raster.
Here is a minimal, self-contained, reproducible example (based on the first example in ?im):
library(spatstat)
mat <- matrix(1:1200, nrow=30, ncol=40, byrow=TRUE)
m <- im(mat)
Solution
library(raster)
r <- raster(m)
Looks like you are using geographic coordinates (longitude, latitude) directly in spatstat. Are you sure this is OK in your context? For regions away from the equator this can be quite misleading. Consider projecting to planar coordinates using sf::st_transform() (see other of my answers on this site for code to do this). Also, in newer versions of sf you can convert directly from sf to spatstat format with e.g. as.ppp().
If you want a kernel density estimate of the intensity at the data points you can use the option at = "points" in density.ppp():
a <- density(coordinates, 2, at = "points")
Then a is simply a vector with length equal to the number of points containing the intensity estimate for each data point. This uses "leave-one-out" estimation by default to minimize bias (see the help file for density.ppp).

Measuring the distance of all points along a line in R (linestring in sf)

I have a smoothed line (a simplified abstraction of a coastline) that is a linestring and I want to measure the length of the line at frequent intervals along it. I can create the smoothed line, and measure its length:
library(raster)
library(sf)
library(tidyverse)
library(rnaturalearth)
library(smoothr)
# create bounding box for the line
xmin=-80
xmax=-66
ymin=24
ymax=45
bbox <- extent(xmin, xmax, ymin, ymax)
# get coarse outline of the US
usamap <- rnaturalearth::ne_countries(scale = "small", country = "united states of america", returnclass = "sf")[1] %>%
st_cast("MULTILINESTRING")
# crop to extent of the bbox and get rid of a border line that isn't coastal
bbox2 <- st_set_crs(st_as_sf(as(raster::extent(-80, -74, 42, 45.5), "SpatialPolygons")), st_crs(usamap))
cropmap <- usamap %>%
st_crop(bbox) %>%
st_difference(bbox2)
# smooth the line
smoothmap <- cropmap %>%
smoothr::smooth(method="ksmooth", smoothness=8)
# measure the line length
st_length(smoothmap) # I get 1855956m
I'm going to be "snapping" sample sites to points along this line, and I need to know how far they are along the coastline. However, I can't figure out how to measure the length of the line at intervals along the coastline. The interval size isn't terribly important so long as it's at relatively fine resolution, perhaps every 1km or every 0.01 degree.
What I would like to produce is a dataframe with x,y columns containing the lat/lon of points along the line, and a "length" column containing the distance along the line (from the origin to that point). Here are some of the things I've tried:
Iterating over bounding boxes. I tried cropping the line with smaller and smaller bounding boxes (using regular intervals in lat/lon), but because the line bends "backward" around -76, 38, some of the cropped boxes don't encompass the complete line segment that I expected. This approach works for the top right half of the line, but not for the bottom left half--that just returns lengths of zero.
Cropping the extent before implementing the smoother, and then measuring the line. Since the smoother function does not produce the same shape if it is only measuring a segment of the original line, this doesn't actually measure distance along the same line.
Getting the coordinates of the linestring with st_coordinates, trimming off one row (one point on the line), and recasting the remaining coordinates as a linestring. This approach does not produce a single linestring but instead a chain of points (since st_cast doesn't know how to connect them again), so it can't be measured normally.
It would be ideal to "edit" the geometry of smoothmap to delete one row of coordinates at a time, repeatedly measure the line, and write out the end point coordinates and the line length to a dataframe. However, I'm not sure if it's possible to edit a sf object's coordinates without turning it into a dataframe.
If I understand your question, I think you can do this:
x <- as(smoothmap, "Spatial")
g <- geom(x)
d <- pointDistance(g[-nrow(g), c("x", "y")], g[-1, c("x", "y")], lonlat=TRUE)
gg <- data.frame(g[, c('x','y')], seglength=c(d, 0))
gg$lengthfromhere <- rev(cumsum(rev(gg[,"seglength"])))
head(gg)
# x y seglength lengthfromhere
#1 -67.06494 45.00000 70850.765 1855956
#2 -67.74832 44.58805 2405.180 1785105
#3 -67.77221 44.57474 2490.175 1782700
#4 -67.79692 44.56095 2577.254 1780210
#5 -67.82248 44.54667 2666.340 1777633
#6 -67.84890 44.53189 2757.336 1774967
tail(gg)
# x y seglength lengthfromhere
#539 -79.09383 33.34224 2580.481 111543.5
#540 -79.11531 33.32753 2542.557 108963.0
#541 -79.13648 33.31306 2512.564 106420.5
#542 -79.15739 33.29874 2479.949 103907.9
#543 -79.17802 33.28460 101427.939 101427.9
#544 -80.00000 32.68751 0.000 0.0
I believe you need sf::st_line_sample:
# Transform to metric
smoothmap_utm <- st_transform(smoothmap, 3857)
# Get samples at every kilometer
smoothmap_samples <- st_line_sample(smoothmap_utm, density = 1/1000)
# Transform back to a sf data.frame
smoothmaps_points <- map(smoothmap_samples, function(x) data.frame(geometry = st_geometry(x))) %>%
map_df(as.data.frame) %>% st_sf() %>%
st_cast("POINT") %>%
st_set_crs(3857) %>%
st_transform(4326)
mapview(smoothmaps_points) + mapview(smoothmap)
Getting to your desired output:
# Function to transform sf to lon,lat
sfc_as_cols <- function(x, names = c("lon","lat")) {
stopifnot(inherits(x,"sf") && inherits(sf::st_geometry(x),"sfc_POINT"))
ret <- sf::st_coordinates(x)
ret <- tibble::as_tibble(ret)
stopifnot(length(names) == ncol(ret))
x <- x[ , !names(x) %in% names]
ret <- setNames(ret,names)
ui <- dplyr::bind_cols(x,ret)
st_set_geometry(ui, NULL)
}
smoothmaps_points_xy <- sfc_as_cols(smoothmaps_points) %>%
mutate(dist = cumsum(c(0, rep(1000, times = n() - 1))))
smoothmaps_points_xy
lon lat dist
1 -67.06836 44.99794 0
2 -67.07521 44.99383 1000
3 -67.08206 44.98972 2000
4 -67.08891 44.98560 3000
5 -67.09575 44.98149 4000
6 -67.10260 44.97737 5000
Important
But if your ultimate goal is to get points distance in a path, I would recommend checking rgeos::gProject.

Line density function in R equivalent to Line density tool in ArcMap (arcpy)

I need to calculate the magnitude-per-unit area of polylines that fall within a radius around each cell. Essentially I need to calculate a km/km2 road density within a 500m pixel search radius. ArcMap has a quick and easy tool that handles this, but I need a pure R solution.
Here is a link on how line density works: http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-analyst-toolbox/how-line-density-works.htm
And this is how to use it in a python (arcpy) script: http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-analyst-toolbox/line-density.htm
I currently execute a backwards approach using raster::focal function, calculating a density of burned in road features. I then convert the km2/km2 output to km/km2.
#Import libraries
library(raster)
library(rgdal)
library(gdalUtils)
#Read-in an already created raster mask (cells are all set to 0)
mask <- raster("x://path to raster mask...")
#Make a copy of the mask to burn features in, keeping the original untouched
roads_mask <- file.copy(mask, "x://output path ...//roads.tif")
#Read-in road features (shapefile format)
roads_sldf <- readOGR("x://path to shapefile" , "roads")
#Rasterize spatial lines data frame ie. burn road features into mask
#Where road features get a value of 1, mask extent gets a value of 0
roads_raster <- gdalUtils::gdal_rasterize(src_datasource = roads_sldf,
dst_filename = "x://output path ...//roads.tif", b = 1,
burn = 1, l = "roads", output_Raster = TRUE)
#Run a 1km circular radius density function (be mindful of edge effects)
weight <- raster::focalWeight(roads_raster,1000,type = "circle")
1km_rdDensity <- raster::focal(roads_raster, weight, fun=sum, filename = '',
na.rm=TRUE, pad=TRUE, NAonly=FALSE, overwrite=TRUE)
#Convert km2/km2 road density to km/km2
#Set up the moving window
weight <- raster::focalWeight(roads_raster,1000,type = "circle")
#Count how many records in each column of the moving window are > 0
columnCount <- apply(weight,2,function(x) sum(x > 0))
#Get the sum of the column count
number_of_cells <- sum(columnCount)
#multiply km2/km2 density by number of cells in the moving window
step1 <- roads_raster * number_of_cells
#Rescale step1 output with respect to cell size(30m) and radius of a circle
final_rdDensity <- (step1*0.03)/3.14159265
#Write out final km/km2 road density raster
writeRaster(final_rdDensity,"X://path to output...", datatype = 'FLT4S', overwrite = TRUE)
After some more research I think I may be able to use a kernel function, however I don't want to apply the smoothing algorithm... As well the output is an 'im' object which I would need to write to as a 'tif'
#Import libraries
library(spatstat)
library(rgdal)
#Read-in road features (shapefile format)
roads_sldf <- readOGR("x://path to shapefile" , "roads")
#Convert roads spatial lines data frame to psp object
psp_roads <- as.psp(roads_sldf)
#Apply kernel density, however this is where I am unsure of the arguments
road_density <- spatstat::density.psp(psp_roads, sigma = 0.01, eps = 500)
Cheers.
See this question https://gis.stackexchange.com/questions/138861/calculating-road-density-in-r-using-kernel-density
Tried to mark as a duplicate but doesn't work because the other Q is on gis stack exchange
Short answer is use spatstat.geom::pixellate()
I also needed spatstat.geom::as.psp(sf::st_geometry(x)) to convert an sf lines object to the correct format and maptools::as.im.RasterLayer(r) to convert a raster. I was able to convert the result to RasterLayer with raster::raster(pix_res)
Perhaps you can use terra::rasterizeGeom which is available in the development version that you can install with install.packages('terra', repos='https://rspatial.r-universe.dev')
Example data
library(terra)
f <- system.file("ex/lux.shp", package="terra")
v <- vect(f) |> as.lines()
r <- rast(v, res=.1)
Solution
x <- rasterizeGeom(v, r, fun="length", "km")
And then use focal sum, but you would not have a perfect circle.
What you could do instead, if your dataset is not too large, is create a circle for each grid cell and use intersect. Something like this:
p <- xyFromCell(r, 1:ncell(r)) |> vect(crs="+proj=longlat")
p$id <- 1:ncell(r)
b <- buffer(p, 10000)
values(v) <- NULL
i <- intersect(v, b)
x <- aggregate(perim(i), list(id=i$id), sum)
r[x$id] <- x[,2]

Distance between a set of points and a polygon with sf in R

I have a dataframe of points on map and an area of interest described as a polygon of points. I want to calculate the distance between each of the points to the polygon, ideally using the sf package.
library("tidyverse")
library("sf")
# area of interest
area <-
"POLYGON ((121863.900623145 486546.136633659, 121830.369032584 486624.24942906, 121742.202408334 486680.476675484, 121626.493982203 486692.384434804, 121415.359596921 486693.816446951, 121116.219703244 486773.748535465, 120965.69439283 486674.642759986, 121168.798757601 486495.217550029, 121542.879304342 486414.780364836, 121870.487595417 486512.71203006, 121863.900623145 486546.136633659))"
# convert to sf and project on a projected coord system
area <- st_as_sfc(area, crs = 7415L)
# points with long/lat coords
pnts <-
data.frame(
id = 1:3,
long = c(4.85558, 4.89904, 4.91073),
lat = c(52.39707, 52.36612, 52.36255)
)
# convert to sf with the same crs
pnts_sf <- st_as_sf(pnts, crs = 7415L, coords = c("long", "lat"))
# check if crs are equal
all.equal(st_crs(pnts_sf),st_crs(area))
I am wondering why the following approaches do not give me the correct answer.
1.Simply using the st_distance fun-doesn't work, wrong answer
st_distance(pnts_sf, area)
2.In a mutate call - all wrong answers
pnts_sf %>%
mutate(
distance = st_distance(area, by_element = TRUE),
distance2 = st_distance(area, by_element = FALSE),
distance3 = st_distance(geometry, area, by_element = TRUE)
)
However this approach seems to work and gives correct distances.
3.map over the long/lat - works correctly
pnts_geoms <-
map2(
pnts$long,
pnts$lat,
~ st_sfc(st_point(c(.x, .y)) , crs = 4326L)
) %>%
map(st_transform, crs = 7415L)
map_dbl(pnts_geoms, st_distance, y = area)
I'm new to spatial data and I'm trying to learn the sf package so I'm wondering what is going wrong here. As far as i can tell, the first 2 approaches somehow end up considering the points "as a whole" (one of the points is inside the area polygon so i guess that's why one of the wrong answers is 0). The third approach is considering a point at a time which is my intention.
Any ideas how can i get the mutate call to work as well?
I'm on R 3.4.1 with
> packageVersion("dplyr")
[1] ‘0.7.3’
> packageVersion("sf")
[1] ‘0.5.5’
So it turns out that the whole confusion was caused by a small silly oversight on my part. Here's the breakdown:
The points dataframe comes from a different source (!) than the area polygon.
Overseeing this I kept trying to set them to crs 7415 which is a legal but incorrect move and led eventually to the wrong answers.
The right approach is to convert them to sf objects in the crs they originate from, transform them to the one the area object is in and then proceed to compute the distances.
Putting it all together:
# this part was wrong, crs was supposed to be the one they were
# originally coded in
pnts_sf <- st_as_sf(pnts, crs = 4326L, coords = c("long", "lat"))
# then apply the transformation to another crs
pnts_sf <- st_transform(pnts_sf, crs = 7415L)
st_distance(pnts_sf, area)
--------------------------
Units: m
[,1]
[1,] 3998.5701
[2,] 0.0000
[3,] 751.8097

Resources