A guide to using Mapbox and Unsplash to build a map of photos.

Plotting photos on a map with the Unsplash API and Mapbox

I moved to New York City in October of 2020. After the vicious first wave of the pandemic, articles flew across the internet declaring that New York was dead. Photography was re-emerging as one of my hobbies during quarantine, so I thought I might take my camera and venture across town to see if I might still be able to find life in this little city.

I set the ambitious, perhaps impossible, goal of photographing every block in NYC to show the life that still exists here. Setting aside the challenge of photographing all 120,000 blocks across the five boroughs (although less than 3,000 in Manhattan), the first thing I needed to do was build a tool to help me display my photos on a map.

What are we building?

Follow along as I attempt to photograph all of NYC at Is New York Dead?

There were several pieces of this that I didn’t want to build myself:

  • Uploading a photo
  • Image transformation and compression
  • Tagging a photo with an address
  • Converting a photo’s address to set of latitude and longitude coordinates

To avoid building all of this myself, I looked for creative shortcuts. I came across Unsplash’s API. Unsplash is a website that allows photographers to make their work available for free use. Unsplash checks all of the boxes above: I can upload my photos (and in bulk groups, up to 10 at a time) and type in an address. Unsplash then makes that address’s coordinates available via API, as well as all of the photo’s metadata. Having that extra metadata will come in handy as my project grows and likely takes a new shape.

The Toolkit

Setup

I’m partial to Ruby on Rails, hosted on Heroku. It’s quick to start and there’s plenty of documentation available. To start, I followed Heroku’s tutorial to creating a new Rails 6 project in Heroku. This tutorial gets you to a point where you’ll have a website live on the internet that says “Hello World!”

Mapbox

To display a map from Mapbox on your website, you’ll first need three things:

  1. A Mapbox account: create one here.
  2. An Access token: once you’re logged into Mapbox, visit /access-tokens/ and create a token there. Mapbox also offers the option to restrict your token to only work on certain URLs. As a security measure to prevent someone else from using your token, add your website’s url to this list.
  3. A Style URL: Mapbox allows you to create your own map styles. Visit studio.mapbox.com to create a new style. After creating your style, you should see an option to share the style URL. The style URL should take the following structure: mapbox://styles/username/style-id

Unsplash

To begin working with the Unsplash API, you’ll first need two things:

  1. An Unsplash account: create one here.
  2. An API access key and secret key: Once you’re logged in, visit the Unsplash developers page and then visit ‘Your apps’. Create a new application and complete the basic information that Unsplash requires to work with their API. Once your app is created, you should see a page that includes an Access Key and Secret Key. Keep these secure and hold on to these for later use.

Adding a Mapbox map to the website

  1. Add the following within the <head></head> tag in application.html.erb:
<script src='https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css' rel='stylesheet' />

2. Next, we’ll add the snippet to our view to control how the map is displayed. If you followed Heroku’s Rails 6 setup tutorial, the file you’re looking for is index.html.erb

<div id='map' style='width: 400px; height: 300px;'></div>
<script>
mapboxgl.accessToken = "YOUR ACCESS TOKEN HERE";
var map = new mapboxgl.Map({
container: 'map',
style: 'YOUR STYLE URL HERE',
center: [-73.975854, 40.728525],
zoom: 12.5,
});
</script>

Make sure to plug in the access token and style url that you created earlier. Then, boot up your server with rails s in Terminal and visit http://localhost:3000. At this point, you should see a map!

Adjust the center or zoom values in the script above and refresh the page to see how these settings affect the map’s default display.

Fetching photos from Unsplash

Installing the Unsplash Gem

  1. Install the Unsplash gem. In terminal, enter $ gem install unsplash
  2. Next you’ll need to configure the gem with your credentials. Create a new file in /config/initializers/ called unsplash.rb and populate it with the following:
Unsplash.configure do |config|
config.application_access_key = ENV['UNSPLASH_KEY']
config.application_secret = ENV['UNSPLASH_SECRET']
config.application_redirect_uri = "https://your-application.com/oauth/callback"
config.utm_source = "alices_terrific_client_app"
end

To securely provide the Unsplash Access Key and Secret that you previously acquired, you’ll need to store them as environment variables. In terminal, enter:

  • $ export UNSPLASH_KEY='PASTE YOUR KEY HERE'
  • $ export UNSPLASH_SECRET='PASTE YOUR SECRET HERE'

If you happen to close your terminal and come back to the project later, you may need to redo this step. Also to make sure this works on your production Heroku app, provide these values as Config Vars on the Settings tab in Heroku.

Testing the API connection

@photo = Unsplash::Photo.search("dogs")[0][:urls][:regular]

Let’s break this call down to understand what’s happening:

  • @photo = : we’re defining a variable that will store the photo’s url and make it accessible in the view.
  • Unsplash::Photo.search("dogs"): This is the API call, using the search method and passing along that we’d like to search for dogs. This returns a big chunk of JSON that we then need to parse to get the url of a dog photo. To understand this, view the expected JSON output of the Unsplash API search here.
  • [0][:urls][:regular]: This bit is combing through the JSON. It selects the first photo in the results array, finds the list of that photo’s URLs, and then selects the URL for the regular sized photo.

Next, let’s render that photo in the view. Currently, @photo stores the URL of a random dog photo (could we ask for a better surprise?). To view the photo, head back to index.html.erb and plug that URL into an HTML image tag:

<img src="<%= @photo %>" alt="">

Boot up your server with rails s and refresh localhost. If you see a dog photo, your API connection is working!

Fetching photos from a collection

To fetch all of the photos and their locations from this collection, we’ll need to make a few API calls:

  1. Get the IDs of all photos in a collection
  2. Looping through those IDs, get and store their information in an array.

Replace the current API call in your controller index method with this:

@photos = []
Unsplash::Collection.find('32804345').photos(page=1, per_page=20).each do |p|
@photos << Unsplash::Photo.find(p[:id])
end

This creates an empty array called @photos , fetches the first 20 photos from collection ID 32804345 (the ID of the collection I just created), and loops through those photos and calls the API again to add their information to the array.

Jumping back to index.html.erb, we can test that this worked by looping through the @photos array and loading the photo into the view.

<% @photos.each do |p| %>
<img src="<%= p[:urls][:regular] %>" alt="">
<% end %>

There’s a few problems with this, however:

  1. We’re making 21 API calls every time the page is loaded. This will quickly deplete the 50 calls/hr allowed in Unsplash’s demo level. This also certainly hurts page performance.
  2. We’re also doing a lot of wrangling of JSON in the view to get the information we want.

To address these issues, I decided to build a simple database that stores just the photo’s URL, description, and coordinates from Unsplash.

Storing photo links in a simple database

Building the database

$ rails generate model Block description:string image_url:string latitude:string, longitude:string

You’ll then need to create and migrate your database by running $ rake db:create db:migrate in Terminal. After pushing your changes to Heroku, you’ll have to do the same in Heroku: $ heroku run rake db:create db:migrate.

Fetching and saving photos on command, instead of on page load

Create a new file in /lib/tasks called blocks.rake. In this file, we’ll do several things:

  1. Loop through the collection and add all photos to an array. To add all, we use the collections [:total_photos] count to create a for loop.
  2. For each photo in the collection, call the API to fetch its details and then use its latitude and longitude to first see if that block already exists in our database. Using find_or_create means we’ll update the block of it exists and create it if it doesn’t.
  3. We’ll then parse the photo’s JSON to find and save the attributes we want to include in our database.
namespace :blocks do
task fetch_all: :environment do
@photos = []
count = Unsplash::Collection.find('32804345')[:total_photos]
pages = count/20.ceil() + 1
for n in 1..pages
Unsplash::Collection.find('32804345').photos(page=n, per_page=20).each do |p|
@photos << Unsplash::Photo.find(p[:id])
@photos.each do |photo|
block = Block.find_or_create_by(latitude: photo[:location][:position][:latitude], longitude: photo[:location][:position][:longitude])
block.description = photo[:description]
block.latitude = photo[:location][:position][:latitude]
block.longitude = photo[:location][:position][:longitude]
block.image_url = photo[:urls][:regular]
block.save
end
end
end
end
end

There’s a few redundancies in this bit of code that could be cleaned up, but it works.

When you add photos to your Unsplash collection, all you’ll need to do is run the following Terminal command to update your database: rake blocks:fetch_all or, in Heroku, heroku run rake blocks:fetch_all. Super slick.

Displaying photos on the map

The rake task created the blocks in the database, so now our controller only needs to get those blocks from the database and provide their information to the view on page load. Returning to our controller, let’s update the index method to replace all of those API calls with a database call:

class WelcomeController < ApplicationController
def index
@blocks = Block.all
end
end

Creating and displaying a Mapbox feature set on the map

Now, in the view, we’ll need to work with Mapbox again to display a set of markers (see Mapbox documentation). To display the photos on the map, we need to do two things:

  1. Use the collection of blocks from our database to create a set of features
  2. Display each of those features on the map

Working again in index.html.erb, we’ll first create the feature set by pasting the following snippet within your Mapbox <script> and just below the block where you defined var map = ….

var geojson = {
type: 'FeatureCollection',
features: [
<% @blocks.each do |b| %>
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [<%= b.longitude %>, <%= b.latitude %>]
},
properties: {
image_url: "<%= b.image_url %>",
}
},
<% end %>
]
};

This takes the @blocks array that was provided by the controller and provides the photo’s longitude, latitude, and image_url to Mapbox.

Next, we’ll use that feature set to create markers on the map. Add the following just below the snippet we just added:

geojson.features.forEach(function(marker) {
var el = document.createElement('div');
el.className = 'marker';
new mapboxgl.Marker(el)
.setLngLat(marker.geometry.coordinates)
.setPopup(new mapboxgl.Popup({ offset: 10 })
.setHTML("</p><img src='" + marker.properties.image_url + "'>"))
.addTo(map);
});

This creates an element with the marker class for each photo in the database. To make those elements visible on the map, we need to add some styling in application.scss:

.marker {
background-color: pink;
border-radius: 50%;
cursor: pointer;
height: 14px;
width: 14px;
}

That’s it! Start your server and you should now see a dot on the map for each photo in your database. Clicking the marker on the map will then display the photo using the URL of the image hosted on Unsplash.

Questions?

Product designer, mechanical engineer, Harvard MBA, and third-generation farm kid in the Hudson Valley.