Quantcast
Channel: 懒得折腾
Viewing all articles
Browse latest Browse all 764

Realtime Maps With Meteor and Leaflet

$
0
0

Realtime Maps With Meteor and Leaflet – Part One

DEC 27TH, 2013 | COMMENTS

this ‘map’ is actually a static image

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.

circles drawn over the static image

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
$ meteor create --example parties

$ cd parties

$ mrt add leaflet
leaflet: Leaflet.js, mobile-friendly interactive maps....

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
<div class="container-fluid">
  <div class="row-fluid">
    <div class="span4">
      {{> details}}
      {{#if currentUser}}
      <div class="pagination-centered">
        <em><small>Double click the map to post a party!</small></em>
      </div>
      {{/if}}
    </div>
    <div class="span8">
        {{> map}}
    </div>
  </div>
</div>
1
2
3
4
5
$(window).resize(function () {
  var h = $(window).height(), offsetTop = 90; // Calculate the top offset
  $mc = $('#map_canvas');
  $mc.css('height', (h - offsetTop));
}).resize();

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
map = L.map($('#map_canvas'), {
  doubleClickZoom: false,
  touchZoom: false
}).setView(new L.LatLng(41.8781136, -87.66677956445312), 13);

L.tileLayer('http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png', {opacity: .5}).addTo(map);

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
map.on("dblclick", function(e) {
  if (! Meteor.userId()) // must be logged in to create parties
    return;

  Session.set("createCoords", e.latlng);
  Session.set("showCreateDialog", true);
});
1
2
3
4
5
6
7
<template name="page">
  {{#if showCreateDialog}}
    {{> createDialog}}
  {{/if}}
  ...
  ...
</template>

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
var title = template.find(".title").value;
var description = template.find(".description").value;
var public = ! template.find(".private").checked;
var latlng = Session.get("createCoords");

Meteor.call('createParty', {
  title: title,
  description: description,
  latlng: latlng,
  public: public
}, function (error, partyId) {
  if (! error) { //party was successfully added to the server's mongo collection
    Session.set("selected", partyId);
    ...
  }
});

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 maptemplate’s created() callback.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var map, markers = {};

Template.map.created = function() {
  Parties.find({}).observe({
    added: function(party) {
      var marker = new L.Marker(party.latlng, {
        _id: party._id,
        icon: createIcon(party)
      }).on('click', function(e) {
        Session.set("selected", e.target.options._id);
      });
      map.addLayer(marker);
      markers[marker.options._id] = marker;
    },
    ...
    ...
  });
}

The final bit of fanciness here is my createIcon() helper function to create a lightweight DivIconthat 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
var createIcon = function(party) {
  var className = 'leaflet-div-icon ';
  className += party.public ? 'public' : 'private';
  return L.divIcon({
    iconSize: [30, 30], // set size to 30px x 30px
    html: '<b>' + attending(party) + '</b>',
    className: className
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.leaflet-div-icon {
  border-radius: 50%;
  border: none;
  line-height: 30px;
  font-family: verdana;
  text-align: center;
  color: white;
  opacity: .8;
  vertical-align: middle;
}

.leaflet-div-icon.public {
  background: #49AFCD;
}

.leaflet-div-icon.private {
  background: #DA4F49;
}

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

DEC 28TH, 2013 | COMMENTS

this is a Leaflet map with DivIcon markers

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
{
  _id: "22dQwpajD64LCv4QW",
  title: "1871",
  description: "Party like it's 1871!",
  latlng: {
    lat: 41.88298161317542,
    lng:  -87.63811111450194
  },
  public: false,
  owner: "52xdsNjprquesL2tQ",
  invited: ["52xdsNjprquesL2tQ", "ci7bzkJCpH9R7HCZK", "5qhRdKFcsmPnxZKBr"]
  rsvps: [
    {
      rsvp: "yes",
      user: "52xdsNjprquesL2tQ"
    },
    {
      rsvp: "maybe",
      user: "ci7bzkJCpH9R7HCZK"
    }
  ]
}

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
var map, markers = {};

Template.map.created = function() {
  Parties.find({}).observe({
    added: function(party) {/* see previous post */},
    changed: function(party) {
      var marker = markers[party._id];
      if (marker) marker.setIcon(createIcon(party));
    },
    removed: function(party) {
      var marker = markers[party._id];
      if (map.hasLayer(marker)) {
        map.removeLayer(marker);
        delete markers[party._id];
      }
    }
  });
}

Using a Halo Marker to Indicate Which Party Is Selected

a selected party

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
L.divIcon({
  iconSize: [50, 50], // set to 50px x 50px
  className: 'leaflet-animated-div-icon'
}
1
2
3
4
5
6
.leaflet-animated-div-icon {
  border-radius: 50%;
  border: none;
  opacity: .2;
  background: black;
}

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: AnimatedMarkerdoes 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.



Viewing all articles
Browse latest Browse all 764

Trending Articles