Skip to content

Instantly share code, notes, and snippets.

@fzenoni
Last active November 2, 2023 11:06
Show Gist options
  • Save fzenoni/ef23faf6d1ada5e4a91c9ef23b0ba2c1 to your computer and use it in GitHub Desktop.
Save fzenoni/ef23faf6d1ada5e4a91c9ef23b0ba2c1 to your computer and use it in GitHub Desktop.
Preserve polygons in orthographic view
# Download earth data first
# https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/physical/ne_110m_land.zip
library(sf)
library(lwgeom)
library(dplyr)
library(ggplot2)
library(mapview)
# Read the data
mini_world <- read_sf('data/ne_110m_land/ne_110m_land.shp')
# Define the orthographic projection
# Choose lat_0 with -90 <= lat_0 <= 90 and lon_0 with -180 <= lon_0 <= 180
lat <- 45
lon <- 2
ortho <- paste0('+proj=ortho +lat_0=', lat, ' +lon_0=', lon, ' +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs')
# Define the polygon that will help you finding the "blade"
# to split what lies within and without your projection
circle <- st_point(x = c(0,0)) %>% st_buffer(dist = 6371000) %>% st_sfc(crs = ortho)
# Project this polygon in lat-lon
circle_longlat <- circle %>% st_transform(crs = 4326)
# circle_longlat cannot be used as it is
# You must decompose it into a string with ordered longitudes
# Then complete the polygon definition to cover the hemisphere
if(lat != 0) {
circle_longlat <- st_boundary(circle_longlat)
circle_coords <- st_coordinates(circle_longlat)[, c(1,2)]
circle_coords <- circle_coords[order(circle_coords[, 1]),]
circle_coords <- circle_coords[!duplicated(circle_coords),]
# Rebuild line
circle_longlat <- st_linestring(circle_coords) %>% st_sfc(crs = 4326)
if(lat > 0) {
rectangle <- list(rbind(circle_coords,
c(X = 180, circle_coords[nrow(circle_coords), 'Y']),
c(X = 180, Y = 90),
c(X = -180, Y = 90),
c(X = -180, circle_coords[1, 'Y']),
circle_coords[1, c('X','Y')])) %>%
st_polygon() %>% st_sfc(crs = 4326)
} else {
rectangle <- list(rbind(circle_coords,
c(X = 180, circle_coords[nrow(circle_coords), 'Y']),
c(X = 180, Y = -90),
c(X = -180, Y = -90),
c(X = -180, circle_coords[1, 'Y']),
circle_coords[1, c('X','Y')])) %>%
st_polygon() %>% st_sfc(crs = 4326)
}
circle_longlat <- st_union(st_make_valid(circle_longlat), st_make_valid(rectangle))
}
# This visualization shows the visible emisphere in red
ggplot() +
geom_sf(data = mini_world) +
geom_sf(data = circle_longlat, color = 'red', fill = 'red', alpha = 0.3)
# A small negative buffer is necessary to avoid polygons still disappearing in a few pathological cases
# I should not change the shapes too much
visible <- st_intersection(st_make_valid(mini_world), st_buffer(circle_longlat, -0.09)) %>%
st_transform(crs = ortho)
# DISCLAIMER: This section is the outcome of trial-and-error and I don't claim it is the best approach
# Resulting polygons are often broken and they need to be fixed
# Get reason why they're broken
broken_reason <- st_is_valid(visible, reason = TRUE)
# First fix NA's by decomposing them
# Remove them from visible for now
na_visible <- visible[is.na(broken_reason),]
visible <- visible[!is.na(broken_reason),]
# Open and close polygons
na_visible <- st_cast(na_visible, 'MULTILINESTRING') %>%
st_cast('LINESTRING', do_split=TRUE)
na_visible <- na_visible %>% mutate(npts = npts(geometry, by_feature = TRUE))
# Exclude polygons with less than 4 points
na_visible <- na_visible %>%
filter(npts >=4) %>%
select(-npts) %>%
st_cast('POLYGON')
# Fix other broken polygons
broken <- which(!st_is_valid(visible))
for(land in broken) {
result = tryCatch({
# visible[land,] <- st_buffer(visible[land,], 0) # Sometimes useful sometimes not
visible[land,] <- st_make_valid(visible[land,]) %>%
st_collection_extract()
}, error = function(e) {
visible[land,] <<- st_buffer(visible[land,], 0)
})
}
# Bind together the two tables
visible <- rbind(visible, na_visible)
# Final plot
ggplot() +
geom_sf(data = circle,
#fill = 'aliceblue') + # if you like the color
fill = NA) +
geom_sf(data=st_collection_extract(visible)) +
coord_sf(crs = ortho)
@AWSisco
Copy link

AWSisco commented May 31, 2019

Hi @fzenoni! Wow! I cannot thank you enough for sharing this! I won't get a chance to dig into it until next week, but I'll be sure to drop any questions and results here as soon as I do. Very much appreciated!

@csaybar
Copy link

csaybar commented Aug 27, 2019

Thanks for sharing @fzenoni, it is simply brilliant!

@rnuske
Copy link

rnuske commented Dec 9, 2019

Your plots in the sf issue look great! So I tried to reproduce them.

The first warning appeared after line 58:
although coordinates are longitude/latitude, st_union assumes that they are planar

The following plot looked as expected. But i wasn't able to create the object visible. Lines 67-68 resulted in the following error

dist is assumed to be in decimal degrees (arc_degrees).
although coordinates are longitude/latitude, st_intersection assumes that they are planar
Error in CPL_geos_op2(op, st_geometry(x), st_geometry(y)) : 
  Evaluation error: TopologyException: Input geom 0 is invalid: Ring Self-intersection at or near point -132.71000788443121 54.040009315423447 at -132.71000788443121 54.040009315423447.
In addition: Warning message:
In st_buffer.sfc(circle_longlat, -0.09) :
  st_buffer does not correctly buffer longitude/latitude data

I'm very grateful for any advice how to get this working.

Session info
> sessionInfo()
R version 3.6.1 (2019-07-05)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 18.04.3 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.7.1
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.7.1

locale:
 [1] LC_CTYPE=de_DE.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=de_DE.UTF-8        LC_COLLATE=de_DE.UTF-8    
 [5] LC_MONETARY=de_DE.UTF-8    LC_MESSAGES=de_DE.UTF-8   
 [7] LC_PAPER=de_DE.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=de_DE.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] mapview_2.7.0 ggplot2_3.2.1 dplyr_0.8.3   lwgeom_0.1-7  sf_0.8-0     

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.3         later_1.0.0        pillar_1.4.2       compiler_3.6.1    
 [5] base64enc_0.1-3    class_7.3-15       tools_3.6.1        digest_0.6.23     
 [9] viridisLite_0.3.0  satellite_1.0.2    lifecycle_0.1.0    tibble_2.1.3      
[13] gtable_0.3.0       lattice_0.20-38    png_0.1-7          pkgconfig_2.0.3   
[17] rlang_0.4.2        shiny_1.4.0        DBI_1.0.0          crosstalk_1.0.0   
[21] fastmap_1.0.1      e1071_1.7-3        raster_3.0-7       withr_2.1.2       
[25] htmlwidgets_1.5.1  webshot_0.5.2      stats4_3.6.1       classInt_0.4-2    
[29] leaflet_2.0.3      grid_3.6.1         tidyselect_0.2.5   glue_1.3.1        
[33] R6_2.4.1           sp_1.3-2           farver_2.0.1       purrr_0.3.3       
[37] magrittr_1.5       codetools_0.2-16   promises_1.1.0     scales_1.1.0      
[41] htmltools_0.4.0    units_0.6-5        assertthat_0.2.1   xtable_1.8-4      
[45] mime_0.7           colorspace_1.4-1   httpuv_1.5.2       KernSmooth_2.23-16
[49] lazyeval_0.2.2     munsell_0.5.0      crayon_1.3.4
Geo Libraries
> library(sf)
Linking to GEOS 3.8.0, GDAL 3.0.2, PROJ 6.2.1
> library(lwgeom)
Linking to liblwgeom 2.5.0dev r16016, GEOS 3.8.0, PROJ 6.2.1

@fzenoni
Copy link
Author

fzenoni commented Dec 9, 2019

Hi @rnuske, thank you for testing the script.

Don't worry about the warning, it is a standard print when you manipulate or intersect geometries in lat/lon coordinates. To avoid it, in principle one should:

  1. project to some planar CRS
  2. do the operation
  3. project back to lat/lon.

Unless you are looking after precise results (e.g. compute the centroid of a region with great accuracy), usually it is not worth it.

Concerning the error, I was able to reproduce it.
I have no explanation about why it used to work before, but I have now replaced line 67 with

visible <- st_intersection(st_make_valid(mini_world), st_buffer(circle_longlat, -0.09)) %>%

That should do it.

@rnuske
Copy link

rnuske commented Dec 9, 2019

Thanks for your very fast answer!
With the new line 67 it also works for me.

@rnuske
Copy link

rnuske commented Dec 9, 2019

just a short follow up question: Do you happen to know how to color the oceans?

@fzenoni
Copy link
Author

fzenoni commented Dec 17, 2019

I am very sorry I missed your message.
To display the color you may uncomment https://gist.github.com/fzenoni/ef23faf6d1ada5e4a91c9ef23b0ba2c1#file-ortho_view-r-L108 and comment https://gist.github.com/fzenoni/ef23faf6d1ada5e4a91c9ef23b0ba2c1#file-ortho_view-r-L109. That will completely hide the graticule, though.

To preserve (and control) the graticule, you need to use the ggplot2's theme() function.

This is inspired by the code I showed in the original Github issue.

ggplot() +
  geom_sf(data = circle,
          fill = 'aliceblue') +
  geom_sf(data=st_collection_extract(visible)) +
  coord_sf(crs = ortho) +
  theme(
    panel.grid.major = element_line(
      color = 'red', linetype = 2, size = 0.5),
    panel.ontop = TRUE,
    panel.background = element_rect(fill = NA)
  )

image

@fzenoni
Copy link
Author

fzenoni commented Dec 17, 2019

You might also want to play with the alpha level, so that the graticule goes below the land masses.
It's a quirky solution because one can't perfectly control the color of the sea anymore, nor the color of the graticule.

# Final plot
ggplot() +
  geom_sf(data = circle,
          fill = 'aliceblue', alpha=0.5)+
  geom_sf(data=st_collection_extract(visible), fill='darkgreen', color = 'black') +
  coord_sf(crs = ortho) +
  theme(
    panel.grid.major = element_line(
      color = 'red', linetype = 2, size = 0.5)
  )

image

@rnuske
Copy link

rnuske commented Dec 17, 2019

Thanks a lot for your suggestions @fzenoni. They helped a lot to get what I wanted. I should definitely learn more ggplot!

Did I understand correctly that the graticule can only be behind everything else (in the background) or in front of everything (in the foreground) but not between the layers?

@fzenoni
Copy link
Author

fzenoni commented Dec 17, 2019

I am glad it worked out for you!

Did I understand correctly that the graticule can only be behind everything else (in the background) or in front of everything (in the foreground) but not between the layers?

After a lot of trial and error, I believe this is correct. But it would be better to ask this question to the ggplot2 maintainers.

@jlguilherme
Copy link

This is brilliant! Thanks for sharing!

@rasmerla
Copy link

This is totally brilliant! Thanks a lot! Got this link trough a question a Stack exchange when I tried to solve it on my own.

I haven't really wrapped my head around the effects of using s2 geometry, but I did not get this script to work unless I set sf_use_s2(FALSE).

@fzenoni
Copy link
Author

fzenoni commented Oct 12, 2021

Thank you @rasmerla, I really appreciate that.

The script was written before sf 1.0, and since then my professional activities shifted a bit, so I haven't had the opportunity yet to adapt it to the new s2 dependency. This vignette gives me the feeling that things got easier though.

@rCarto
Copy link

rCarto commented Nov 2, 2023

Here is an example of ortho view using s2:
https://gist.github.com/rCarto/02dff288244691a53b3c7b55ee44ef86
It's probably not bulletproof though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment