Making maps is a great way to learn JavaScript. Along with specific guides, like Codecademy and Eloquent Javascript, it's useful to have some specific guidance on how mapping JavaScript works.
You're likely familiar with the idea of variables: in programming languages, they're names that you can give to things, like
var myName = 'Tom';
After you name something with a variable, you can modify it by that name and send it around to different functions.
var myShoutingName = myName.toUpperCase();
Now myName
is equal to 'Tom'
but myShoutingName
is 'TOM'
.
You might be familiar with some basic data in programming languages:
var dogName = 'Barky';
var dogAge = 5;
var dogIsAGirl = true;
Objects are like 'bundles' of multiple different values that are grouped together. For instance, instead of the three facts about my dog Barky being separate variables, they can be stored in one object.
var barky = {
name: 'Barky',
age: 5,
girl: true
};
// you can access part of this dog and
// change it, if you get an electric fence
// for instance
barky.name = 'Yelpy';
Classes are ways to make the same kind of object more than once. For instance, if you have ten dogs, you might grow tired of typing so much, so you can create a class:
function Dog(name, age, girl) {
this.name = name;
this.age = age;
this.girl = girl;
}
// get a new dog
var newDog = new Dog('Professor Barksalot', 1, false);
Classes make objects in a set way - if something is made with Dog
, you know, for instance, that it has a .name
you can grab and look at.
Here's a little wrinkle that might be confusing: traditionally, when you create a JavaScript class like we did above, you then use new
, a special operator in JavaScript, to create objects from it. To create a date, you would call
var now = new Date();
Date
is the class, now
is the variable that you made that now contains the current date.
This is a little awkward, and when you forget to write new
before a class, you run into the worst kind of bug: software that's neither completely working nor totally broken.
Because of this annoyance, a lot of classes that you run into will support what's called 'auto-new', which means, simply, that you don't need to write new
in front of them to make one. In the case of maps and Leaflet/Mapbox.js, there's a little hint: any class that starts with a lowercase letter is initialized with auto-new, whereas any that begins with a capital is a traditional class. So, for instance:
// a place, created with auto-new
var place = L.latLng(0, 0);
// another place, with a traditional constructor
var place2 = new L.LatLng(0, 0);
Let's look at examples of this in action:
var map = L.mapbox.map('map');
map
is a variable - that's what you're going to call your map. L.mapbox.map
is a class - it's an automated way to say 'make me an object with all of the things I need in a map.' What you get out of this, the map
, is an object: it has functions like map.setZoom()
that are useful for doing mappy things.
One of the things that might take you by surprise is that your code has to deal with the idea of time. Some parts of your code might only run after something has happened, like a file has been downloaded or someone clicks on the page. Some of it might never run.
JavaScript deals with this problem with an idea known as the callback. A callback is a JavaScript function like any other, but it's gets its name from the way that it's used.
This is probably the simplest possible callback: window.setTimeout
is a built-in function that lets you set a timer for something to happen, and it takes as arguments another function, and a number of milliseconds.
When the number of milliseconds - here 1,000, so 1 second - has passed, the page calls the function you provide. This is a vital detail of how callbacks work: they're functions that you don't call directly, but are called for you, when the time is right.
window.setTimeout(function() {
alert('hi');
}, 1000);
You'll see callbacks everywhere in maps: for instance, let's say you wanted to watch for every time that someone clicked a map.
map.on('click', function() {
alert('I was clicked!');
});
One of the main places where we see the existence of callbacks is in concert with AJAX, the way that JavaScript is able to download resources on the fly in order to show content. For instance, when you create a new L.mapbox.featureLayer
, you might be tempted to do this:
// get the markerlayer with the map id `foo.bar`
var featureLayer = L.mapbox.featureLayer('foo.bar');
// what GeoJSON content do those markers have?
var thatGeoJSON = featureLayer.getGeoJSON();
But you'll find, to your chagrin, that there's no content in that getGeoJSON
call. That's because when JavaScript wants to download something like a bunch of markers from a server, it doesn't stop at the line
var featureLayer = L.mapbox.featureLayer('foo.bar');
Before starting up and running the next line - it says "start downloading those markers!" and then keeps on keeping on.
So how do you know when the markers are downloaded? Events and callbacks.
var featureLayer = L.mapbox.featureLayer('foo.bar');
featureLayer.on('ready', function() {
// what GeoJSON content do those markers have?
var thatGeoJSON = featureLayer.getGeoJSON();
}):
The L.mapbox.featureLayer
class has a callback that fires right as soon as the markers are loaded in full. If you wait for this callback, then you can know for sure that .getGeoJSON()
and all other functions that rely on those markers being fully downloaded will work just as expected.
You'll likely see the terms JSON & GeoJSON when you hear about map data: GeoJSON is a very popular format for storing things like the lines, points, and polygons that make up map overlays.
GeoJSON is so-named because it's written in a more general language called JSON. JSON, which stands for JavaScript Object Notation, is a way of storing data that otherwise would be hardcoded into software, into standalone files that are easily readable not only by JavaScript but by other languages and systems.
So, GeoJSON is a subset of JSON, and JSON is a subset of what you can represent in JavaScript. Let's say that in JavaScript you have an object like:
var myBird = {
// this is the bird's age
age: 10,
// and its name
name: 'Smokey'
};
JSON is like this but stricter: no var
, no comments allowed, and you have to use "
instead of '
all the time:
{
"age": 10,
"name": "Smokey"
}
GeoJSON unfortunately doesn't have a way to represent this bird: it's only for maps. Some GeoJSON might look like
{
"type": "Point",
"coordinates": [0, 0]
}
That's a point at latitude=0, longitude=0.
The important aha moment to have with GeoJSON and JSON in general is that, while it's pretty strict for how you write it, once you bring a JSON object into JavaScript by AJAX or however, it's the same as any other JavaScript object: you can change it, use it as a variable in functions, or anything else. That's why when there's a function like
featureLayer.setGeoJSON(geoJsonObject);
What it's really saying is that it expects a JavaScript object that's formatted like GeoJSON. It doesn't care how it got there, or what's in its past. Now is the time.
As we discussed earlier, the L.mapbox.map
class includes 'everything that you need for a map'. What are these things, and what do we call them?
The map itself doesn't actually contain much: it basically keeps track of:
- Dimensions of the map in pixels
- Centerpoint & Zoom
Beyond those, it keeps references to other bits of code that do particulars, like:
- Layers, like
L.mapbox.tileLayer
- Handlers, like
.scrollWheelZoom
- Controls, like
L.mapbox.legend
One design quirk that might be unfamiliar coming from other systems is how Leaflet keeps track of 'layers' and 'layer groups'.
The most common kind of layer you have in a map is a tile layer, like L.tileLayer
or L.mapbox.tileLayer
. These layers are just themselves, so if you add a few to a map, the setup will look like:
- Map
- tileLayer a
- tileLayer b
- tileLayer c
To access these layers, you could call
map.eachLayer(function(l) {
// here you have each layer object in order
// this callback function is called with
// a, b, and then c
});
For more complex layers, this gets a little bit trickier. For instance, for an L.geoJson
layer, instead of having
- Map
- tileLayer a
- geojsonLayer b
Instead, a L.geoJson
layer is actually a L.layerGroup
behind the scenes: so it's really a collection of smaller layers representing each feature:
- Map
- tileLayer a
- geojsonLayer b
- L.Path a
- L.Circle b
So, for each feature
in your GeoJSON data, the geojsonLayer actually has a specific little layer for that feature.
Want to put a point on the map? There are quite a few ways! Let's nail down when you'd want each of them.
The lowest-level interface is putting shape layers on the map: let's say you add a marker and a line to the map:
// a line
var polyline = L.polyline([[0,0], [10, 20]]).addTo(map);
// a marker
var marker = L.marker([0,0]).addTo(map);
These are the low-level interfaces to Leaflet's drawing code, and they're very useful if you have basic ideas of what you want to draw, like 'a line from A to B'.
They have the drawback that there's no easy way to transfer this kind of overlay as data, since there's no format for it, and it's not immediately simple to manage what you put on the map. You can fix the management problem by using a L.layerGroup:
var group = L.layerGroup([polyline, marker]);
If you want to transfer these overlays around, you'll want to use a real file format: that's GeoJSON.
var myVectors = L.geoJson({
type: 'FeatureCollection',
features: [{
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [[0, 0], [10, 20]]
}
}, {
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: [0, 0]
}
}]
})
The output of this is similar to the group
we made before - the GeoJSON
data is, behind the scenes, instantiated with Leaflet's lower-level shapes but
kept in a group. This has the advantage that you can AJAX your data in or
manage it with any tool that knows how to talk GeoJSON.
How about nice-looking markers, and data that you've added in Mapbox?
Mapbox.js extends Leaflet with some simple abstractions to make this simpler.
The basic element is the L.mapbox.featureLayer()
.
This is like a L.geoJson
object but with some differences:
You can instantiate a featureLayer
just with a map id, and it will magically
pull the markers down for you:
var markers = L.mapbox.featureLayer('my.mapid').addTo(map);
// behind the scenes the markers are downloaded with AJAX, and when they're
// done, the `ready` event is fired.
You can do the same with another GeoJSON file:
markers.loadURL('mymarkers.geojson');
// behind the scenes the markers are downloaded with AJAX, and when they're
// done, the `ready` event is fired.
Finally, a special convenience built in to L.mapbox.map()
is that markers
for a given map ID are automatically loaded into map.featureLayer
:
var map = L.mapbox.map('my.mapofthings');
// this marker layer contains any markers that are saved along with the map
// mapofthings
map.featureLayer;
If you don't want this to happen, it's easy to opt-out:
var map = L.mapbox.map('my.mapofthings', {
featureLayer: false
});
That said, if you want to load custom GeoJSON data, you should not just
reuse map.featureLayer
, because it'll get overwritten by the data automatically
downloaded. So just create a new L.mapbox.featureLayer
, add it to the map
as demonstrated above, and load your custom data into it.
L.mapbox.featureLayer
instances have the special feature that they will style
markers with simplestyle and
will use the Maps API
to retrieve marker icons.