8. Participatory GIS with Firebase


A quick note about the practicals...

Remember to use Mozilla Firefox to do these practicals. Please do NOT use Microsoft Internet Explorer / Edge, this is not suitable for web development. Coding should be done in Notepad++

You do not have to run every bit of code in this document. Read through it, and have a go where you feel it would help your understanding. If I explicitly want you to do something, I will write an instruction that looks like this:

This is an instruction that tells you something to either think about, or do.

Shortcuts: Part 1 Part 2


In which we will use a Firebase database in order to make a participatory GIS platform.

Part 1

Getting Started with Firebase

One of the most interesting and exciting things about GIS on the Web is that it opens up the possibility to collect data from a wide variety of people all around the world. There are many different kinds of database and an in-depth study of database management is beyond the scope of this course. However, we are going to use one type of online database this week, and use it to store and manage some GIS data from our map.

We are going to use an online database called Firebase, which is owned by Google and is designed to let you store and retrieve data easily using JavaScript. Firebase is an example of a NoSQL database, meaning that data can be stored without having to pre-define a structure of tables to hold the data (as is the case with traditional SQL databases). Firebase data is actually stored as JSON, which is helpful as we already know all about that!

Before we get started, you will notice a bit of a step up in the level of challenge this week. I am expecting you to build upon your knowledge from the previous weeks and be able to work out where to put all of the event listeners that we will use to contro the website. As a rule of thumb, they should always go just after the object that they are attached to is created…

Right, let’s make a database…

Before you do anything, make a new web page using the usual template, name it something like week8.html

Making your Firebase database

Go to the Firebase homepage and click on SIGN IN (located in the top right-hand corner). If you have a Google account already (e.g. if you use GMail, Google Docs, Android etc…), you should be able to do this straight away. If not, you will need to follow the instructions to sign up with Google first.

Once you are signed in, click on GO TO CONSOLE (also in the top right-hand corner) and you should see a screen similar to the below (except that you obviously won’t have the gisandtheweb project listed!):

firebase console

Click on the Add Project button and fill in the form that appears:

firebase form

Once that is done, select the project card that appears:

And then select Database in the sidebar and click the Create Database button:

This will bring up a box like this:

Select Start in Test Mode and click Enable.

Now you have a database! There are just a couple of things that we need to do before we can use it though: we need to set the database type and setup some security rules.

You will be able to see a dropdown box near the top of the screen that says Cloud Firestore. Change it to Realtime Database:

Then select the Rules Tab, which shows the rules for accessing the database in JSON format. Set the rules JSON to this:

(You can copy and paste the rules from here for ease)

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

Now click Publish to save your rules and return to Project Overview in the sidebar on the left. We are now ready to start using our firebase! You should see something like this:

firebase get started

Click Add Firebase to your Web App:

firebase add to web app button

and this should appear:

firebase add to web app

This box contains all of the code that you need to:

  • Download the Firebase JavaScript Library
  • Configure your firebase connection
  • Initialise the firebase object

Make sure that you can broadly follow how this code works

Click COPY to add the code snippet to your clipboard.

Paste the <script> element that imports Firebase into the <head> of your web page

Paste the rest into a new JavaScript function called initDb()

Add a call to initDb() at the start of your initMap function

Securing your Firebase

Now, go back to the Firebase website. We need a way for the users to sign in so that we know who they are. Of course, we could just make the database completely open so that anyone can read and write to it, but that would leave it open to abuse from script-kiddies who will maliciously fill your database with data for no good reason, possibly resulting in you being charged by Google! Of course, we also don’t want to have to get each participant in a PPGIS survey to sign up for a user account before taking part, so we will use a middle ground and use anonymous sign-in. Simply, this means that each user is automatically signed in with a temporary user account, they won’t even notice, but this added level of security means that Google will notice if an unexpectedly high level of data starts pouring into your database, and intervene accordingly before any damage is done…

Fortunately, as complicated as the above sounds, it is actually very straightforward to achieve, all that you need to do is go to Authentication in the Console and select the SIGN-IN METHOD tab:

Firebase Authentication

Simply click on Disabled next to where is says Anonymous and this box will appear - set the Enable switch to on and click Save.

Firebase Authentication

This means that you allow anonymous sign-ins in your database. Once this is done, all you need is the following snippet of code:

//sign in anonymously - this helps stop your database being abused
firebase.auth().signInAnonymously().catch(function(error) {
	//if there's a problem, print the error to the console
	console.log(error.message);
});

Once the user is signed in, we will add one more line to get a global reference to the database itself (this is just for our own convenience):

//global reference to your 'clicks' database
myDb = firebase.database().ref().child('clicks');

Add the above two snippets to your initDb() function

Remember to initialise myDb as a global variable

The above line does something important here, it provides the connection to the database with the firebase.database().ref() section, and then creates a new child (which is really a JSON Array) called 'clicks', which will be where we store our data (you can call it whatever you want). In essence, this has changed our database (which is simply stored as JSON) from being empty object like this:

{ };

to an object that contains a single key (clicks) with the value of an (initially) empty array:

{ clicks: [] };

Once we add some data to the database, it will be loaded into the clicks array as GeoJSON objects, like this:

// this is the JSON object for the database
{ 
  
  // this is the JSON array for the data
	clicks: [
    
    //this is a GeoJSON object representing a click
    {
      "type": "Feature",
  		"geometry": {
    		"type": "Point",
    		"coordinates": [
          -2.268848, 
          53.491019
        ]
  		},
    },
    
    //then there would be another, and another in the array
		...
	] 	//close the clicks array
};	//close the object

Connecting Your Map to Firebase

Now we have a connection to firebase and our map is ready to go, it is time to connect the Map and Firebase together. We are going to do this using Events. Our goal is to let users click on the map in order to add a point to it. In order to achieve this, we are simply going to listen for any click events on the map.

Anonymous Functions

We will now add a click listener to the map. You will remember from Week 2 that we are used to adding listeners like this:

// set listener for click event on the map
map.on('click', myClickListener);

/**
 * Handle a click event on the map
 */
function myClickListener(e) {
  
  //do something here...
});

In this case we have created a function myClickListener and assigned it as the listener to the click event using map.on(). This week, we will use a slightly different approach known as an Anonymous Function. Because our listener is only going to be used for a single purpose (being the click listener for the map) and will therefore never be referred to by name except when passed as an argument to map.on(), we can actually use a shorthand method that avoids giving it a name:

// set listener for click event on the map		
map.on('click', function(e) {

  //do something here...
});

Can you see what is happening there? We are defining the function (without a name) at the location where it is being passed as an argument. This way you are not storing a reference to the function, merely creating it “in place”, passing the actual function (rather than its name to map.on() . In a lot of ways, this is not really any different from if you were passing a number like this:

//pass the number without storing in a variable first
myFunction(2);

Instead of like this:

//store the number in a variable...
var myNumber = 2;

//...then pass it
myFunction(myNumber);

Once again, in this example, you are simply putting the value directly into the function call as an argument, rather than a variable containing it. It makes no real difference whether the variable holds a function or a number.

Create an anonymous function as a click listener for the map (remember the e argument!), which contains the following commands:

// create a marker
var geojson = L.marker(e.latlng).toGeoJSON();

// push the data into the database, set inline callback
myDb.push(geojson, function(error) {

  //if no error is returned to the callback, then it was loaded successfully
  if (!error) { 
    console.log("successfully added to firebase!");

  // otherwise pass the error to the console
  } else {
    console.error(error);
  }
});

This code is getting the current location of the click using the event object (e) and extracting the location of the click using the .latlng property. We then convert the marker to GeoJSON format (using the .toGeoJSON() function).

Once we have our GeoJSON, we just need to load it into Firebase. Fortunately, this is very easy because, as we know, we are simply adding our data to a JSON Array called clicks. As with any other JavaScript Array, we can simply add an element to it with the (push())[https://www.w3schools.com/jsref/jsref_push.asp] function. The only difference to a normal use of array.push() is that for firebase, we pass two arguments rather than one:

  1. The element that we want to push (our point location)
  2. a callback function for when the data has been loaded into the database that tests if an error variable is populated (which only happens if there is an error) and print either a success or an error message to the console depending upon the outcome

That’s it! At this stage, you have hooked up your map to the database, meaning that any clicks that you make on the map will appear in the database. However, we have not yet done anything to make those clicks appear on the map!

Nevertheless, click a few times on your map (knowing that nothing will appear on the map) and then go back to the Firebase Console web page. Open the Database tab (click to enter your webgis project then click Database on the left) and then select the Data tab. You should now be able to see some data in there:

firebase console

You can also expand each entry to see the data inside…

firebase console

Not bad eh!!

Part 2

Connecting Firebase back to your Map

Okay, now our map is able to talk to Firebase, we can start making it so that Firebase talks back to the map. Once again, we will do this using event listeners. We have already used a listener or a click event on the map in order to store our data in the database, now it’s time to draw the click onto the map as well!

We could at this stage simply add markers to the map in the click event as well, but this could lead to problems if the data is not loaded into the database successfully. It is better, therefore, if we can only draw the marker onto the map once it has successfully been added to the database, and we will do this using a listener for the child_added event on the database. This might seem like an odd name, but it is common in data structures to think in terms of parents and children. Essentially, parents contain children, in our case:

{ 
  
  // this array is the PARENT
	clicks: [
    
    //this GeoJSON object is a CHILD of the parent (because it is inside it)
    {
      "type": "Feature",
  		"geometry": {
    		"type": "Point",
    		"coordinates": [
          -2.268848, 
          53.491019
        ]
  		},
    },
    
    //all subsequent GeoJSON objects would also be children of the array
		...
	]
};

So our click GeoJSON points are the children of our clicks array in the database. Simple!

As luck would have it, the syntax for adding a listener to the database is pretty much the same as adding one to our map:

// Listener for when a location is added to the database - add it to the map as well
myDb.on('child_added', function(snapshot) {

		// get the click location from firebase as a GeoJson
    var newfeature = snapshot.val();

    // add the point to the map (remembering to flip the coordinates)
    var marker = 
	}
);

This snippet creates an anonymous function as the listener for 'child_added' on myDb, taking a single argument called snapshot, which contains the data that was added to the database (a copy of all or part a database is often referred to as a snapshot).

snapshot is an object from firebase that contains a number of properties and functions, the important ones of which are:

Function / Property Description
snapshot.key() Returns a unique id for that particular datum in the database (e.g. -LaK5fOorZio6ujt5Gfj). This is used to help firebase find the data that you are working with (adding / editing / deleting etc.).
snapshot.val() Returns the data object stored at that location (our GeoJSON point in this case)

The terminology key and val (value) are used bacause this is the terminology used in JSON objects (which is the format in which firebase stores data). Inside firebase, therefore, the data would be stored in this form (with the unique ID as the key and the GeoJSON object as the value):

{
	LaK5fOorZio6ujt5Gfj: {
    "geometry": {
      "coordinates": [
        -2.269535,
        53.482274
      ],
      "type": "Point"
    },
    "type": "Feature"
  },
  ...
}

Clearly the code snippet for the 'child_added' event listener is incomplete! Paste it into your web page and console.log() the contents of newfeature to work out how to populate marker with an L.Marker() constructor (remember .addTo(map)!). Add the result to your code.

Now try clicking on your map again, you should have a fully functional map! Click a few times then close the map in the browser and open it again, the points should still be there! Check in on the website, you should be able to see it!

Er… hang on a minute…

Hang on - something strange is happening here… Why does all of the data appear when you reopen the page? We have only told it to add them as they are created… Well, actually, the child_added event is called for each item in the database when you connect, and THEN for every new one as it is added, so you get a copy of the entire dataset at the point of connection, which is handy!

Adding More Events

Now that we can add points to a map we can start collecting data! However, we could make this much more effective if we add a little more interaction, such as letting users edit or even delete their markers. Fortunately, this is quite straightforward, it is simply a matter of adding some more events! here is the extra functionality that we want to add in:

  • Click on a point to delete it
  • Drag a marker to update the database with its new location

To achieve this, we clearly need to add two listeners to our markers

  • click listener to be called when a marker is clicked
  • dragend listener to be called when a marker is dragged (specifically, when the drag stops)

Let’s get to it:

Deleting a marker on click

We need to add a line of code that will add a new listener to every single marker. We must therefore add a new click listener to the marker as it is created. Deleting from the database is (as far as Firebase is concerned), simply a matter of updating the relevant part of the database to null. Here are the steps:

1: Stop the Propagation of the click event, so that it doesn’t fire on the map after it has fired on the marker (which would happen by default because the marker is located on the map). Without this line, therefore, the click event would be fired on the marker (deleting it) and then on the map (putting it back again!):

// prevent the click event from being fired on the map as well
L.DomEvent.stopPropagation;

2: Create an object reflecting the structure of the database and referring to the current data point (accessed in the object as "/clicks/" + snapshot.key, where clicks is the name that we gave to the collection and snapshot is the object returned from firebase when the data was correctly inserted). To delete an object, we simply want to set it to null in our new object and then pass it to firebase’s update() function:

// initialise an empty object
var updates = {};

// set the current data point to null
updates['/clicks/' + snapshot.key] = null;

// pass the new data object to firebase to overwrite that stored on the server
firebase.database().ref().update(updates, function(error) {
  if (!error) {
    console.log("successfully deleted point!");
  } else {
    console.error(error);
  }
});

3: Finally, remove the marker from the map:

// remove the marker from the map
e.target.remove();

Put the above three steps into an event listener for the click event on each marker (the key is putting it in the correct location in your code - the listener needs to be added to the marker as it is created)

Does it work?

Updating a marker on dragend

Though the code to update Firebase looks a bit odd, it is actually relatively simple (create an object to with which to replace the existing object in the database and use it to ovewrite the contents of the database). Now we have successfully managed to delete a marker from the database using the above, it should be relatively simple to allow the users to update the location of the markers by dragging them.

To achieveve this, we will firstly need to allow the markers to be dragged.

Update the L.marker constructor to this:

// add the point to the map (remembering to flip the coordinates)
var marker = L.marker(
  [ newfeature.geometry.coordinates[1], newfeature.geometry.coordinates[0] ], 
  { draggable: true }
).addTo(map);

See how we have added the option draggable: true, meaning that the user can now drag the markers around. Now all that we need to do is get the new location from the drag event and update the database with it. This will, obviously, be exactly the same as the click listener, but instead of updating our new data object to null, we will set it to

updates['/clicks/' + snapshot.key] = e.target.toGeoJSON();

If that all worked, you should now have a map that looks and works a little like this:

If so, then well done! You have just created your own web-based Participatory GIS!

Finished!

Some of the material on these pages is derived from the excellent Leaflet Tutorials and Mozilla Developers websites. Mozilla Developers by Mozilla Contributors is licensed under CC-BY-SA 2.5.