Friday, 20 May 2011

Leaflet

Edit: The code below is for a very early version of Leaflet and is not the best way to use Leaflet now. Much has been improved and extended. The range of plugins has grown enormously too.  Take a look at the Leaflet site for more information.

Vladimir Agafonkin announced that CloudMade have released Leaflet, a new open source JavaScript library for displaying map tiles as a slippy map. I've used OpenLayers up to now, so I thought I'd take a look at it.

I have a love-hate relationship with JavaScript. It is great that there is a powerful language that runs in browsers, but the environment it runs in is horrible: DOM, various browsers supporting various features, a fiddly run-time that makes debugging horrible etc. Firebug helps.

I decided that a reasonable test was to reproduce a slippy map page I already have to see how it works with Leaflet. That would help me compare with OpenLayers, but does run the risk of me using Leaflet in an OpenLayers way, which I hope I've avoided. It also only covers a small part of what Leaflet offers, but I seems like a start.

My page needs to display a slippy map with markers on it. A side bar will display details and photo for any marker that is clicked and a small message bar will explain the current situation. To get the layout I used a very simple HTML page and a simple style sheet. I tend to not embed styles in HTML or JavaScript, but to use either IDs or Classes to apply styles and declare these in separate style sheets, sometime more than one. I rarely embed JavaScript in HTML either, other than calling functions in events, so JavaScript goes into separate files too. All of these files are available to download, see the links at the end. The Leaflet JavaScript and style sheet are also included. The map is intended to show the locations and information about a series of sculptures around the City of Hull, as part of the Philip Larkin celebrations. They are no longer in place, but I had the data so I used it.

The first part seemed very easy: display a slippy map. The map is displayed in an HTML div. Ours has an id of map. The code to make the map appear is:

var map;
var hull = new L.LatLng(53.775, -0.356);
function initmap() {
map = new L.Map('map');
var osmUrl='http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
var osmAttrib='Map data © openstreetmap contributors';
var osm = new L.TileLayer(osmUrl,{minZoom:8,maxZoom:18,attribution:osmAttrib});

map.setView(hull,11);
map.addLayer(osm);
}

That is a fairly simple piece of code to display a slippy map. The variables map and hull were defined outside of the function so they are available to other functions as global variables. The single most confusing part is probably the line

map = new L.Map('map')

The first part is a map variable used throughout the code. The second (L.Map) is the leaflet-defined object for a map. This defines how the map object behaves. The last part ('map') is because the div that displays the map has an id of map, which is in the HTML code. The URL includes {s} for the server prefix needed, {z}, {x} & {y} for the zoom, x and y tile coordinate. This allows various tile providers' tile structure to be used.  Now the map needs markers.

Adding markers with Leaflet is very straightforward when you have the longitude and latitude for each marker. However, displaying a map with hundreds or even thousands of markers slows things down a lot, so I like to only add the markers that are visible in the currently displayed part of the map. You can get this by calling the getBounds() method of the map object. This returns the north and south latitude and the east and west longitude that describe the edges of the displayed map. Of course this can change as the slippy map is dragged to a new position or the map is zoomed in or out. So the way to handle this change is to respond to map events. The one which seems to fit the bill for us is moveend. This is called whenever the map is moved, including zooming. We supply a function that is called whenever the moveend event is fired. This is achieved by adding the line

map.on('moveend', onMapMove);

Now, whenever the the map is moved our function onMapMove will be called. In our example onMapMove simply calls the function askForToads() which gets the location of the markers we want. I use this extra layer of functions to make debugging easier.

We still don't know where the markers are to be placed, I do that by using AJAX to request the information about all the markers that fall within the bounds of the current map display. askForToads calls the getBounds() method of our map object, extracts the north, south, east and west coordinates and wraps it up into a call to a PHP routine that finds the list of markers and returns this in a JSON format. This returned list is processed by the stateChanged() function which is called as part of the AJAX process. The JSON provides a neat way to return this data, because simply calling eval on the returned string turns it into an array of, in our case, marker data. I then remove any markers already shown and recreate the new markers one at a time, attaching the data for that marker to a data field and recording each marker in an array (toadlayers.push). Working through the array allows me to delete the markers when the map changes.

You now need to respond to each marker that is clicked. This means that the click event needs to be responded to. This needs the line

toadmark.on('click',markerClick);

to ensure that any marker that gets clicked is responded to by the markerClick() function. Here the details from the data field are displayed in the side bar.

Throughout the process the message div displays messages about the progress of fetching the marker info and how many toad markers are being displayed.

This may seem a lot of work to display the markers, but we are making two substantial changes to the normal system of displaying markers, firstly we are only displaying some markers. If there were thousands of potential markers spread across a wide area this would make the process manageable by only displaying a few at a time. Secondly instead of just displaying a popup we are making something outside of the map div change, which opens up a wide range of actions.

How does this compare with OpenLayers? Most of it is simpler. OL provides an interface to AJAX which makes the process a bit simpler. It also puts a collection of markers together into a single layer which is easier to manage - Leaflet makes each marker independent so I had to keep track of the markers I had added. I like Leaflet, and for many things it is something I will use.

You can find the code examples on github If you want the database definition or even the data I can supply that too. You can see the working example here.

26 comments:

Adx said...

Your example is very impressive !
I am beginning to play with Leaflet myself, but can't find how to display markers from a SQL DB. I have done this previously in google maps, but can't seem to replicate the same in Leaflet.
Any advice or pointers would be greatly appreciated !

Chris Hill said...

There are basically 2 ways to add Leaflet markers to a web page from a SQL DB:

1, extract the data as the page is being created and use this populate an array to use in the markers. This means the page needs to be created with a scripting language such as ASP, PHP or the like.

2. use AJaX to send a request from the web page to some of your code which will extract the data and return it to your web page for use.

I tend to prefer the AJaX route. It is more powerful and dynamic, but probably a bit harder to master. There is a good starter guide to AJaX on the W3Schools website. I return the data as JSON or GeoJSON data which works well.

Both routes need a good grasp of JavaScript because all of the power of Leaflet is controlled by JavaScript. You will benefit from a debug tool - I use the Firebug addon to Firefox, without it JavaScript debugging is very hard work.

HTH

Anonymous said...

This is great information, but I'm confused on how you keep track of the markers. I'm trying to keep track of multiple markers on a map so when one is clicked, I "know" which one it is and can call functions based on which marker is clicked. I tried creating an array of LatLng coordinates and tried to compare the marker's coordinates, but it doesn't seem to work. Would you know how to identify a marker when it is clicked?

Chris Hill said...

Leaflet passes you the marker that is clicked so you don't need to keep track of them.

Take a look at the JavaScript in swan.js on my github store https://github.com/chillly/Leaflet-Toads

In there look at the stateChanged() function. There the data for each marker (toad) is added to the marker as the marker is created, so it is stored with the marker in the data variable.

Later in the markerClick() function the parameter e is passed which has the marker that is clicked as the variable target. If you use e.target.data you get to the data attached to the marker when it was created above.

HTH

Anonymous said...

That's great! I try to reproduce the example but I can't get the following line:
$picquery="SELECT photo FROM toadpiclist t JOIN toadpics p on t.picid=p.picid where t.toadid='{$row["toadid"]}'";

I would like to have a list of pictures to display too... How does the SQL table look like?

Chris Hill said...

Vokabb,
Here's the SQL for the three tables:

--
-- Table structure for table `toad`
--

CREATE TABLE IF NOT EXISTS `toad` (
`toadid` int(11) NOT NULL auto_increment,
`toadname` varchar(50) NOT NULL,
`lon` double NOT NULL,
`lat` double NOT NULL,
`sponsor` varchar(150) default NULL,
`designer` varchar(50) NOT NULL,
`artist` varchar(50) NOT NULL,
PRIMARY KEY (`toadid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COMMENT='where the toads are' AUTO_INCREMENT=54 ;


--
-- Table structure for table `toadpiclist`
--

CREATE TABLE IF NOT EXISTS `toadpiclist` (
`toadid` int(11) NOT NULL,
`picid` int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;


--
-- Table structure for table `toadpics`
--

CREATE TABLE IF NOT EXISTS `toadpics` (
`picid` int(11) NOT NULL auto_increment,
`photo` varchar(50) NOT NULL,
PRIMARY KEY (`picid`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COMMENT='List of photos of toads, maybe more than one per toad' AUTO_INCREMENT=41 ;

The table layouts are crude. I allowed for the udea that there might be more than one picture for each toad by decoupling via the table ToadPicList. The table ToadPics gives the filename to display.

I hope that helps.

Anonymous said...

Thanks Chris,

"I allowed for the idea that there might be more than one picture for each toad by decoupling via the table ToadPicList."

Ok, I guess this is what I didn't get. I also wanted to know if you stored the pictures as blobs or filenames. Now I have all my answers, thanks!
Ludovic

Olivier said...

You can group markers with LayerGroup object. It allows you to manage a group of object (marker or others...)

Chris Hill said...

Thanks Oliver,
You are, of course, quite right. The LayerGroup didn't exist when I originally wrote the code and the post.

I have used the LayerGroup and it works well.

PeterT said...

A big picky but there are two typos in the line:

var osm = new L.TileLayer(osmUrl,{minZoom; 8, maxZoom;18, attribution: osmAttrib});

after minZoom and maxZoom there should be colons, not semicolons, e.g.

var osm = new L.TileLayer(osmUrl,{minZoom: 8, maxZoom: 18, attribution: osmAttrib});

Chris Hill said...

Computers are picky, so you right to point it out, thanks. I've corrected it.

I might need to review the code as Leaflet has moved on. Integration with jQuery for the initialisation and ajax is useful too.

Valerio said...

Thanks for the example!
But this solution is free also for commercial use?

Chris Hill said...

Valerio,
My code is Open, you can freely use it or copy part of it for any purpose, including commercial use. It would be nice if you mention my code in your app (e.g. link to this blog), but you don't have to. I'm pleased you find it useful.

Nate Iler said...

Great post!!!

It looks like your ajax response is in XML format. I'm looking to do something similar, but using their GeoJSON layer support:
(http://leaflet.cloudmade.com/examples/geojson.html --and -- http://leaflet.cloudmade.com/examples/geojson-example.html). The issue I'm having is removing a layer...like your loop method, I'm used to storing the layers in an array and looping through to remove them. Do you have any insight on how one might remove a GeoJSON layer?

Chris Hill said...

The PHP code returns JSON, not GeoJSON. As another comment points out I would now group all the markers into one LayerGroup, which didn't exist when I wrote the post. You can then delete the group without the loop. You can remover a single GeoJSON layer, or any other layer in one go. Check out the leaflet documentation, which keeps improving.

Nate Iler said...

Hey Chris,

I figured out what I was doing wrong. Thanks for your example.

Arnie Shore said...

Thanks SO much - I'm a new Leaflet devotee!

Now I'm adding an icon to the map with the usual
L.marker(e.latlng).addTo(map)
But I want to remove any prior icon - allowing just one. How do I do that?

Chris Hill said...

Arnie,
This example is now more than a year old and is looking rather out-of-date. I'll try to use create a new example to use some of the new facilities that the latest version of Leaflet offers.

If you are just going to ever have one marker on the map, store it's value in a global var (say as mark) and use map.removeLayer(mark) to remove the marker then create a new one. You can also move a marker by changing its lon / lat so you don't need to remove one and add a new one, just move the existing one. If you do want multiple markers, add them to a layerGroup then you can remover or hide them as a single entity.

Leaflet's documentation is much better now than when I started, but solid working examples do still seem useful. Judging by the comments and hits here on a 15 month old blog post people still want help.

Robert said...

Hi Chris,
in case you manage a self-hosted WordPress site: try installing my plugin "Leaflet Maps Marker", which allows you to pin, organize & show your favorite places through OpenStreetMap, Google Maps, Google Earth (KML), Bing Maps, GeoJSON, GeoRSS or Augmented-Reality browsers easily. More details at the plugin website at www.mapsmarker.com
Regards,
Robert

tingenek said...

Thanks for the examples - I appreciate things have moved on but you've written some nice plain JS that's easy to understand and it's a good starting place for our project.
BTW Bath has done a similar thing with Lions and with Pigs. I'll have to suggest Toads!

Anonymous said...

leaflet is fantastic.

and thanks for your pointers.

if i may, what's the SQL for grabbing data only visible within the viewport.

and can one remove points and add new ones during zoom and pan.

Chris Hill said...

This is now badly out of date and Leaflet has developed such a lot, but this does still work.

The SQL is in the php script ajxToad.php in the server folder in the github. I pass the bounding box (west, south, east, north) to the ajax call which then compares the points to see which ones fall into the current viewport.

I have written a more recent post about using the GIS functions in MySQL to do a similar thing but for linestrings rather than points. Testing points by hand as I do here is fairly easy, but testing a linestring would mean testing every point that makes up the linestring, and a polygon even more. The GIS functions of MySQL, and even better ones of PostgreSQL, make light work of this, but I started simply.

If you group your markers together in a LayerGroup (not available when I wrote the post) you can remove all of the markers with one call: clearLayers(). As the pan and zoom changes the viewport I remove the markers and then request a new set for the new viewport. You can use events to MoveStart and ZoomStart to remove markers at the start of zoom/pan rather than when the zoom/pan ends as I do.

There is a Google group and an IRC channel for Leaflet where help is available.

IonuČ› said...

Hello,
I am a beginner in WMA developing, though not alien to geospatial concepts. I am trying to do an interactive map using leaflet and an trying to follow the steps suggested by you regarding the connection to the database and generation of points based on the existing records.
Out of the instructions set and even the demo, I did not understand exactly where the database files are stored in relation to the rest of the files. Could you help me in this regard? I saw in the ajxToad, the following sequence: include ".. / include / db.inc". Does it have something to do with the problem illustrated?
I hope it's not too much of a trouble and thank you for your understanding and help

Chris Hill said...

Hu Pascu,
The database is accessed by the server-side. The db.inc is where I stored variables that hold the database name, db user name and password, so I didn't share the real contents of course.

The point of ajax is to allow data from the server or database to be requested without refreshing the web page. The client web page does not access the database directly but requests data from the server using ajax.

HTH

IonuČ› said...

And if I have the database on the same server, how would that change the code?
I am using Xampp and created a db called toad with user and password on localhost.
but just removeing the 'include' and replaceing the variables with my values for user password and database, still won't work

Chris Hill said...

As I understand it Xampp is a way to install Apache, MySQL and PHP all at once. That means your database will be a MySQL one, the host will be 'localhost', the database will be 'toad' and you need to add your own username and password. That should be fine. The database schema needs to match the one I used (see previous comments). I suggest you try creating a really simple PHP page that accesses the database to return a simple string to be sure you have everything working then move up to ajax calls.

I hope you get it working, but debugging someone else's code that I can't see, on a server that I can't access is near impossible. My example is just a suggestion. Try http://switch2osm.org too.