Realtime Maps With Meteor and Leaflet – Part One
The parties example bundled with Meteor is a nifty demonstration of the framework’s core principles, but it uses a 500 x 500 pixel image of downtown San Francisco as a faux map. This means that we cannot pan or zoom the “map,” and when we double-click the image to create new parties, the circle markers are drawn at the position of the clicks in relation to the image element in the browser window, and not at geospatial coordinates.
I decided to update the example to use Leaflet.jsto make a real map that looked and felt as close to the original example as possible. In particular, I wanted to preserve the color-coded circles (red for private, blue for public parties) labeled with the number of RSVPs, and the larger animated circle indicating which party is currently selected, with its details displayed in a section outside the map. This is a useful pattern for displaying individual marker details without using a popup that occludes part of the map.
Here is the end result with source code. In the next two posts, I will go over the changes I made to the original example. I won’t be covering how Meteor works, and will assume you have some understanding of how the parties example works as well.
Setting the Stage
First off, I created the example and added leaflet to the project using Meteorite.
1 2 3 4 5 6 |
|
I then edited the page
template to use Bootstrap’s fluid classes to generate a responsive page layoutand added a window.resize()
handler to adjust the map’s size as the browser is resized. I use this pattern when creating responsive Leaflet maps, and it’s not specific to Meteor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 |
|
Map Initialization
Stamen Design’s toner themed map tiles make a nice replacement for the black & white map image in the example. I disabled double-click and touch zoom when initializing the map since those actions are how users create new parties, and I increased tile opacity to lighten the overall background and improve the visibility of markers on the map. Leaflet initialization code goes into the map
template’s rendered()
callback.
1 2 3 4 5 6 |
|
The next significant change was to replace the map
template’s event handler from the original example with Leaflet’s "dblclick"
event handler to manage the creation of new parties. The Leaflet version conveniently returns a LatLng
which I saved to a Session variable before triggering createDialog
. The mechanism to trigger dialogs by setting the associated Session variables Session.showCreateDialog
and Session.showInviteDialog
is unchanged from the original example, and it works because Meteor Session variables are reactive.
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 |
|
Creating and Saving a Party to the Database
This part of the application is also more or less unchanged from the original example except that I passed the party’s LatLng
(instead of click position) along with other details from the createDialog
template to the Meteor.methods()
call to createParty
. If the callback is successful, the new party’s _id
is saved to another reactive Session variable Session.selected
, which drives the details
template on the left.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Adding Markers to the Map in Realtime
As soon as a new party is added to the Parties mongo collection on the server, behind the scenes, Meteor transmits it back to a client-side minimongo collection with the same name on all connected and authorized clients. This can be verified by typing Parties.findOne()
into the JavaScript console. This is well and good, but the next task is to replace the D3 code to draw circles from the original example with code to add Leaflet markers to the map.
To do that, I hooked up a cursor.observe()
added()
callback to create the map marker and I added a click handler to the marker to update the Session.selected
variable with the party’s _id
. As users click on different parties, this reactively triggers the context for the details
template on the left. I also saved a reference to the marker in a local markers
hash to efficiently access the marker for future changes. Since we only need to set this up once, I put this code into the map
template’s created()
callback.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
The final bit of fanciness here is my createIcon()
helper function to create a lightweight DivIcon
that uses a simple div
element instead of an image icon. I used CSS border-radius
to style the div
as a circle of the appropriate color and set CSS line-height
to the height of the div
to vertically center the text. The attending()
helper function from the original example returns the number of Yes RSVPs.
1 2 3 4 5 6 7 8 9 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Now I can log in and create a few parties, and they all show up as markers with the appropriate color and label. When I click on a marker, its details are automatically rendered into the details
template on the left. But there’s no visual indication on the map as to which party is currently selected — I just need to remember which marker I clicked on last! As it turns out, this usability quirk is easy to address.
Realtime Maps With Meteor and Leaflet – Part Two
Recap
In the last post, I initialized a Leaflet map to work with Stamen Design’s toner themed map tiles and Bootstrap’s responsive layout. I then set up a double-click event handler to gather additional details about the new party, and hooked up the dialog’s save button to pass those details to a Meteor.methods()
call to save the party into a server-side mongo collection. Finally, I hooked up a cursor.observe()
added()
callback to the client-side minimongo collection and set up the callback to automatically add a circular DivIcon
marker at the specified coordinates.
Updating Party Details in the Database
A party document looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Each party contains an array of RSVP objects, which must be updated when any user adds or updates their RSVP to the party. In addition, private parties contain a set of invited users’ ids; the party owner can invite additional users at any time. So rsvps
and invited
are the two mutable party attributes in our example. The owner, title, description, coordinates or public/private setting cannot be changed, but a party’s owner can delete the party if no user is RSVPd as Yes.
The code to update and delete parties in the server-side mongo collection is virtually unchanged from the original. The invite()
and rsvp()
template event handlers are hooked to Meteor.methods()
calls that perform the necessary checks before updating the mongo collection on the server. As usual, behind the scenes, Meteor synchronizes the client-side minimongo collection with the server collection.
Updating and Removing Map Markers in Realtime
I hooked up the cursor.observe()
changed()
callback to update the party’s icon, and removed()
callback to delete the marker from the map and the local markers
hash.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Using a Halo Marker to Indicate Which Party Is Selected
Up to this point, there’s been no visual indication on the map as to which party is currently selected. Like in the original Parties example, I solved this by creating a 50px x 50px transparent grey circular marker and making it concentric with the currently selected party’s marker such that it formed a 20px halo around the selected party. The halo marker is purely a UI artefact that does not need to be saved on the server.
1 2 3 4 |
|
1 2 3 4 5 6 |
|
Animating the Halo Marker
For a final flourish, I used the AnimatedMarker
Leaflet plugin from OpenPlans to animate the halo’s movement on the map when a user selects different parties rather than simply making it reappear at a different location. AnimatedMarker
takes a Leaflet polyline
object as the first argument to its initialize function, and draws a marker at the beginning of the polyline, which it then animates along the polyline
at a speed (in meters/ms) that’s configurable via a second argument.
I needed to make a minor tweak to the plugin’s source code to support my needs: AnimatedMarker
does not allow setting the animation polyline
after the marker is initialized. In other words, it requires the animation path to be known before creating the marker. I wanted to create the marker around the currently selected party without knowledge of it’s future animation path, and to set the animation path dynamically as soon as a user selected a different marker — the path would be a segment from the current location to the center of the selected marker. To accomplish this, all I needed to do was reset the animation index in the marker’s setLine
method. This modification is available at my fork on github.
And ta-da! This is the end result: http://www.chicago-parties.meteor.com with source code for the complete application. You need to log in with a github account to create or RSVP to parties.