Meteor tutorial: plotting online users on a map

Here’s the functionality this tutorial will go through (in under 200 lines of code):

  • Users log in
  • Authenticated users see a listing of all other users
  • Authenticated users see one another’s locations on a map

This tutorial focuses solely on those goals, so there are no CSS styles added except to define the map. To be upfront, I’m still new to Meteor so this tutorial might not reflect best practices in some ways. I think this should be treated more like a learning exercise than a production-ready system. The source code is on github.

Generate a new Meteor app.

Run the following sequence of commands in shell:

# Check that Meteor is a recent version. Mine is > 1.3
meteor --version
# Update if the version if not up to date
meteor update
# Generate a simple app structure
meteor create onlineUsersMap
cd onlineUsersMap/

Remove autopublish

Autopublish isn’t recommended for production apps. Since I built this as an exercise in learning Meteor, I’m removing it here so I can learn how to work without it.

meteor remove autopublish

Delete some generated code

Remove all content from server/main.js, client/main.js, and client/main.html and replace it with the following:

// server/main.js
import { Meteor } from 'meteor/meteor';
// client/main.js
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import './main.html';
// client/main.html
<body></body

Add accounts packages

I have personally never used a framework that made it this easy.

First, run this in shell:

meteor add accounts-password accounts-ui

Then, edit client/main.html so it looks like this:

<body>{{> loginButtons }}</body

At this point you can start the server with “meteor run”, browse to localhost:3000 and test the auth system. You can keep this server running while editing source code. Changes are auto-pushes to the client and there’s often no need to refresh the page.

Publish which users are online

The intended functionality here is to create an “online users” list. The emails of online users will be shown on the DOM. When a user signs comes online or goes offline, the DOM will react to this.

First, add an Atmosphere package:

meteor add mizzao:user-status

Next, add a “publish” block to server/main.js:

Meteor.publish("userStatus", function() {
if (this.UserId) { // check if there is a logged in user
return Meteor.find({ "status.online": true })
} else { return [] }
})

The “status.online” attribute used in the lookup is provided by the meteor-user-status package. It won’t return all connected users, just those who have already authenticated & logged in. Also, when a user closes their browser window it will be set to “false” even if they haven’t clicked logout.

Subscribe on the client

Add the following code to client/main.js:

Meteor.startup( () => Meteor.subscribe("userStatus") )

The subscription doesn’t explicitly say what data is being subscribed to, just the name of the publication. In effect, though, this makes it so that clients can use “Meteor.users.find({})” and they will get some results.

Display on the DOM

This is going to be contained in a Template called “userList”. Add the following to client/main.js:

Template.userList.helpers({
usersOnline() { return Meteor.users.find({}) }
})

At this point in time only logged in users are published to the client. So on the client, there is no reason to add any restrictions on “find”. Next, edit client/main.html so it looks like this:

<body>{{> loginButtons }} {{> userList }}</body>
<template name="userList">
{{#each user in usersOnline}}
{{user.emails.[0].address}}
{{/each}}
</template>

Note the kind of non-intuitive way to get a user’s email address. “emails” is an array, and to get the first element “.[index]” is used (the dot at the front is necessary).

At this point, visiting localhost:3000 should show a list of emails for online users.

Get a map on the page

I have to give some credit to this great tutorial on meteorcapture.com for helping me piece together this section’s code.

Start off by adding a package from Atmosphere:

meteor add dburles:google-maps

Then add some boiler so that a map shows up on the page. In order to do this, you must create an app on the Google Developers console, enable Google Maps Javascript API, and find the browser key in the “credentials” section.

// client/main.html
<body>{{> loginButtons }}{{> userList }}{{> map }}</body>
<template name="map">
<div class="map-container">
{{> googleMap name="map" options=exampleMapOptions }}
</div>
</template>
<template name="userList">
{{#each user in usersOnline}}
{{user.emails.[0].address}}
{{/each}}
</template>
// client/main.js
Template.map.onRendered(function() {
GoogleMaps.load( {
v: '3', key: 'PUT A BROWSER KEY HERE'
} )
})
Template.map.helpers({
exampleMapOptions: function() {
if (GoogleMaps.loaded()) {
return {
center: new google.maps.LatLng(-37.8136, 144.9631),
zoom: 1
}
}
}
});
// client/main.css
.map-container { width: 800px; max-width: 100%; height: 500px; }

Make sure that the browser key is filled in with the Google credentials. At this point you should be able to open “localhost:3000” and see a map. If you don’t see one, open the debugger console in the browser and look for errors.

Geolocate users and update map

Add the following Atmosphere package:

meteor add mdg:geolocation

Then add the following to server/main.js.

// This is a server method that is called from the client. 
Meteor.methods({
updateUser(user) {
if (user._id && (Meteor.userId() == user._id)) {
var id = user._id
delete user._id
var keysAreValid = Object.keys(user).every((field) => {
return ["latitude", "longitude"].indexOf(field) != -1
})
if (keysAreValid) {
Meteor.users.update(id, { $set: user, })
} else { throw new Error("invalid update fields to user") }
} else { throw new Error("invalid userId to update user") }
}
})

This method expects an argument that is a hash with latitude, longitude, and _id keys. It checks that the ID belongs to the current signed in user, and that the remaining keys are either latitude or longitude (this is so that clients can’t just update anything). If all this goes well, it updates the user. Otherwise, it throws an error.

Also edit the “publish” method in server/main.js so it looks like so:

Meteor.publish("userStatus", function() {
if (this.userId) {
return Meteor.users.find(
{"status.online": true },
{ fields: { latitude: 1, longitude: 1 } }
);
} else { return [] }
});

Now it returns the “latitude” and “longitude” fields, which aren’t by default included with the publication.

This actually marks the conclusion of this tutorial’s server-side code. Next, some code is added to the “map” template in client/main.html:

// client/main.html
<template name="map">
<div class="map-container">
{{#unless geolocationError}}
{{> googleMap name="map" options=exampleMapOptions}}
{{else}}
Geolocation failed: {{geolocationError}}
{{/unless}}
</div>
</template>

Note that now, successful Geolocation is required to view the map. Unfortunately (or fortunately?) https:// is required to get the Geolocation to work. For developing locally, you can install a self-signed SSL certificate. See this StackOverflow answer for a tutorial. I’m personally developing on nitrous.io at the moment, so HTTPS is already set up. Once the site is loaded with HTTPS, there will a prompt to accept geolocation permissions. This needs to be accepted.

Finally, add code to client/main.js:

  • The autorun block calls “plotAllUsers”, which goes through all users and plots them on the map
  • The autorun block also calls “setupReactiveVarsAndGetCoords”, which creates a custom reactive variable on the window object and makes it change randomly on an interval
  • At the end of the autorun block, the server is pinged with an update containing user coords
  • If at any point the data on the server changes, the “Meteor.users.find({…})” cursor on the client will change, trigger autorun to re-run and the map data to be updated.

That’s pretty much, except for removing users from the map which I didn’t add yet. If nothing works, clone https://github.com/maxpleaner/spacedagger. You’ll still have to add a Google browser key to client/main.js, setup an HTTPS connection.