I'm building a shapefile of states where Alaska and Hawaii are represented as being somewhere south of Texas, for ease of making an illustrative map. Using maptools package and some code from https://rud.is/b/2014/11/16/moving-the-earth-well-alaska-hawaii-with-r/, I have been able to do this with shapes from TIGER.
However, I am running into trouble now that I want to add cities to my map. Making my shapes into sp objects and then using maptools::elide works fine for Alaska before, but elide with scale doesn't work the same way on a collection of points so my cities wind up in the wrong place:
library(maptools)
library(sf)
library(tmap)
library(tidyverse)
library(tidygeocoder)
ak_city_sf <-
tribble(~city_name, ~city_search_string,
"Juneau", "Juneau, Alaska, United States",
"Anchorage", "Anchorage, Alaska, United States",
"Utqiagvik", "Utqiagvik, Alaska, United States",
"Scammon Bay", "Scammon Bay, Alaska, United States") %>%
geocode(city_search_string, method = 'osm', lat = latitude , long = longitude) %>%
st_as_sf(coords = c("longitude","latitude"))
st_crs(ak_city_sf) <- 4326
ak_city_sf <-
ak_city_sf %>%
st_transform(2163)
ak_state_sf <-
tigris::states(cb = T) %>%
filter(STUSPS == "AK") %>%
st_transform(2163)
# before transformation, everything looks fine...
tm_shape(ak_state_sf) +
tm_borders() +
tm_shape(ak_city_sf) +
tm_dots(size = .1) +
tm_text("city_name",
size = .5)
SCALE_FACTOR <- 10000
ak_state_sf_scaled <-
ak_state_sf %>%
as("Spatial") %>%
elide(scale = SCALE_FACTOR) %>%
st_as_sf()
st_crs(ak_state_sf_scaled) <- 2163
ak_city_sf_scaled <-
ak_city_sf %>%
as("Spatial") %>%
elide(scale = SCALE_FACTOR) %>%
st_as_sf()
st_crs(ak_city_sf_scaled) <- 2163
# after scaling, things don't look so good
tm_shape(ak_state_sf_scaled) +
tm_borders() +
tm_shape(ak_city_sf_scaled) +
tm_dots(size = .1) +
tm_text("city_name",
size = .5)
maptools::elide seems to be the best command for doing anything like this (even though it forces me to convert to an sp object). The documentation for scale doesn't mean much to me. (I don't think I can combine them in a single object because they are points for the cities and multipolygons for the state). How can I scale the points the same way I've scaled the state?
To scale or rotate two separate geometries so that they can be mapped together, it's necessary to define a centroid that both your state geometries and city geometries will be rotated or scaled around. This makes some logical sense, since scaling or rotating a collection of points (in this case representing cities) can't happen unless you define (implicitly or explicitly) the center of the scaling or rotating.
Once you've defined a common centroid (in my case, I just used the centroid of the states I was transforming), you can use the affine transformations shown here:
https://geocompr.robinlovelace.net/geometric-operations.html#affine-transformations.
Related
Shouldn't the area of polygons remain the same when you transform the shapefile projection? I am currently working with the shapefile here (https://data.humdata.org/dataset/f814a950-4d4e-4f46-a880-4da5522f14c4/resource/731e11cb-be02-46cf-8347-db0a07abff4e/download/gin_admbnda_adm4_2021_ocha.zip) for the country GIN which I call gin_shp after I read it in with the st_read function in R as follows:
gin_shp <- st_read(dsn = "INPUT FOLDER", layer = "sous_prefectures")
gin_shp ##this shows the crs is WGS84 obviously in degrees
##compute the area of the multipolygon geometries
gin_shp$area <- sf::st_area(gin_shp) ##computes areas in m^2
gin_shp$area <- units::set_units(gin_shp$area, "km^2") ##converting the area to square kms
sum(gin_shp$area) ##compute total area of the entire country (google seems to agree!)
245084.2 [km^2]
I need to create 1sqkm grids (using sf::st_make_grid) so I tried to transform crs projection from degrees to UTM by calling the st_transform function as follows:
crs_dt <- rgdal::make_EPSG() ##first load the dataframe of CRS projections available in R
gin_shp <- sf::st_transform(gin_shp, crs = crs_dt$prj4[crs_dt$code == 4328]) ##select one and assign it to gin_shp
#now I try to compute total area under new projection regime by running the same code as before
gin_shp$area <- sf::st_area(gin_shp) ##computes areas in m^2
gin_shp$area <- units::set_units(gin_shp$area, "km^2") #converting the area to square kms
#compute total area of the entire country
sum(gin_shp$area)
44363.83 [km^2] (way way off)
Do you know why this might be happening? Any ideas how to fix it?
No, the area of polygons is not guaranteed to remain constant when transforming between different CRSes. The Earth is round, maps & computer screens are flat - something has to give; either area or shape has to be distorted somewhat.
There are some area preserving projections - such as the Mollweide - but these are more of exception than rule.
For an exaggerated example consider the world dataset, taken from {giscoR} package. Greenland (close to the pole) and Congo (on equator) have roughly the same area on sphere (calculated on WGS84 using spherical geometry tools) but wildly different one when projected. Especially when projected to Mercator and its derivatives (e.g. Web Mercator as used by Google maps).
library(sf)
library(dplyr)
library(giscoR)
library(ggplot2)
# the world in, 1: 20M
svet <- gisco_get_countries(resolution = "20")
# Greenland & Congo - to be drawn in red
glmd <- svet %>%
filter(CNTR_ID %in% c('GL', 'CD'))
# web mercator = default for google maps and other web based tools
ggplot() +
geom_sf(data = glmd, fill = "red", color = NA) +
geom_sf(data = svet, fill = NA, color = "gray45") +
coord_sf(crs = st_crs("EPSG:3857"),
ylim = c(-20e6, 20e6))
# Mollweide - equal area projection
ggplot() +
geom_sf(data = glmd, fill = "red", color = NA) +
geom_sf(data = svet, fill = NA, color = "gray45") +
coord_sf(crs = st_crs("ESRI:53009"))
First time posting on SO
I have a shapefile that has the geometries for each Zipcode along with state name. I want to figure out which zipcodes lie on the state borders.
The way I figured to achieve this is by combining all zipcodes for each state and leading to the geometry for a state and then finding the neighboring zipcodes for each state.
I combined the zipcodes into states using:
state_shape <- shapefile %>% group_by(State) %>% summarise(geometry = sf::st_union(geometry))
But then when I try to find the neighboring zipcodes using poly2nb
state_nb <- poly2nb(st_geometry(state_shape))
It gives me an Error:
Error in poly2nb(st_geometry(state_shape)) : Polygon geometries required
I understand to find the border zipcodes I will have to pass the zipcode geometries in poly2nb, but the error persists.
Any help will be highly appreciated, also any other approaches to this problem are more than welcome.
Consider this example, built on the widely available North Carolina shapefile that is distributed with {sf} package.
What the example does is:
creates a border line of North Carolina by first dissolving the counties, and then casting the resulting multipolygon to a multilinestring
runs sf::st_touches() on the counties and borderline with sparse set to false; the result is a logical vector that can be used to subset the original shapefile (filtering out the counties that share a border with the NC border)
presents the results in a graphical format, using {ggplot2}; the bordering counties are blue and the rest just blank for context
library(sf)
library(dplyr)
library(ggplot2)
# all NC counties (from shapefile distributed with {sf})
shape <- st_read(system.file("shape/nc.shp", package="sf"))
# border, via dplyr::summarise() & cast as a linestring
border <- shape %>%
summarise() %>%
st_cast("MULTILINESTRING")
# logical vector of length nrow(shape)
neighbours <- sf::st_touches(shape,
border,
sparse = F)
# report results
ggplot() +
geom_sf(data = shape[neighbours, ], fill = "blue") + # border counties
geom_sf(data = shape, fill = NA, color = "grey45") # all counties for context
I would like to create evenly spaced polylines going North to South with 50 mile spacing between each line and 10 miles long. Not sure if this is possible using sf package. In the example below, I would like to have the lines filling the counties across the state of Washington.
library(tigris)
library(leaflet)
states <- states(cb = TRUE)
counties<-counties(cb=TRUE)
counties<- counties%>%filter(STATEFP==53)
states<- states%>%filter(NAME=="Washington")
leaflet(states) %>%
addProviderTiles("CartoDB.Positron") %>%
addPolygons(fillColor = "white",
color = "black",
weight = 0.5) %>%
addPolygons(data=counties,color='red',fillColor = 'white')%>%
setView(-120.5, 47.3, zoom=8)
I've updated to include an image of what I'd like to do below.
You can create a multilinestring sf object from scratch by specifying coordinates.
You can get these coordinates from the extent (bounding box) of Washington, but you may also be interested in knowing how to create a grid, which I will demonstrate below because it may be helpful.
Copy and paste this reproducible example:
library(tidyverse)
library(tigris)
library(leaflet)
library(sf)
library(raster)
states <- states(cb = TRUE)
# subset for WA and transform to a meter-based CRS
states <- states %>%
filter(NAME == "Washington") %>%
st_transform(crs = 3857) # Mercator
# fifty miles in meters
fm <- 80467.2
# subset for Washington
states_sp <- as(states, "Spatial")
# create a grid, convert it to polygons to plot
grid <- raster(extent(states_sp),
resolution = c(fm, fm),
crs = proj4string(states_sp))
grid <- rasterToPolygons(grid)
plot(states_sp)
plot(grid, add = TRUE)
# find the top y coordinate and calculate 50 mile intervals moving south
ty <- extent(grid)[4] # y coordinate along northern WA edge
ty <- ty - (fm * 0:7) # y coordinates moving south at 10 mile intervals
# create a list of sf linestring objects
l <- vector("list", length(ty))
for(i in seq_along(l)){
l[[i]] <-
st_linestring(
rbind(
c(extent(grid)[1], ty[i]),
c(extent(grid)[2], ty[i])
)
)
}
# create the multilinestring, which expects a list of linestrings
ml <- st_multilinestring(l)
plot(states_sp)
plot(as(ml, "Spatial"), add = TRUE, col = "red")
As you can see, I switch back and forth between sf and sp objects using the functions as(sf_object, "Spatial") and st_as_sf(sp_object). Use these to transform the data to your needs.
I have a shapefile, http://census.cso.ie/censusasp/saps/boundaries/Census2011_Small_Areas_generalised20m.zip
and want to extract the long/lat, but I am not sure how to map the correct coordinate to the correct small area.
mycode is:
require(ggplot2)
require(proj4)
require(rgdal)
a=readOGR(....shp)
dublin = a[a$NUS3NAME=='Dublin',]
dublin=spTransform(dublin,CRS('=proj=longlat +ellps=WGS84 +datum=WGS84'))
b=data.frame(dublin)
sa=fortify(dublin,SA='SMALL_AREA')
pj=project(sa[,1:2],proj4string(dublin),inverse=TRUE)
latlon=data.frame(latdeg=pj$y,londeg=pj$x)
sa=data.frame(cbind(latlon,sa)
The number of unique sa$id (4500) is the same as the number of unique b$SMALL_AREA (4500 rows). How is (for example) and id of 22 mapped from sa to the correct small area in b?
there are 56k rows in sa and 4500 rows in b
Any suggestions are appreciated
I am working in R
Shapefiles are much easier to work with and understand using the sf package in R. It keeps things tidy and rectangular, with the added $geometry list-column.
For your example, getting the lat & lon for the Dublin area:
library(sf)
library(tidyverse)
a <- read_sf('Census2011_Small_Areas_generalised20m/Census2011_Small_Areas_generalised20m.shp')
# dplyr filter() works for sf objects
dublin <- a %>% filter(NUTS3NAME == 'Dublin')
# Tranform to WGS84 coordinates
dublin <- dublin %>% st_transform(st_crs(4326))
# Proof CRS has changed
st_crs(dublin)
# lat/lon coords
st_coordinates(dublin) %>% head()
In this case, the sf geometry is of MULTIPOLYGON type. Each observation has between 4 and 168 connected lat/lon points associated with it. If you are interested in a single point for each observation, the centroid might be a good approximation.
Using dublin %>% st_centroid() will return all the data, but with the $geometry column consisting of a single point. Getting just the centroid points (as a matrix) can be achieved using dublin %>% st_centroid %>% st_coordinates().
Finally, a plot of the Dublin subset of the shapefile & the respective centroid points. There are quite a few shapes in a small area, making things hard to see. In the outskirts with larger polygons the centroids should be more visible.
dublin %>%
st_centroid() %>%
ggplot() +
geom_sf( size = .4, color = '#FF7900') +
geom_sf(data = dublin,
color = '#009A49',
fill = NA,
size = .2) +
theme(panel.background = element_rect(fill = "black")) +
coord_sf(datum = NA)
I am trying to measure the length of a coastline. This will be used as a metric for position along a coastline in an analysis. For example, imagine I have data on the location of all public beaches in a region, and I want to describe how they are distributed in space by measuring their distance from a reference point on the coast.
I have followed this extremely helpful tutorial on calculating the length of a coastline using rulers of different lengths. However, it is only accurate if you want to measure the entire polygon length, that is, the geographic object you are interested in is an island.
To get a shapefile of a coastline (note that in the ne_countries call I am using a coarse scale on purpose, to make the coastline smoother, and only keeping the first shape returned--the "scalerank" name is not important):
library(raster)
library(sf)
library(rnaturalearth)
basemap <- rnaturalearth::ne_countries(scale = 110, country = "united states of america", returnclass = "sf")[1]
bbox <- extent(-82, -65, 27, 35)
cropmap <- st_crop(basemap, bbox)
plot(cropmap)
This returns a shape showing the South Atlantic coast down into Florida. However, if I measure the length of this shape, it will include all sides of the polygon--not just the coastline. How do I isolate the coastline (see below for a map of what part of the polygon is actually coastal) and just measure its length in R?
ggplot() +
geom_sf(data=basemap) +
geom_sf(data=cropmap, color="blue", fill="blue")
Here's a version similar to Robert's, staying in sf. I converted the original basemap from polygons to lines and then "cut out" the bbox like you did. Then you can just measure with st_length.
library(sf)
library(rnaturalearth)
basemap <- rnaturalearth::ne_countries(scale = 110, country = "united states of america", returnclass = "sf")[1]
Convert from polygon to lines to avoid the parts of the polygon you don't want
basemap_lines <- basemap %>% st_cast("MULTILINESTRING")
plot(basemap_lines)
Basemap as multilinestring
Then create the cookie cutter polygon, setting the crs to lat/lon
xmin <- -82
xmax <- -65
ymin <- 27
ymax <- 35
bbox <- st_polygon(list(rbind(c(xmin,ymin), c(xmin,ymax), c(xmax,ymax), c(xmax,ymin),c(xmin,ymin)))) %>% st_sfc()
st_crs(bbox) <- 4326
Then just crop with st_intersection
crop_lines <- st_intersection(basemap_lines, bbox)
plot(crop_lines)
st_length(crop_lines)
Cropped line
For your bbox dimensions I get 1162849 meters (~723 miles), which aligns with the other answer.
It is pretty easy if you transform the polygons to lines before the cropping
Example data
library(raster)
library(rnaturalearth)
m <- rnaturalearth::ne_countries(scale = 110, country = "united states of america", returnclass = "sp")
ext <- extent(-82, -65, 27, 35)
Transform and crop
m <- as(m, "SpatialLines")
croplines <- crop(m, ext)
Because the CRS is longitude/latitude, use geosphere, not rgeos, to compute the length
library(geosphere)
lengthLine(croplines)
#[1] 1162849
(i.e. 1162.8 km)
In some cases you may need intersect with a polygon instead of an extent (see raster::drawPoly and raster::intersect) or perhaps use crop, disaggregate, and visually select to get the lines you want.