Building an earthquake map with RethinkDB and GeoJSON
RethinkDB 1.15 introduced new geospatial features that can help you plot a course for smarter location-based applications. The database has new geographical types, including points, lines, and polygons. Geospatial queries makes it easy to compute the distance between points, detect intersecting regions, and more. RethinkDB stores geographical types in a format that conforms with the GeoJSON standard.
Developers can take advantage of the new geospatial support to simplify the development of a wide range of potential applications, from location-aware mobile experiences to specialized GIS research platforms. This tutorial demonstrates how to build an earthquake map using RethinkDB’s new geospatial support and an open data feed hosted by the USGS.
Fetch and process the earthquake data
The USGS publishes a global feed that includes data about every earthquake detected over the past 30 days. The feed is updated with the latest earthquakes every 15 minutes. This tutorial uses a version of the feed that only includes earthquakes that have a magnitude of 2.5 or higher.
In the RethinkDB administrative console, use the r.http
command to fetch the
data:
r.http("http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_month.geojson")
The feed includes an array of geographical points that represent earthquake epicenters. Each point comes with additional metadata, such as the magnitude and time of the associated seismic event. You can see a sample earthquake record below:
{
id: "ak11383733",
type: "Feature",
properties: {
mag: 3.3,
place: "152km NNE of Cape Yakataga, Alaska",
time: 1410213468000,
updated: 1410215418958,
...
},
geometry: {
type: "Point",
coordinates: [-141.1103, 61.2728, 6.7]
}
}
The next step is transforming the data and inserting it into a table. In cases
where you have raw GeoJSON data, you can typically just wrap it with the
r.geojson
command to convert it into native geographical types. The USGS
earthquake data, however, uses a non-standard triple value for coordinates,
which isn’t supported by RethinkDB. In such cases, or in situations where you
have coordinates that are not in standard GeoJSON notation, you will typically
use commands like r.point
and r.polygon
to create geographical types.
Using the merge
command, you can iterate over earthquake records from the
USGS feed and replace the value of the geometry
property with an actual point
object. The output of the merge
command can be passed directly to the
insert
command on the table where you want to store the data:
r.table("quakes").insert(
r.http("earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_month.geojson")("features")
.merge(function(quake) {
return {
geometry: r.point(
quake("geometry")("coordinates")(0),
quake("geometry")("coordinates")(1))
}
})
)
The r.point
command takes longitude as the first parameter and latitude as
the second parameter, just like GeoJSON coordinate arrays. In the example
above, the r.point
command is passed the coordinate values from the
earthquake object’s geometry
property.
As you can see, it’s easy to load content from remote data sources into RethinkDB. You can even use the query language to perform relatively sophisticated data transformations on the fetched data before inserting it into a table.
Perform geospatial queries
The next step is to create an index on the geometry
property. Use the
indexCreate
command with the geo
option to create an index that supports
geospatial queries:
r.table("quakes").indexCreate("geometry", {geo: true})
Now that there is an index, try querying the data. For the first query, try fetching a list of all the earthquakes that took place within 200 miles of Tokyo:
r.table('quakes').getIntersecting(
r.circle([139.69, 35.68], 200,
{unit: "mi"}), {index: "geometry"})
In the example above, the getIntersecting
command will find all of the
records in the quakes
table that have a geographic object stored in the
geometry
property that intersects with the specified circle. The r.circle
command creates a polygon that approximates a circle with the desired radius
and center point. The unit
option tells the r.circle
command to use a
particular unit of measurement (miles, in this case) to compute the radius. The
coordinates used in the above example correspond with the latitude and
longitude of Tokyo.
Let’s say that you wanted to get the largest earthquake for each individual
day. To organize the earthquakes by day, use the group
command on the date.
To get the largest from each day, you can chain the max
command and have it
operate on the magnitude property.
r.table("quakes").group(r.epochTime(
r.row("properties")("time").div(1000)).date())
.max(r.row("properties")("mag"))
The USGS data uses timestamps that are counted in milliseconds since the UNIX
epoch. In the query above, div(1000)
is used to normalize the value so that
it can be interpreted by the r.epochTime
command. It’s also worth noting that
commands chained after a group
operation will automatically be performed on
the contents of each individual group.
Build a simple API backend
The earthquake map application has a simple backend built with node.js and
Express. It implements several API endpoints that client applications can
access to fetch data. Create a /quakes
endpoint, which returns a list of
earthquakes ordered by magnitude:
var r = require("rethinkdb");
var express = require("express");
var app = express();
app.use(express.static(__dirname + "/public"));
var configDatabase = {
db: "quake",
host: "localhost",
port: 28015
}
app.get("/quakes", function(req, res) {
r.connect(configDatabase).then(function(conn) {
this.conn = conn;
return r.table("quakes").orderBy(
r.desc(r.row("properties")("mag"))).run(conn);
})
.then(function(cursor) { return cursor.toArray(); })
.then(function(result) { res.json(result); })
.finally(function() {
if (this.conn)
this.conn.close();
});
});
app.listen(8081);
Add an endpoint called /nearest
, which will take latitude and longitude
values passed as URL query parameters and return the earthquake that is closest
to the provided coordinates:
app.get("/nearest", function(req, res) {
var latitude = req.param("latitude");
var longitude = req.param("longitude");
if (!latitude || !longitude)
return res.json({err: "Invalid Point"});
r.connect(configDatabase).then(function(conn) {
this.conn = conn;
return r.table("quakes").getNearest(
r.point(parseFloat(longitude), parseFloat(latitude)),
{ index: "geometry", unit: "mi" }).run(conn);
})
.then(function(result) { res.json(result); })
.finally(function(result) {
if (this.conn)
this.conn.close();
});
});
The r.point
command in the code above is given the latitude and longitude
values that the user included in the URL query. Because URL query parameters
are strings, you need to use the pareFloat
function (or a plus sign prefix)
to coerce them into numbers. The query is performed against the geometry
index.
In addition to returning the closest item, the getNearest
command also
returns the distance. When using the unit
option in the getNearest
command,
the distance is converted into the desired unit of measurement.
Build a frontend with AngularJS and leaflet
The earthquake application’s frontend is built with AngularJS, a popular JavaScript MVC framework. The map is implemented with the Leaflet library and uses tiles provided by the OpenStreetMap project.
Using the AngularJS $http
service, retrieve the JSON quake list from the
node.js backend, create a map marker for each earthquake, and assign the array
of earthquake objects to a variable in the current scope:
$scope.fetchQuakes = function() {
$http.get("/quakes").success(function(quakes) {
for (var i in quakes)
quakes[i].marker = L.circleMarker(L.latLng(
quakes[i].place.coordinates[1],
quakes[i].place.coordinates[0]), {
radius: quakes[i].properties.mag * 2,
fillColor: "#616161", color: "#616161"
});
$scope.quakes = quakes;
});
};
To display the points on the map, use Angular’s $watchCollection
to apply or
remove markers as needed when a change is observed in the contents of the
quakes
array.
$scope.map = L.map("map").setView([0, 0], 2);
$scope.map.addLayer(L.tileLayer(mapTiles, {attribution: mapAttrib}));
$scope.$watchCollection("quakes",
function(addItems, removeItems) {
if (removeItems && removeItems.length)
for (var i in removeItems)
$scope.map.removeLayer(removeItems[i].marker);
if (addItems && addItems.length)
for (var i in addItems)
$scope.map.addLayer(addItems[i].marker);
}
);
You could just call $scope.map.addLayer
in the fetchQuakes
method to add
markers directly as they are created, but using $watchCollection
is more
idiomatically appropriate for AngularJS—if the application adds or removes
items from the array later, it will dynamically add or remove the corresponding
place markers on the map.
The application also displays a sidebar with a list of earthquakes. Clicking on
an item in the list will focus the associated point on the map. That part of
the application was relatively straightforward, built with a simple ng-repeat
that binds to the quakes
array.
To complete the application, the last feature to add is support for plotting the user’s own location on the map and indicating which earthquake in the list is the closest to their position.
The HTML5 Geolocation standard introduced a browser method called
geolocation.getCurrentPosition
that provides coordinates of the user’s
current location. In the callback for that method, assign the received
coordinates to the userLocation
variable in the current scope. Next, use the
$http
service to send the coordinates to the /nearest
endpoint.
$scope.updateUserLocation = function() {
navigator.geolocation.getCurrentPosition(function(position) {
$scope.userLocation = position.coords;
$http.get("/nearest", {params: position.coords})
.success(function(output) {
if (output.length)
$scope.nearest = output[0].doc;
});
});
};
To display the user’s position on the map, use $watch
to observe for changes
to the value of userLocation
. When it changes, create a new place marker at
the user’s coordinates.
$scope.$watch("userLocation", function(newVal, oldVal) {
if (!newVal) return;
if ($scope.userMarker)
$scope.map.removeLayer($scope.userMarker);
var point = L.latLng(newVal.latitude, newVal.longitude);
$scope.userMarker = L.marker(point, {
icon: L.icon({iconUrl: "mark.png"})
});
$scope.map.addLayer($scope.userMarker);
});
Put a pin in it
To view the complete source code, you can check out the repository on
GitHub. To try the example, run
npm install
in the root directory and then execute the application by running
node app.js
.
To learn more about using geospatial queries in RethinkDB, check out the documentation. Geospatial support is only one of the great new features introduced in RethinkDB 1.15. Be sure to read the release announcement to get the whole story.