How to make a simple map app using Ruby on Rails, React and Leaflet, part 1: Backend

Ana
4 min readJan 9, 2019

--

Meridian Hill Park, Washington, D.C., source: author

Maps can be a great addition to your app, however working with them can be quite intimidating, especially if you’re new to geospatial software. I recently made an app that has an interactive map of street trees in Washington, DC. This is the most challenging app I’ve ever worked on, and I’ve wanted to share my experience here in hope that someone might find it helpful.

Database

For this project I used PostgreSQL and its spatial extension PostGIS that enables location queries in SQL. For more info and installation guide click here (PostgreSQL) and here (PostGIS).

The tree information is part of the DC Open Data API. It came in a form of a large CSV file with almost 180,000 rows. This was the first challenge I faced since I’ve never worked with large database. Importing the data wasn’t the only goal, I had to keep in mind performance and speed. After some research I found out that ActiveRecord has a handy library called ActiveRecord-Import that solves this exact problem (for more info see the official docs and this blog). However, before importing the data, I had to prepare it.

Preparing data

This means the following:

  1. Transform x and y coordinates to the following format: POINT (x y) (otherwise PostGIS spatial queries won’t work)
  2. Match column names in the CSV file to column names in the database table (otherwise ActiveRecord-Import won’t work)
  3. Delete trees that don’t have x and y coordinates (they won’t appear on the map anyway)

After some thinking and a lot of research, I’ve decided to create a rake task that will handle this data transformation (more on rake tasks and how to write them here). That way my data can be prepared by simply running rake data:prepare in the terminal.

In lib/tasks/data.rake I wrote the following:

lib/tasks/data.rake

Importing data

This is where ActiveRecord-Import does its magic. The code is very straightforward. In app/models/tree.rb I wrote the import method that looks like this (lines 8–17):

app/models/tree.rb

And then I simply call it in db/seeds.rb and pass it ‘data.csv’ as an argument (the file where all the prepared data was saved):

db/seeds.rb

Organizing our code this way makes it easy to control data preparation and database seeding from Terminal. All it takes are two commands (rake data:prepare and rake db:seed) to run these tasks.

Spatial queries

For my project, I wanted to make an interactive map that allows the user to filter data by different parameters. To be more precise, I wanted to implement searching by radius, filtering by various tree features (e.g. common name, condition, ward), as well as an option to render only trees within a certain bounding box. Bounding box query is also used to improve performance by fetching and rendering only trees that are within the borders of the user’s screen. All these filters use spatial queries built into PostGIS. More on PostGIS: data types, setup and radius search.

Using scope instead of methods makes it possible to chain and combine database queries and that way filter by more than one parameter (e.g. search trees with condition ‘excellent’ in 3 miles radius from a certain point). To learn more about scope, click here and here.

Making bounding box queries (lines 29–39) was one of the most challenging parts of this app mostly because there weren’t any docs, blogs or tutorials on it. The bounding box query takes two points as arguments (longitude and latitude for SouthWest point, and longitude and latitude for NorthEast point) and uses them to create a rectangle or square area on the map using ‘factory.point()’ syntax. Argument points are sent from the frontend and represent the points that match the corners of the user’s screen.

The query that finds trees with condition ‘excellent’ (lines 42–44) is just an example of the syntax. The same approach can be used to filter trees by their genus name, family name, common name, etc.

app/models/tree.rb

PostGIS queries use meters as a default unit, but you can easily transform it to miles:

scope :within, -> (lon, lat, miles=1 { 
where (%{
ST_Distance(xy, 'POINT(%f %f)') < %d
} % [lon, lat, miles * 1609.34]) }

These methods are called in the controller using params passed in from the frontend part of the application. As you can see on the line 29 of the code snippet bellow, we can combine multiple queries thanks to PostGIS.

app/controllers/trees_controller.rb

Preparing API for frontend

Mapping libraries used on the frontend of this app require GeoJSON data format in order to work. GeoJSON APIs have their data structured in a certain way, which can easily be achieved with ActiveModel Serializer. I ended up writing two custom serializers in order to get the required structure. I then had to specify one of the two serializers in the Tree Controller (code snippet above, lines 16 and 18).

Tree Serializer structures every tree object in the following way:

app/serializers/tree_serializer.rb

Tree serializer is used by Trees serializer to achieve the structure required by GeoJSON and nest the tree data under ‘features’:

app/serializers/trees_serializer.rb

And that’s it! Our backend is ready. In the next post I will describe the frontend part of the app that uses Leaflet-React library, MapBox base map, and React-Leaflet-Markercluster library.

--

--

Ana

I moved from Europe to USA and from human languages to programming languages.