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). Conveniently, Firebase data is actually stored as JSON, which is helpful as we already know all about that!

Dealing with data in this way requires the use of a lot of event listeners,both on the map (e.g. for when a user clicks on the map), and on the database (e.g. for when data are added to the database). 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 control the website, but here is a hint: As a rule of thumb, event listeners 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.

Add an empty function called initDb()

Making your Firebase database

Follow these step-by-step instructions to set up your 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 as follows:

Please name it something unique rather than copying the below - it seems Firebase think that they are being attacked if they get too many simultaneous requests for the same name!

firebase form

Press next, disable Google Analytics (as shown below) and click Create Project:

firebase analytics

You will then see this as Google sets up your project:

firebasewheel

Once it has completed, you will see the below - click Continue.

firebase complete

This will take you to a screen that looks something like this:

firebase page

Click on Authentication in the menu on the left, which will open this screen:

firebase authentication

Click Set up sign-in method and select the pencil next to Anonymous (you have to hover your mouse to make it appear):

firebase anonymous

Select Enable then Save:

firebase anonymous 2

Make sure that Anonymous is now set to Enabled. Now go to Database in the menu on the left and scroll down to the section for Realtime Database (NOT Cloud Firestore) - select Create database

This will bring up a box like this:

Select Start in Test Mode and click Enable.

Now click on Project Overview in the menu on the left. We are now ready to start using our firebase! You should see something like this:

firebase get started

Click this button:

firebase add to web app button

The below form will appear - fill it in as below and click Register app:

firebase add to web app

This window will then open:

firebase add to web app

Now, we need to copy and paste part (but not all!) of this code - this is the credentials that we need to be able to access the database from your web page.

Select the firebaseConfig object (but nothing else!) and copy and paste it into the initDb() function in your web page

Connecting to your new database

Right, let’s get down to business:

Add the following elements to your HTML in order to import the components of firebase that we need:

<!-- Load Firebase -->
<script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-database.js"></script>

Add a call to initDb(); as the first line in initMap()

Add a global variable called myDb

Add the following three snippets to initDb immediately after the declaration of firebaseConfig:

This one initialises firebase using the information in your firebaseConfig object:

// initialise firebase
firebase.initializeApp(firebaseConfig);

This signs into firebase anonymously (which helps to prevent misuse of your website)

//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);
});

Finally, add this 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');

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 contain all of the data that you send to the database (because this is just a variable in Firebase, 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 empty array:

{ clicks: [] };

Part 2

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 the database. This means that we can collect clicks from people anywhere in the world in order to answer a variety of spatial questions.

In order to achieve this, we are simply going to listen for any click events on the map, and record the clicked locations in the database as GeoJSON. Simple!

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() . This is an elegant approach to programming that might be worth bearing in mind for Assessment 2…

Create an anonymous function as a click listener for the map (remember the e argument!),

We then need to get the click location (e.latlng)and convert it to GeoJSON to load into the database. There are a number of ways that we could make it into JSON:

  • manually construct a valid GeoJSON object that represents the e.latLng location
  • store the e.latlng location as a L.marker and use it’s in-built .toGeoJSON() function to convert to export the location directly to GeoJSON
  • use turf to create a turf.Point() object (which is stored as GeoJSON)

Ordinarily, I would prefer to use turf here (as it is the simplest), but because we are not using turf for anything else in this project, it seems silly to import the whole library just for one operation. As such, we will use the L.marker approach:

Create a variable in your anonymous function called geojson that creates a marker out of the e.latlng object using the L.marker() constructor, then call the .toGeoJSON() function of the resulting L.marker object in order to convert it to valid GeoJSON.

Once you have done that, add the following code snippet to your anonymous function, immediately after the creation of the geojson variable.

// 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 snippet loads the array into Firebase, which 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() 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!

Click a few times on your map (nothing will appear on the map) and see if successfully added to firebase! appears in your console - if so, then it is working!

Go back to the Firebase Console web page. Open the Database tab (click to enter your gis-web-2019 project then click Database on the left) and then select the Data tab.

You should now be able to see some data in your database!:

firebase console

interact with the data to understand how it is structured. You should be able to recognise it as an array of GeoJSON objects.

Connecting Firebase back to your Map

Okay, now our map is able to talk to Firebase, but so far nothing is happening on the map, which isn’t great for the user!

Next we need to make it so that Firebase talks back to the map. Once again, we will do this using event listeners. We have already used a listener for 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 array (because it is inside it)
		{
			"type": "Feature",
			"geometry": {
				"type": "Point",
				"coordinates": [
					-2.268848, 
					53.491019
				]
			},
		},

	]
};

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
	const newfeature = snapshot.val();
});

This snippet creates an anonymous function as the listener for 'child_added' on myDb, taking a single argument called snapshot, which is an object containing 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 Property that 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() Function that 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"
	},

}

Add the myDb.on... snippet above to your code (remember that listeners should normally be attached immediately after the creation of an object)

Add a line to the end of the child_added listener that creates a marker using the data stored in newfeature and adds it to the map (remember that you need to reverse the order of the coordinates between GeoJSON (longitude, latitude) and L.Marker (latitude, longitude)!

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.

Add a click listener using an anonymous function to each marker as it is created (again, listeners should be attached immediately after the marker is created)

The first thing that we need to do in the listener is to make sure that the browser doesn’t get mixed up between clicks on the markers and clicks on the map. because the marker is on the map, clicking on a marker will fire both click events - meaning that it will delete the marker and then add a new one in its place! To avoid this we need to stop the propagation of the click event, which prevents it being passed (propagated) to the map after it has fired on the marker.

Add the following snippet to your code to stop propagation of the click event from the marker to the map

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

Next, we need to create an object that reflects the structure of the database and refers 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 in the database, we simply want to set it to null in our new object and then pass it to firebase’s update() function.

Add the below snippet to your listener

// 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);
  }
});

Finally, we just need to remove the marker from the map!

Add the below snippet to your code in a location so that it only removes the marker if it has been successfully removed from the database

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

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 your 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 the new marker location:

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

Add a dragend listener to each marker using an anonymous function that updates the location of a marker in the database when it is dragged

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! Very handy for dissertations…

Some extra ideas…

As with last wek, I will provide you with some additional optional content to challenge yourself if you wish. Here are some ideas:

See if you can re-style the markers on the map

See if you can add some information from HTML <input> fields that is stored in the GeoJSON alongside each location

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.