Bike Lanes in Chicago

Let’s make an ambitious map

Matt Gardner
NYC Planning Tech
Published in
8 min readJul 23, 2018

--

Note: This is part one of a series that will explore making map-based web applications.

Mapping is the reason I got into programming in the first place. But I always ran up against limitations with the early mapping applications I was creating. Adding new features to my jQuery + LeafletJS projects turned into a nightmare. So I picked up an application framework to help me organize my code a little more.

I chose EmberJS because it includes a full suite of everything I’d need to build an app. What I found missing from the many blog posts on EmberJS, however, was this unique combination of mapping and application development. This tutorial seeks to cover:

  • EmberJS application basics
  • Promises, Fetch, and Responses in JavaScript
  • MapboxGL and Ember property bindings

Spin up a new app

Let’s get started: first, run npm install -g ember-cli in your Terminal window. You’ll probably need npm first. Once that’s done, you can spin up a blank app by running ember new map-demo-app. This’ll start you off with a blank slate and include the basic structural app objects and all the helpful Ember accoutrements.

Run cd map-demo-app and then run ember s. If you visit http://localhost:4200/, you should see Tomster next to a friendly message. Let’s keep this server up and running, open a new Terminal window, and then open up the map-demo-app directory in your favorite code editor.

The EmberJS folder structure divides up different objects according to their responsibility. For example, helpers are basically simple functions that you can call anywhere inside your markup, usually to format numbers or dates.

Routes are responsible for defining behavior based on where we are in the URL. If you look at the URL right now, you will see something like “medium.com/nycplanninglabs/blog-post-title”. This tells us that we are reading a blog post in the “nycplanninglabs” organization at the Medium website. It also tells the application what to dig up to show the user.

Components are a big part of EmberJS. They are configurable, composable, reusable, self-contained pieces of an interface. They help organize application responsibilities into logical groups. Ergonomically speaking, they also enforce a sense of encapsulation so others might be able to look at your code later and understand what’s going on.

For our map, we’re going to start simple, and define a new route so we can define the behavior we want when a user first enters the app. In Ember, when a user visits /, the root of our application, it will go to the application route. In general, you can call your routes whatever you want, and change how they are cosmetically represented in the actual URL, but in this case, the “application” route is always the name Ember uses for the top-most level of our routes.

Setup the main route

Run ember g route application. When we ran ember new earlier, ember-cli went ahead and created a template file for the application route for us, so ember-cli will ask if you want to overwrite it. You can just hit n for “no.” Afterwards, the command should create a route file for us, called application.js. Note how the name of the JavaScript file matches the name of the template file — this is a convention in EmberJS that tells our compiler that these two things go together.

For this project, the application route is going to be responsible for fetching the data for our map. In almost any Ember application, the route is responsible for preparing its template for presentation. Routes in EmberJS include a number of hooks — think of them as events that are run at specific moments in the application lifecycle. Want to run something after the data is loaded? There’s a hook for that.

Getting the data into the app

Open your application.js route file, and add a method for the model hook:

// routes/application.js
import Route from '@ember/routing/route';
export default Route.extend({
model() {
return {};
}
});

The model hook is a Promise-aware route hook, meaning anything after it is guaranteed to be run after the Promise resolves. For example, if we use the model hook to fetch some data from GitHub, we can be confident that our template won’t render until that data is loaded. This is helpful because we can now more easily reason about the asynchronous nature of JavaScript.

Note that in our code above, we’re just returning a blank object literal. If you open your application.hbs and write {{log model}}, you will see a blank object literal in your JavaScript console. What this tells us is that we have access to our data in the application route template through the model property. It also tells us that we should get some real data into the app.

Let’s start with some Chicago bike lane data I found. GitHub is nice enough to generate a preview for us:

Two things need to happen to our model hook to get this line data: first, we need a way to have the browser make a network request for the data. Second, we need to know where the data is actually stored on the web.

First thing’s first. We’re going to use the Fetch API, a new standard for getting data in the browser. It’s not supported by Internet Explorer yet, so let’s run ember install ember-fetch, which will guarantee that all users can run this app even if they’re using IE. Finally, let’s import it into our route file:

import Route from '@ember/routing/route';
import fetch from 'fetch';
export default Route.extend({
model() {
return {};
}
});

Second, let’s find out where the data is. If we open the GitHub Gist and click the “Raw” button, we can copy the URL and use it as the first argument of the fetch function:

import Route from '@ember/routing/route';
import fetch from 'fetch';
export default Route.extend({
model() {
return fetch('https://gist.githubusercontent.com/allthesignals/f9224f5b2860e19c900180f295f0dbe7/raw/ad81c60619ee95932dd6957a22a1a8d8b0e6a3b8/tutorial_chicago_bike_routes.geojson');
}
});

That’s a bit ugly. Let’s move that first part of the URL into a constant called HOST:

import Route from '@ember/routing/route';
import fetch from 'fetch';
const HOST = 'https://gist.githubusercontent.com/allthesignals/f9224f5b2860e19c900180f295f0dbe7/raw/ad81c60619ee95932dd6957a22a1a8d8b0e6a3b8';export default Route.extend({
model() {
return fetch(`${HOST}/tutorial_chicago_bike_routes.geojson`);
}
});

That’s a little better. Assuming you added {{log model}} into application.hbs, we should see something new in the console:

This is called a Response object — the Fetch API can actually request a lot of different types of data including images and video so we need to tell fetch how to process that data. In our case, we just want JSON:

import Route from '@ember/routing/route';
import fetch from 'fetch';
const HOST = 'https://gist.githubusercontent.com/allthesignals/f9224f5b2860e19c900180f295f0dbe7/raw/ad81c60619ee95932dd6957a22a1a8d8b0e6a3b8';export default Route.extend({
model() {
return fetch(`${HOST}/tutorial_chicago_bike_routes.geojson`)
.then(blob => blob.json());
}
});

If you look in your JavaScript console, you should see something more familiar:

That’s what makes the Route’s model hook so nice — it’s very easy to reason about our application when we know for certain that our data is available.

Mapping the data

Now that we have data, let’s do something with it. I like to use MapboxGL for this because it provides for a superior user experience. Before jumping right in and adding the MapboxGL library to the project, I like to do a little research beforehand to see if others have written any bindings or integrations for the problem I’m trying to solve. We can use ember-mapbox-gl to make implementation of our map much easier. Why do we need all that? Essentially, we want something that will handle the state management between EmberJS and MapboxGL. If I pass down a style property to a MapboxGL map in Ember, I don’t want to have to call setStyle methods on MapboxGL — Ember already does attribute binding for us. For that reason, I always try to find an integration addon.

Start by running ember install ember-mapbox-gl. This will install the addon. You may have to restart your server. Then, grab an API key from Mapbox. This API key is a public key used for basemaps. It’s possible to skip this step and use your own basemap, but we can cover that in a later tutorial. Add this API key to your config/environment.js file:

// config/environment.js
'mapbox-gl': {
accessToken: 'ACCESS TOKEN HERE',
map: {
style: 'mapbox://styles/mapbox/basic-v9',
zoom: 8,
center: [ -87.4985112, 41.8832547 ]
}
},

This sets up the default position of the map for all usages of mapbox-gl maps. Take a look at your application.hbs file and remove {{welcome-page}} (this is what displays the Tomster image). Now, let’s add the map component and dive into the details:

There’s a lot going on here. Some of this requires a basic understanding of MapboxGL’s JavaScript API. There are many different types of objects at your disposal when making maps, but in essence, these maps are made up of sources and layers. Sources manage the data side of what is on the screen — if you are using raster data or tiled data, MapboxGL’s API has you covered. Layers manage the presentation logic of what’s displayed on screen, and they reference a loaded source ID.

What we’re doing in line 3 is invoking the mapbox-gl component that we just installed and passing a block into it. Much like HTML tags, this functionally contextualizes the inner details of whatever is passed. Sometimes it just wraps it in another div tag, other times it exposes other functionality, like in line 4, where we see this:

{{#map.source options=...}}

Here, we’re defining a new mapbox gl source on the map instance. The connection between the map instance and the source object is established by the contextual parameter signified in line 3 by {{ ... as |map|}}.

You’ll also notice that in line 4, we say data=model. Here, we’re passing the resolved model data into a mapbox-gl source component. Finally, we invoke a layer component with {{source.layer ...}} on line 5. In MapboxGL, sources must exist inside the map before layers can call upon them. The nested template API here helps illustrate that.

Finally, when you look at your app in the browser, you’ll still see a blank page. You can fix that by adding some CSS to expand the width and height of the map. Open app/styles/app.css and add the following line:

.mapboxgl-map {  height: 100vh;  width: 100vw;}

There we go:

What now?

We’ve now laid the groundwork for creating an ambitious map. Our application architecture will help us organize our code for a more sustainable project.

Next week, let’s add some hover effects including highlighting and popups. We will also add filters. Let me know in the comments below if you have questions!

--

--