While going through my blog on Vislauzaing Plane Crash Data , one of my friends asked me if to try and plot the Flight Routes of Crashed Planes on a 3-D view of Earth.
As interesting as it sounds, this was certainly an excellent suggestion and I started exploring web to find out some R package which could help me achieve this feat. Finally I found the package threejs which could do this easily.
Let us see how it can be done .
#
# As always, install and load the requisite packages first
#
#
#
list.of.packages <-
c("geosphere", # For spatial methods
"threejs", # threejs is used for 3-D interactive Earth Visualization
"rworldmap", # For creating earth map
"leaflet", # Leaflet for R provides functions to control and integrate Leaflet, a JavaScript library for interactive maps, within R.
"rgeos", # Provides functions for handling operations on topologies.
"raster", # For raster image
"DT", # For creating interactive tables
"ggplot2",
"sp" , # For Spatial processing of data
"ggmap", # To reverse geocode Long/Lat
"knitr", # TO enable 3-D visualization embedding in the HTML page
"rglwidget",
"rgl"
)
#
#You should be able to simply reuse the following lines of code as is
#
new.packages <- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])]
#
if(length(new.packages)) suppressMessages(suppressWarnings(install.packages(new.packages)))
#
# By now we have installed the requisite packages. Time to load them .
#
lapply(list.of.packages,function(x){suppressMessages(suppressWarnings(library(x,character.only=TRUE)))})
## [[1]]
## [1] "geosphere" "sp" "stats" "graphics" "grDevices" "utils"
## [7] "datasets" "methods" "base"
##
## [[2]]
## [1] "threejs" "geosphere" "sp" "stats" "graphics"
## [6] "grDevices" "utils" "datasets" "methods" "base"
##
## [[3]]
## [1] "rworldmap" "threejs" "geosphere" "sp" "stats"
## [6] "graphics" "grDevices" "utils" "datasets" "methods"
## [11] "base"
##
## [[4]]
## [1] "leaflet" "rworldmap" "threejs" "geosphere" "sp"
## [6] "stats" "graphics" "grDevices" "utils" "datasets"
## [11] "methods" "base"
##
## [[5]]
## [1] "rgeos" "leaflet" "rworldmap" "threejs" "geosphere"
## [6] "sp" "stats" "graphics" "grDevices" "utils"
## [11] "datasets" "methods" "base"
##
## [[6]]
## [1] "raster" "rgeos" "leaflet" "rworldmap" "threejs"
## [6] "geosphere" "sp" "stats" "graphics" "grDevices"
## [11] "utils" "datasets" "methods" "base"
##
## [[7]]
## [1] "DT" "raster" "rgeos" "leaflet" "rworldmap"
## [6] "threejs" "geosphere" "sp" "stats" "graphics"
## [11] "grDevices" "utils" "datasets" "methods" "base"
##
## [[8]]
## [1] "ggplot2" "DT" "raster" "rgeos" "leaflet"
## [6] "rworldmap" "threejs" "geosphere" "sp" "stats"
## [11] "graphics" "grDevices" "utils" "datasets" "methods"
## [16] "base"
##
## [[9]]
## [1] "ggplot2" "DT" "raster" "rgeos" "leaflet"
## [6] "rworldmap" "threejs" "geosphere" "sp" "stats"
## [11] "graphics" "grDevices" "utils" "datasets" "methods"
## [16] "base"
##
## [[10]]
## [1] "ggmap" "ggplot2" "DT" "raster" "rgeos"
## [6] "leaflet" "rworldmap" "threejs" "geosphere" "sp"
## [11] "stats" "graphics" "grDevices" "utils" "datasets"
## [16] "methods" "base"
##
## [[11]]
## [1] "knitr" "ggmap" "ggplot2" "DT" "raster"
## [6] "rgeos" "leaflet" "rworldmap" "threejs" "geosphere"
## [11] "sp" "stats" "graphics" "grDevices" "utils"
## [16] "datasets" "methods" "base"
##
## [[12]]
## [1] "rglwidget" "knitr" "ggmap" "ggplot2" "DT"
## [6] "raster" "rgeos" "leaflet" "rworldmap" "threejs"
## [11] "geosphere" "sp" "stats" "graphics" "grDevices"
## [16] "utils" "datasets" "methods" "base"
##
## [[13]]
## [1] "rgl" "rglwidget" "knitr" "ggmap" "ggplot2"
## [6] "DT" "raster" "rgeos" "leaflet" "rworldmap"
## [11] "threejs" "geosphere" "sp" "stats" "graphics"
## [16] "grDevices" "utils" "datasets" "methods" "base"
#
# Set this property to enable 3-D visualization
#
knit_hooks$set(webgl = hook_webgl)
Based on the source and destination of a flight from the flights data (Refer my BLOG for details around plane crash data), I gecoded the Source and Destination addresses using Google Geocoding API.
I will load the geocoded addresses for source and destination (limiting it to 1000 addresses to avoid cluttering of paths) and will visualize the paths and source and destination location using various spatial methods inclusing the 3-D interactive visualization of paths.
#
# Read the file
#
input_data<-read.csv("C:\\PB Backup\\R\\Great_Circles.csv", header = TRUE, stringsAsFactors = FALSE)
#
str(input_data)
## 'data.frame': 999 obs. of 4 variables:
## $ SourceLong: num -0.236 5.93 1.025 -78.429 2.341 ...
## $ SourceLat : num 44 43.1 51.1 44.5 48.9 ...
## $ DestLong : num 2.34 24.06 4.48 -90.2 14.43 ...
## $ DestLat : num 48.9 -26.6 51.9 38.6 50.1 ...
#
#
head(input_data)
## SourceLong SourceLat DestLong DestLat
## 1 -0.235721 43.98616 2.34120 48.85693
## 2 5.930290 43.12534 24.06189 -26.61898
## 3 1.025000 51.07486 4.47846 51.92285
## 4 -78.429441 44.53110 -90.19956 38.62775
## 5 2.341200 48.85693 14.43322 50.07908
## 6 -75.610702 42.93709 2.34120 48.85693
#
#
# Let us use the function datatable that creates an HTML widget to display rectangular data (a matrix or data frame) using JavaScript using package DT
#
datatable(input_data, rownames = FALSE)
#
#
#
While the purpose of the blog is to create a 3-D visualization of Flight routes, let us do some basic Spatial Data processing to get some understanding of Spatial Analytics in R
#
# Converting Data to spatial objects
#
source_df<-data.frame(SourceLong=input_data$SourceLong,SourceLat=input_data$SourceLat)
#
# Create object of class SpatialPoints using SpatialPoints function from sp package
#
source_sp<-SpatialPoints(source_df, proj4string=CRS("+proj=longlat"))
#
#
str(source_sp)
## Formal class 'SpatialPoints' [package "sp"] with 3 slots
## ..@ coords : num [1:999, 1:2] -0.236 5.93 1.025 -78.429 2.341 ...
## .. ..- attr(*, "dimnames")=List of 2
## .. .. ..$ : NULL
## .. .. ..$ : chr [1:2] "SourceLong" "SourceLat"
## ..@ bbox : num [1:2, 1:2] -162.7 -54.8 175 69.7
## .. ..- attr(*, "dimnames")=List of 2
## .. .. ..$ : chr [1:2] "SourceLong" "SourceLat"
## .. .. ..$ : chr [1:2] "min" "max"
## ..@ proj4string:Formal class 'CRS' [package "sp"] with 1 slot
## .. .. ..@ projargs: chr "+proj=longlat"
#
head(source_sp)
## class : SpatialPoints
## features : 1
## extent : -0.235721, -0.235721, 43.98616, 43.98616 (xmin, xmax, ymin, ymax)
## coord. ref. : +proj=longlat
#
# Convert to Spatial Dataframe
#
source_spdf <- SpatialPointsDataFrame(source_sp, data = source_df)
#
str(source_spdf)
## Formal class 'SpatialPointsDataFrame' [package "sp"] with 5 slots
## ..@ data :'data.frame': 999 obs. of 2 variables:
## .. ..$ SourceLong: num [1:999] -0.236 5.93 1.025 -78.429 2.341 ...
## .. ..$ SourceLat : num [1:999] 44 43.1 51.1 44.5 48.9 ...
## ..@ coords.nrs : num(0)
## ..@ coords : num [1:999, 1:2] -0.236 5.93 1.025 -78.429 2.341 ...
## .. ..- attr(*, "dimnames")=List of 2
## .. .. ..$ : NULL
## .. .. ..$ : chr [1:2] "SourceLong" "SourceLat"
## ..@ bbox : num [1:2, 1:2] -162.7 -54.8 175 69.7
## .. ..- attr(*, "dimnames")=List of 2
## .. .. ..$ : chr [1:2] "SourceLong" "SourceLat"
## .. .. ..$ : chr [1:2] "min" "max"
## ..@ proj4string:Formal class 'CRS' [package "sp"] with 1 slot
## .. .. ..@ projargs: chr "+proj=longlat"
#
head(source_spdf)
## SourceLong SourceLat
## 1 -0.235721 43.98616
## 2 5.930290 43.12534
## 3 1.025000 51.07486
## 4 -78.429441 44.53110
## 5 2.341200 48.85693
## 6 -75.610702 42.93709
#
# Similarly for the Destination Locations
# Converting Data to spatial objects
#
dest_df<-data.frame(DestLong=input_data$DestLong,DestLat=input_data$DestLat)
#
# Create object of class SpatialPoints using SpatialPoints function from sp package
#
dest_sp<-SpatialPoints(dest_df, proj4string=CRS("+proj=longlat"))
#
# Convert to Spatial Dataframe
#
dest_spdf <- SpatialPointsDataFrame(dest_sp, data = dest_df)
I’ll create the distance and bearing of each source location from the respective destination location using the distHaversine and bearing functions from geosphere package.
Bearing: A bearing is the angular difference away from a north or south baseline, ranging from 0° to 90°. Determining your bearing begins with a north or south baseline, whichever is closer to your direction. The angle is then measured east or west from the baseline.
#
# Create a Combined data frame for display purpose only
#
comb_df<-data.frame(input_data)
#
# Calculate distance between Source and Destination
#
comb_df$distance<-distHaversine(source_sp,dest_sp)
#
comb_df$bearing<-bearing(dest_sp,source_sp)
#
# Display the combined dataframe using interactive table
#
datatable(comb_df, rownames = FALSE)
Let us prepare a simple plot of Source and Destination location
#
# Plot source points
#
plot(comb_df[1:2],col= "red", pch = 3,xlab="",ylab="")
#
# Add Destination points
#
points(comb_df[3:4],col="blue")
USA (primarily East Coast) and Europe are seemingly worst hit by plane crashes as appear from the graph.
A better visual can be created using rworldmap package and ggplot2 package
#
(worldMap <- getMap() ) # rworldmap function to get Long/Lat Polygon data for all countries
## class : SpatialPolygonsDataFrame
## features : 243
## extent : -180, 180, -90, 83.64513 (xmin, xmax, ymin, ymax)
## coord. ref. : +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs
## variables : 49
## names : ScaleRank, LabelRank, FeatureCla, SOVEREIGNT, SOV_A3, ADM0_DIF, LEVEL, TYPE, ADMIN, ADM0_A3, GEOU_DIF, GEOUNIT, GU_A3, SU_DIF, SUBUNIT, ...
## min values : 1, 1, Adm-0 country, Afghanistan, AFG, 0, 2, Country, Afghanistan, ABW, 0, Afghanistan, ABW, 0, Afghanistan, ...
## max values : 6, 6, Admin-0 countries, Zimbabwe, ZWE, 1, 2, Sovereign country, Zimbabwe, ZWE, 1, Zimbabwe, ZWE, 0, Zimbabwe, ...
#
world.points <- fortify(worldMap) # Convert data into dataframe using fortify from ggplot2
## Regions defined for each Polygons
#
#
head(world.points)
## long lat order hole piece id group
## 1 61.21082 35.65007 1 FALSE 1 Afghanistan Afghanistan.1
## 2 62.23065 35.27066 2 FALSE 1 Afghanistan Afghanistan.1
## 3 62.98466 35.40404 3 FALSE 1 Afghanistan Afghanistan.1
## 4 63.19354 35.85717 4 FALSE 1 Afghanistan Afghanistan.1
## 5 63.98290 36.00796 5 FALSE 1 Afghanistan Afghanistan.1
## 6 64.54648 36.31207 6 FALSE 1 Afghanistan Afghanistan.1
#
world.points$region <- world.points$id
#
world.df <- world.points[,c("long","lat","group", "region")]
#
worldmap <- ggplot() +
geom_polygon(data = world.df, aes(x = long, y = lat, group = group)) +
geom_point(aes(x=comb_df[,1], y=comb_df[,2]),color="yellow", size=1) + # Plot Source Locations
geom_point(aes(x=comb_df[,3], y=comb_df[,4]),color="green", size=1) + # Plot Dest Location
scale_y_continuous(breaks = (-2:2) * 30) +
scale_x_continuous(breaks = (-4:4) * 45) +
theme_bw() +
theme(axis.line = element_line(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_blank())
#
worldmap
By adding the coord_map line to the ggplot call we can transform the map projection to a globe.
worldmap +coord_map("ortho", orientation=c(40, -40, -10))+theme(panel.background = element_rect(fill = 'gray', colour = 'white'), panel.grid.major = element_line(color = "white"),
panel.grid.minor = element_line(color = "white"))
By changing the parameters of the coord_map function we can change the map projection. Lots of information about map projections can be found here.
The azimuthal equidistant projection is of particular interest to this task as it can include the whole world and correctly shows distances and directions. Great circles (shortest paths between two points on a globe) will apear as straight lines on this projection.
Here I plot the sample points on an azimuthal equidistant projection with the North Pole at the centre of the map. (To avoid a polygon rendering difficulty with this particular projection I’ve removed the Antartica polygon)
worldmap <- ggplot() +
geom_polygon(data = world.df[world.df$region != 'Antarctica',], aes(x = long, y = lat, group = group)) +
geom_point(aes(x=comb_df[,1], y=comb_df[,2]),color="yellow", size=1) + # Plot Source Locations
geom_point(aes(x=comb_df[,3], y=comb_df[,4]),color="green", size=1) + # Plot Dest Location
scale_y_continuous(breaks = (-2:2) * 30) +
scale_x_continuous(breaks = (-4:4) * 45) +
coord_map("azequidist",orientation=c(90, 0, 0))+
theme(panel.background = element_rect(fill = 'azure3'))
#
worldmap
Let us reverse geocode the Lat/Long information of these 50 most frequent source locations to get the name of the cities using revgeocode function from ggmap package that calls Google API for reverse-geocoding
# Let us also plot top 50 such Source locations from where the flights crahsed frequently
#
# Approximate locations as factors rounding off Long / Lat to 2 decimal places
#
source_da <- factor(sprintf("%.2f:%.2f",comb_df[,2], comb_df[,1]))
#
str(source_da)
## Factor w/ 490 levels "-0.10:34.77",..: 343 328 413 350 382 326 25 427 54 213 ...
#
head(source_da)
## [1] 43.99:-0.24 43.13:5.93 51.07:1.02 44.53:-78.43 48.86:2.34
## [6] 42.94:-75.61
## 490 Levels: -0.10:34.77 -0.23:-78.52 -0.76:122.98 ... 9.74:-84.08
#
# A table of Source frequencies
#
freq <- sort(table(source_da), decreasing=TRUE)
#
# The most frequent source airports in these data, possibly airports like NYC, London, Paris ?
#
frequent_destinations <- names(freq)[1:50]
#
# Subset the flight data by source frequency
#
idx <- source_da %in% frequent_destinations
#
# Get the Long Lat data for source airports having most crash frequency
#
LongLat <- unique(comb_df[idx,1:2])
#
frequent_flights <-comb_df[idx,]
#
str(LongLat)
## 'data.frame': 50 obs. of 2 variables:
## $ SourceLong: num 2.34 -75.61 13.38 -110.98 -1.26 ...
## $ SourceLat : num 48.9 42.9 52.5 29.1 54.5 ...
#
head(LongLat)
## SourceLong SourceLat
## 5 2.341200 48.85693
## 6 -75.610702 42.93709
## 8 13.376980 52.51607
## 10 -110.984001 29.12122
## 12 -1.264001 54.54761
## 16 -118.243679 34.05223
#
# Call Reverse Geocoding function
#
result <- suppressMessages(suppressWarnings(do.call(rbind,
lapply(1:nrow(LongLat),
function(i)revgeocode(as.numeric(LongLat[i,1:2])))) ))
#
# Let us see the output
#
datatable(subset(result,result != 'NA'))
#
Now we will have some fun with a 3d interactive globe of Flight Route locations using the threejs library to render a 3d globe.
We will Plot frequent source airports (top 50 calculated above) as bars, and their flights routes as arcs. Adjust arc width and color by frequency.
#
#
(earth <- system.file("images/world.jpg", package="threejs")) # Get the image of globe
## [1] "C:/Users/ankitagarwal5/Documents/R/win-library/3.2/threejs/images/world.jpg"
#
# 3-D visual Dataframe
#
test_df <- data.frame(origin_lat = frequent_flights[,2], origin_long = frequent_flights[,1], dest_lat = frequent_flights[,4], dest_long = frequent_flights[,3])
#
globejs(img=earth, lat=LongLat[,2], long=LongLat[,1], arcs=test_df,
arcsHeight=0.3, arcsLwd=2, arcsColor="#ffff00", arcsOpacity=0.15,
atmosphere=TRUE, height=800, width = 800)
You can click on the Globe and drag it to rotate it. Try it out.
We can plot paths of all the Flight Routes (1000 that we have) as well. Let us see how it looks
#
#
(earth <- system.file("images/world.jpg", package="threejs")) # Get the image of globe
## [1] "C:/Users/ankitagarwal5/Documents/R/win-library/3.2/threejs/images/world.jpg"
#
# 3-D visual Dataframe
#
test_df <- data.frame(origin_lat = comb_df[,2], origin_long = comb_df[,1], dest_lat = comb_df[,4], dest_long = comb_df[,3])
#
globejs(img=earth, lat=LongLat[,2], long=LongLat[,1], arcs=test_df,
arcsHeight=0.3, arcsLwd=2, arcsColor="red", arcsOpacity=0.15,
atmosphere=TRUE,bg="white", height = 800 , width = 800)
You can click on the Globe and drag it to rotate it. Try it out.
Hope you have enjoyed this 3-D visualization of Globe.