This walkthrough builds on some of the previous sections of the lesson to show how you can add interactive GeoJSON layers to your web map using Leaflet. You will build a map containing your Philadelphia basemap tiles and two GeoJSON layers on top representing urban gardens and food pantries (i.e., food banks). A user can click one of the gardens or food pantries to see the name of the feature below the map (as an alternative to using popups). The clicked feature changes color while it is selected. Hopefully, you can think of many ways to apply these pieces of functionality to the web map you're building for your term project.
Note that this is a sample dataset culled from OpenStreetMap and is not a comprehensive list of these features in Philadelphia. If you know of any other gardens or food pantries, please add them in OpenStreetMap (more about this in Chapter 9)!
Before continuing, download and unzip the data for this project [1]. Copy its contents into your Jetty home folder, which should have a path such as
c:\Users\{username}\GeoServer 2.x.x\webapps\geog585\
This is the same folder where you saved your Lesson 6 walkthrough and where your local stylesheet style.css (required for this exercise) is located.
This folder contains two JavaScript files containing GeoJSON data. gardens.js holds a gardensData variable with polygon GeoJSON and pantries.js holds a pantriesData variable with point GeoJSON.
There are also two SVG (scalable vector graphics) files that will be used for symbolizing the food pantries. The yellow symbol will be for the unselected features and the blue symbol for the selected features.
There are a couple of ways that you could get this kind of data for your own applications.
In both cases you would need to either save the data as a JS file and define the GeoJSON as a variable (the approach we took here), or use an extension like Leaflet AJAX to read the data directly out of the file (beyond the scope of this course).
To get icons, all the icons available in QGIS are available on your machine in a folder named something like:
C:\Program Files\QGIS <name of your version>\apps\qgis\svg
I used the open source program Inkscape to change the color of the icon.
Before diving into the JavaScript code, create an empty text file and insert the following code. Then save it as lesson7.html next to all the other files you just downloaded and copied into your home folder.
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Food resources: Community gardens and food pantries</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" type="text/css" crossorigin=""> <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js" crossorigin=""></script> <script src="gardens.js"></script> <script src="pantries.js"></script> <link rel="stylesheet" href="style.css" type="text/css"> <script type="text/javascript"> . . . </script> </head> <body onload="init()"> <h1 id="title">Food resources: Community gardens and food pantries</h1> <div id="mapid"></div> <div id="summaryLabel"> <p>Click a garden or food pantry on the map to get more information.</p> </div> </body> </html>
If you open this, you should see a blank map frame surrounded by an HTML title and descriptive text. In the script and link tags, notice that we are loading in Leaflet code and stylesheets, as well as the gardens and pantries.js files.
Now let's add the Leaflet JavaScript code that creates the map and layers.
var map; function init() { // create map and set center and zoom level map = new L.map('mapid'); map.setView([39.960,-75.210],14); // create and add the tile layer var tiles = L.tileLayer('https://<URL of your S3 bucket>/PhillyBasemap/{z}/{x}/{y}.png'); tiles.addTo(map); . . . }
https://geog585-somestring.s3.someregion.amazonaws.com/PhillyBasemap/{z}/{x}/{y}.png
var gardenLayer; var pantryLayer; var selection; var selectedLayer; . . .Nothing is happening with these yet, but it's important that you understand their future purpose in the code:
// define the styles for the garden layer (unselected and selected) function gardenStyle(feature) { return { fillColor: "#FF00FF", fillOpacity: 1, color: '#B04173', }; } function gardenSelectedStyle(feature) { return { fillColor: "#00FFFB", color: '#0000FF', fillOpacity: 1 }; } . . .
// handle click events on garden features function gardenOnEachFeature(feature, layer){ layer.on({ click: function(e) { if (selection) { resetStyles(); } e.target.setStyle(gardenSelectedStyle()); selection = e.target; selectedLayer = gardenLayer; // Insert some HTML with the feature name buildSummaryLabel(feature); L.DomEvent.stopPropagation(e); // stop click event from being propagated further } }); } . . .
// add the gardens GeoJSON layer using the gardensData variable from gardens.js var gardenLayer = new L.geoJSON(gardensData,{ style: gardenStyle, onEachFeature: gardenOnEachFeature }); gardenLayer.addTo(map); . . .
// create icons for pantries (selected and unselected) var pantriesIcon = L.icon({ iconUrl: 'pantries.svg', iconSize: [20,20] }); var selectedPantriesIcon = L.icon({ iconUrl: 'pantries_selected.svg', iconSize: [20,20] }); // handle click events on pantry features function pantriesOnEachFeature(feature, layer){ layer.on({ click: function(e) { if (selection) { resetStyles(); } e.target.setIcon(selectedPantriesIcon); selection = e.target; selectedLayer = pantryLayer; // Insert some HTML with the feature name buildSummaryLabel(feature); L.DomEvent.stopPropagation(e); // stop click event from being propagated further } }); } // add the pantries GeoJSON layer using the pantriesData variable from pantries.js pantryLayer = new L.geoJSON(pantriesData,{ pointToLayer: function (feature, latlng) { return L.marker(latlng, {icon: pantriesIcon}); }, onEachFeature: pantriesOnEachFeature } ); pantryLayer.addTo(map); . . .
// handle clicks on the map that didn't hit a feature map.addEventListener('click', function(e) { if (selection) { resetStyles(); selection = null; document.getElementById('summaryLabel').innerHTML = '<p>Click a garden or food pantry on the map to get more information.</p>'; } }); . . .
// function to set the old selected feature back to its original symbol. Used when the map or a feature is clicked. function resetStyles(){ if (selectedLayer === pantryLayer) selection.setIcon(pantriesIcon); else if (selectedLayer === gardenLayer) selectedLayer.resetStyle(selection); } . . .Separate lines of code are needed above for the pantries and gardens layers because points represented by icons and polygons have different styling syntaxes in Leaflet.
// function to build the HTML for the summary label using the selected feature's "name" property function buildSummaryLabel(currentFeature){ var featureName = currentFeature.properties.name || "Unnamed feature"; document.getElementById('summaryLabel').innerHTML = '<p style="font-size:18px"><b>' + featureName + '</b></p>'; }The above function brings in the currently selected feature and reads its "name" attribute. It then gets the HTML element with the ID of "summaryLabel" and sets its innerHTML to a carefully constructed string of HTML into which the name (represented by the variable featureName) is inserted. Note that if our gardens and pantries layers had different attribute field names (such as "PANTRYNAME" and "GARDENNAME" then we would need to add more code above to handle those cases.
If your page does not work, carefully compare your code to the full code below to make sure you have inserted everything in the right place. Also:
An OpenLayers 3 version of the walkthrough code [3] is available for the curious. Note that the GeoJSON files must be adjusted for this version of the walkthrough to function. It must be pure GeoJSON and not contain any declared variables or JavaScript code.
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Food resources: Community gardens and food pantries</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" type="text/css" crossorigin=""> <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js" crossorigin=""></script> <script src="gardens.js"></script> <script src="pantries.js"></script> <link rel="stylesheet" href="style.css" type="text/css"> <script type="text/javascript"> var map; function init() { // create map and set center and zoom level map = new L.map('mapid'); map.setView([39.960,-75.210],14); // create and add the tile layer var tiles = L.tileLayer('https://<URL of your S3 bucket>/PhillyBasemap/{z}/{x}/{y}.png'); tiles.addTo(map); var gardenLayer; var pantryLayer; var selection; var selectedLayer; // define the styles for the garden layer (unselected and selected) function gardenStyle(feature) { return { fillColor: "#FF00FF", fillOpacity: 1, color: '#B04173', }; } function gardenSelectedStyle(feature) { return { fillColor: "#00FFFB", color: '#0000FF', fillOpacity: 1 }; } // handle click events on garden features function gardenOnEachFeature(feature, layer){ layer.on({ click: function(e) { if (selection) { resetStyles(); } e.target.setStyle(gardenSelectedStyle()); selection = e.target; selectedLayer = gardenLayer; // Insert some HTML with the feature name buildSummaryLabel(feature); L.DomEvent.stopPropagation(e); // stop click event from being propagated further } }); } // add the gardens GeoJSON layer using the gardensData variable from gardens.js var gardenLayer = new L.geoJSON(gardensData,{ style: gardenStyle, onEachFeature: gardenOnEachFeature }); gardenLayer.addTo(map); // create icons for pantries (selected and unselected) var pantriesIcon = L.icon({ iconUrl: 'pantries.svg', iconSize: [20,20] }); var selectedPantriesIcon = L.icon({ iconUrl: 'pantries_selected.svg', iconSize: [20,20] }); // handle click events on pantry features function pantriesOnEachFeature(feature, layer){ layer.on({ click: function(e) { if (selection) { resetStyles(); } e.target.setIcon(selectedPantriesIcon); selection = e.target; selectedLayer = pantryLayer; // Insert some HTML with the feature name buildSummaryLabel(feature); L.DomEvent.stopPropagation(e); // stop click event from being propagated further } }); } // add the pantries GeoJSON layer using the pantriesData variable from pantries.js pantryLayer = new L.geoJSON(pantriesData,{ pointToLayer: function (feature, latlng) { return L.marker(latlng, {icon: pantriesIcon}); }, onEachFeature: pantriesOnEachFeature } ); pantryLayer.addTo(map); // handle clicks on the map that didn't hit a feature map.addEventListener('click', function(e) { if (selection) { resetStyles(); selection = null; document.getElementById('summaryLabel').innerHTML = '<p>Click a garden or food pantry on the map to get more information.</p>'; } }); // function to set the old selected feature back to its original symbol. Used when the map or a feature is clicked. function resetStyles(){ if (selectedLayer === pantryLayer) selection.setIcon(pantriesIcon); else if (selectedLayer === gardenLayer) selectedLayer.resetStyle(selection); } // function to build the HTML for the summary label using the selected feature's "name" property function buildSummaryLabel(currentFeature){ var featureName = currentFeature.properties.name || "Unnamed feature"; document.getElementById('summaryLabel').innerHTML = '<p style="font-size:18px"><b>' + featureName + '</b></p>'; } } </script> </head> <body onload="init()"> <h1 id="title">Food resources: Community gardens and food pantries</h1> <div id="mapid"></div> <div id="summaryLabel"> <p>Click a garden or food pantry on the map to get more information.</p> </div> </body> </html>