The links below provide an outline of the material for this lesson. Be sure to carefully read through the entire lesson before returning to Canvas to submit your assignments.
Note: You can print the entire lesson by clicking on the "Print" link above.
Note: Currently, this lesson teaches the Leaflet API. If you are looking for the previous materials on OpenLayers, see the Lesson 8 archive page [1].
So far, you've learned how to create various types of web map layers and overlay them using Leaflet. Simple mashups with "dots on a map" and a few popups may be all you need in many situations. In fact, some managers are thrilled to see these basic web maps if they have never visualized their data in geographic form before; however, as a student of GIS you'll want to be aware of additional possibilities to make the map layers more interactive or informative.
This lesson introduces you to a variety of "extra" things you can do to enrich the interactivity and information exposed by your web maps. Some of these features cannot be covered in full detail in this course; however, the concepts are introduced here in case you ever encounter them or need to use them in the workplace. During the lesson assignment, you'll have the opportunity to choose one of these techniques (or other FOSS of your choice) and explore it more fully.
A word of caution is necessary here: Just because you can do something doesn't mean that you should. Many of the best web maps are effective because they are focused and uncluttered. You'll see in this lesson that Leaflet makes it easy to add all kinds of controls to your web maps, but before doing this, pause and determine whether the extra features are really needed. The same applies to WFS, WPS, widgets provided by web display frameworks, and other features mentioned in this lesson.
Choropleth maps, proportional symbols maps, and on-the-fly feature filtering all provide alternative functionality to the typical "dots on a map" mashup. How can you achieve these things with Leaflet? In this section of the lesson, we'll talk about how to create Leaflet symbols based on attributes in your data. You can then apply these symbols to vectors to get various types of maps, such as choropleth maps based on class breaks.
First, it's important to be clear that symbolizing layers on the client using Leaflet code is not the only way to achieve choropleth maps, proportional symbols, etc., on the web. Earlier in this course, you became familiar with dynamically drawn WMS maps defined by SLD styling, and tiled maps styled in QGIS. In these approaches, you do the cartography on the server rather than the client, or at least the spatial data is passed by the server to the client in an already styled format, typically as one or multiple image files. Server-side maps are most appropriate for advanced cartographic effects. They may also result in better performance when hundreds or thousands of features are in play.
Why, then, are we discussing making our specialized maps with Leaflet styles when so many other cartographic tools lie within reach? The reason is that defining symbols on the client opens the door to flexibility and interactivity.
In order to create a styling rule based on some attribute value, it's first necessary to read the attribute. How do you do this in Leaflet?
Let's consider a dataset that I downloaded from the Buenos Aires open data portal [2], showing subway ("subte") lines. Each subway line is composed of many polyline segments. Each polyline segment has an attribute stating which subway line it belongs to (e.g., "LINEA B"). I've created a GeoJSON file out of this, and I want to display it in a web map using unique colors for each subway line, like this:
First, let's take the easy case where the color value is directly coded into the attribute table. Notice the COLOR field below which contains color hex values for each line segment:
If you are fortunate enough to have this setup and the colors work for you, then you can apply them directly to the color property when you define the layer style. In Leaflet you can use the syntax feature.properties.<PROPERTY> to get the value of any feature attribute:
// Set up styles for subway lines function subteStyle(feature) { return { "color": feature.properties.COLOR, "weight": 5 }; } // Create layer and add it to the map var subteLayer = L.geoJSON(subteData, { style: subteStyle });
The above code creates a vector layer from the variable subteData which comes from a GeoJSON file. To style the layer, the subteStyle function reads the hex value from the COLOR field and inserts it in the color property of the layer. Notice how the syntax feature.properties.COLOR
is utilized to read the color value.
Although the above technique is convenient, it's not always practical. What if the colors provided in the file aren't appropriate for your map? Fortunately, in our scenario, you could work around this by providing color values in your JavaScript (i.e., your client-side) code based on the name of the subway line.
Let's look at how you'd apply some style rules on the client side using Leaflet. Examine the following variation on the code we viewed previously. This code produces the exact same map:
// Set up styles for subway lines function subteStyle(feature) { var colorToUse; var line = feature.properties.LINEASUB; if (line === "LINEA A") colorToUse = "#46C7FA"; else if (line === "LINEA B") colorToUse = "#E81D1A"; else if (line === "LINEA C") colorToUse = "#4161BA"; else if (line === "LINEA D") colorToUse = "#599C65"; else if (line === "LINEA E") colorToUse = "#65018A"; else if (line === "LINEA H") colorToUse = "#FAF739"; else colorToUse = "#000000"; return { "color": colorToUse, "weight": 5 }; } // Create layer and add it to the map var subteLayer = L.geoJSON(subteData, { style: subteStyle });
The above example employs a function subteStyle to read the attribute LINEASUB from each feature, thereby figuring out the name of the subway line (LINEA A, LINEA B, etc.). If/then logic is applied to find the appropriate color to use based on the subway line name. Finally, this color is applied to a style returned by the function.
If you have a field with some numeric attribute, such as the length of a subway line or the number of riders per day, you may want to size the symbols proportionally so that big values are represented with a bigger symbol. Let's consider an example where we have a GeoJSON dataset showing statistics for some of South America's largest metro rail systems. You'll get more familiar with this dataset in the lesson walkthrough, but here's the basic attribute table and map:
In the above image, the metro systems are all drawn using the same symbol. But let's suppose that you wanted to proportionally size the symbols so that the more stations in a metro system, the larger the map symbol. Notice the STATIONS attribute contains this information. The desired map would look something like the following:
Accomplishing the proportional symbols in Leaflet requires you to define some mathematical function that will size each symbol based on each feature's attribute value for STATIONS. The syntax below is somewhat advanced, but pay attention to the line which reads the value for STATIONS, divides it by 80, and multiplies it by 30 to derive the width and height in pixels for any given metro system symbol. These numbers signify that a metro system with 80 stations will have a symbol 30 pixels wide, a metro system with fewer stations will be less than 30 pixels, and a metro system with more stations will be greater than 30 pixels (the numbers 80 and 30, of course, are entirely arbitrary and could be adjusted to fit your own data):
// function to size each icon proportionally based on number of stations function iconByStations(feature){ var calculatedSize = (feature.properties.STATIONS / 80) * 30; // create metro icons return L.icon({ iconUrl: 'metro.svg', iconSize: [calculatedSize, calculatedSize] }); } // create the GeoJSON layer and call the styling function with each marker var metroLayer = L.geoJSON(metroData, { pointToLayer: function (feature, latlng) { return L.marker(latlng, {icon: iconByStations(feature)}); } });
In the above code, iconSize is a two-item JavaScript array containing the width and the height in pixels that should be applied to the icon. Also, notice the use of the pointToLayer property, which is necessary when you want to replace the default Leaflet markers with your own graphics.
In some situations, it may be more appropriate to break up your numerical data into various classifications that are symbolized by graduated colors or some similar approach. This is especially true for line and polygon datasets that are sometimes harder to conceptualize using proportional symbols. The boundaries for your classes (in Esri software you may have heard these referred to as "class breaks") could be determined based on equal interval, quantile, natural breaks, or some other arbitrary scheme.
For example, in the image below, metro systems with over 100 stations are symbolized using dark red. Systems with 50 to 100 stations are symbolized with red. Systems with fewer than 50 stations are symbolized with pink:
To symbolize data classes in Leaflet, we'll read one of the feature attributes, then use if/then logic to check it against our class breaks. The code below defines the three classes used in our metro rail example. Each class references a different SVG symbol with its unique hue of red:
// create metro icons var metroLowIcon = L.icon({ iconUrl: 'metro_low.svg', iconSize: [25,25] }); var metroMediumIcon = L.icon({ iconUrl: 'metro_medium.svg', iconSize: [25,25] }); var metroHighIcon = L.icon({ iconUrl: 'metro_high.svg', iconSize: [25,25] }); // function to use different icons based on number of stations function iconByStations(feature){ var icon; if (feature.properties.STATIONS >= 100) icon = metroHighIcon; else if (feature.properties.STATIONS >= 50) icon = metroMediumIcon; else icon = metroLowIcon; return icon; } // create the GeoJSON layer and call the styling function with each marker var metroLayer = L.geoJSON(metroData, { pointToLayer: function (feature, latlng) { return L.marker(latlng, {icon: iconByStations(feature)}); } });
Although the above may look like a lot of code, notice that half of it is just setting up the icons. A function that classified lines or polygons might be much simpler because a single style could be defined with a varying stroke or fill color based on the attribute of interest.
If you intend to use a classification system such as Jenks natural breaks, equal interval, quantile, etc., you must calculate the break values yourself (or find a library [3] that does it) before defining the rules. You can either do this by hand or add more JavaScript code to calculate the values on the fly.
In some situations, you may want to display only a subset of the features in a dataset, based on some attribute value or combination of values. (If you're familiar with Esri ArcMap, this is called a "definition query".) Suppose you wanted to show only the metro systems whose COUNTRY attribute was coded as "Brazil":
You can do this directly in the filter property when you create the layer in Leaflet. Here you write a function that evaluates a feature and returns a value of true or false. Leaflet will only draw features for which a value of true is returned:
// create metro icons var metroIcon = L.icon({ iconUrl: 'metro.svg', iconSize: [25,25] }); // create the GeoJSON layer and call the styling function with each marker var metroLayer = L.geoJSON(metroData, { pointToLayer: function (feature, latlng) { return L.marker(latlng, { icon: metroIcon }); }, filter: function(feature, layer) { if (feature.properties.COUNTRY === "Brazil") return true; else return false; } });
The filter function in the above example tests for features where the COUNTRY property equals "Brazil". Note that the above example simply reads an attribute, it does not do a spatial query to find the bounds of Brazil. Your website will run much faster if you can preprocess the data to put a country attribute on each metro system, rather than trying to figure that out on the fly using spatial processing in a web environment.
Now, let's look at a scenario with a numeric attribute. Suppose we wanted to show only the metro systems with over 75 stations:
This could be accomplished with the following filter:
// create metro icons var metroIcon = L.icon({ iconUrl: 'metro.svg', iconSize: [25,25] }); // create the GeoJSON layer and call the styling function with each marker var metroLayer = L.geoJSON(metroData, { pointToLayer: function (feature, latlng) { return L.marker(latlng, { icon: metroIcon }); }, filter: function(feature, layer) { if (feature.properties.STATIONS > 75) return true; else return false; } });
In the code above, note the query for a STATIONS value that is greater than 75.
Leaflet offers a variety of options for symbolizing data on the client side based on attribute values. You should not feel limited to using the default symbology or a single symbol type for the entire layer.
Creating Leaflet styles can require some trial and error, along with some skillful debugging. You'll have more success if you start with a working example and tweak it piece by piece, frequently stopping to test your alterations. This section of the lesson is deliberately code-heavy so that you will have many examples available to get you started.
Although Leaflet does not offer a point-and-click interface for building a web application, it does give you a few pre-coded "controls" that you can add to the map with just a few lines of code. These include a scale bar, layer switcher, zoom buttons, and attribution box. OpenLayers offers these same types of controls, plus a few more such as an overview map, geographic coordinates written on the page, and so on.
The example below shows a Leaflet layer switcher control in action. Hovering the mouse over (or tapping) the layer icon in the corner displays a layer list, where you can choose a basemap and toggle the visibility of thematic layers. This image shows the default OpenStreetMap Mapnik basemap selected, with the option to switch to the Stamen Toner basemap. You'll apply this code in your walkthrough later in the lesson. Only one thematic layer is available below, the "Subway lines" layer; however, you could include more layers here simply by adding them to the Leaflet map.
Here's how you would code the layer switcher above. First, set up all the layers and add the ones to the map that you want to have initially visible. Then, call a function that sets up two simple JavaScript objects containing your basemaps and thematic layers. Finally, create the layer switcher (L.control.layers [4]), passing in those two JavaScript objects as parameters.
// create and add osm tile layer var osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }); osm.addTo(map); // create stamen osm layer (not adding it to map) var stamen = L.tileLayer('http://tile.stamen.com/toner/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/">OpenStreetMap</a> and contributors, under an <a href="http://www.openstreetmap.org/copyright" title="ODbL">open license</a>. Toner style by <a href="http://stamen.com">Stamen Design</a>' }); // Set up styles for subway lines function subteStyle(feature) { return { "color": feature.properties.COLOR, "weight": 5 }; } // Create layer and add it to the map var subteLayer = L.geoJSON(subteData, { style: subteStyle }); subteLayer.addTo(map); createLayerSwitcher(); // function to create a layer switcher control function createLayerSwitcher() { // define basemap and thematic layers and add layer switcher control var basemaps = { "OSM Mapnik": osm, "OSM Stamen Toner": stamen }; var overlays = { "Subway lines": subteLayer }; L.control.layers(basemaps,overlays).addTo(map); }
Usually control frameworks are extensible, so you may even decide to pull in controls developed by others or, if you're feeling ambitious, code your own. Although customizing the out-of-the-box Leaflet controls is beyond the scope of this course, you should be aware that controls have a stylesheet you can tweak. Also, controls with images can be customized by substituting your own image. Examining the Leaflet stylesheets and example stylesheets may help you understand which CSS properties to override when applying your customizations.
The Leaflet API reference describes the controls [5] in the namespace L.Control. The best way to learn about controls is to experiment with them yourself and expand your functionality, bit by bit, using existing examples, documentation, and some trial and error. In other web mapping APIs, controls may be called "widgets," "plugins," or other names.
In Lesson 7, you saw some ways that vector data can be drawn by the web browser or client. The lesson focused on standalone KML and GeoJSON files, yet it is also possible for a web service to send the data to the client on request. The data can be in any format as long as both the server and the client are following the same specification. To standardize the process of sending vector data through web services, the Open Geospatial Consortium (OGC) has produced the Web Feature Service (WFS) specification.
You've already seen a related service, the WMS, in previous lessons. How is WMS different from WFS? WMS involves the server sending a single map image, whereas WFS involves the server sending vector data as text to be drawn by the client. In simple terms, a WMS map is drawn by the server and a WFS map is drawn by the client.
Like a WMS, a WFS supports a set of operations that typically take in parameters directly within the URL. These operations include GetCapabilities, DescribeFeatureType, and GetFeature. The GetFeature operation is the one that actually retrieves features.
Below is an example GetFeatures WFS request for the US state of Colorado (I picked something with a simple geometry). I adapted this from the Boundless WFS tutorial [6], which unfortunately doesn't seem to be available online anymore. Another recommended source for further information on WFS is the OGC e-Learning page [7]. You won't be able to use the link below yourself, but see if you can guess what each parameter signifies, then continue reading about the response:
http://suite.opengeo.org/geoserver/wfs?service=wfs&version=1.1.0&request=GetFeature&typename=usa:states&featureid=states.23
By examining the above URL parameters, you can see that a feature is requested from the WFS using version 1.1.0 of the WFS specification. The service is being hosted on GeoServer in a layer named states in the USA workspace. The feature with index 23 is returned.
A WFS returns data using Geography Markup Language (GML), a specification for expressing GIS data using XML. GML can contain both geometry and attribute information. Because it is based on XML and is designed to be flexible enough to handle many geographic feature types, GML is much more verbose (takes more text) than GeoJSON. Deep down in the GML for Colorado, you can find the geometry:
<gml:posList>37.48468400000007 -109.04348799999995 38.164690000000064 -109.04176199999989 38.27548899999999 -109.06006199999996 41.0006590000001 -109.05007599999999 41.00235900000007 -102.051717 36.993015999999955 -102.04208899999992 36.99908400000004 -109.0452229999999 37.48468400000007 -109.04348799999995</gml:posList>
You can also find attributes like this:
<usa:NAME10>Colorado</usa:NAME10>
The same type of request could be made to one of your own services running on GeoServer. Here's how I made a request for a Philadelphia neighborhood using one of the layers we published earlier in this course:
http://localhost:8080/geoserver/wfs?service=wfs&version=1.1.0&request=GetFeature&typename=geog585:Neighborhoods&featureid=Neighborhoods.12
The response looks like this and contains geometry and attributes for the Olney neighborhood:
<wfs:FeatureCollection numberOfFeatures="1" timeStamp="2014-03-03T15:07:31.822Z" xsi:schemaLocation="http://localhost:8080/geoserver/geog585 http://localhost:8080/geoserver/wfs?service=WFS&version=1.1.0&request=DescribeFeatureType&typeName=geog585%3ANeighborhoods http://www.opengis.net/wfs http://localhost:8080/geoserver/schemas/wfs/1.1.0/wfs.xsd"><gml:featureMembers><geog585:Neighborhoods gml:id="Neighborhoods.12"><geog585:the_geom><gml:MultiSurface srsDimension="2" srsName="urn:x-ogc:def:crs:EPSG:3857"><gml:surfaceMember><gml:Polygon srsDimension="2"><gml:exterior><gml:LinearRing srsDimension="2"><gml:posList>-8363968.786751106 4869301.13520122 -8363706.077778376 4871057.31164155 -8363880.846283749 4871132.918517317 -8363697.377540309 4872031.511981935 -8363780.660729433 4872179.806916264 -8363847.448310932 4872208.890548547 -8363802.926044645 4872557.878939522 -8363802.44449278 4872626.491915396 -8363025.915000884 4872530.247301338 -8361543.138729884 4872310.6731403675 -8361453.88028348 4872223.294811407 -8361493.045963939 4872015.489274301 -8361627.94355705 4871826.7318475135 -8361690.687270048 4871673.398417745 -8361627.94355705 4871403.748827802 -8361286.901117077 4870791.777211798 -8361326.368936536 4870458.7113405885 -8361498.408149585 4869986.8871721085 -8361555.111808623 4869831.380121785 -8362695.297708079 4869623.850560427 -8363168.406381819 4869548.2551895585 -8363968.786751106 4869301.13520122</gml:posList></gml:LinearRing></gml:exterior></gml:Polygon></gml:surfaceMember></gml:MultiSurface></geog585:the_geom><geog585:STATE>PA</geog585:STATE><geog585:COUNTY>Philadelphia</geog585:COUNTY><geog585:CITY>Philadelphia</geog585:CITY><geog585:NAME>Olney</geog585:NAME><geog585:REGIONID>214146.0</geog585:REGIONID></geog585:Neighborhoods></gml:featureMembers></wfs:FeatureCollection>
Although the syntax for WFS requests and responses may look intimidating, you will rarely have to worry about composing this yourself. Various FOSS and proprietary software packages include support for viewing and serving WFS.
As shown in the above examples, GeoServer can expose your layers through WFS. This is enabled by default and requires no configuration on your part. Other FOSS GIS servers such as Map Server and Deegree support the creation of WFS web services.
In the proprietary realm, Esri ArcGIS Server gives users the option to expose their web services through WFS; however, Esri has developed its own "feature service" that works through REST and accomplishes many of the same things as a WFS. Be aware that the Esri web mapping APIs and editing widgets are designed to work with the feature service rather than WFS. The communication patterns of the feature service are openly documented in the GeoServices REST Specification.
Support for WFS varies across web mapping APIs. WFS can be viewed "out of the box" as a vector layer in OpenLayers, but not within Leaflet. As mentioned above, Esri web mapping APIs are designed to work with the REST feature service rather than WFS.
Although Leaflet won't draw the GML responses sent by a WFS, it will draw GeoJSON; therefore, one pattern of working with WFS via Leaflet is to configure the server to send back GeoJSON instead of GML. You can then send WFS requests to the server via AJAX (asynchronous web requests) and use Leaflet classes to draw the geoJSON coming back. Such patterns are exposed through numerous forum posts [8], as well as third-party plugins.
On the desktop side, QGIS supports viewing and editing WFS. (Note: I got errors when trying to edit a WFS in QGIS, but I did not have a database or proxy host behind the layer.) Other FOSS clients such as uDig also recognize the WFS format. In Esri ArcMap, you use the Data Interoperability Extension to add WFS layers to the map (see "Connecting to a WFS Service [9]"). ArcGIS Pro allows users to add WFS, and the extension does not appear to be required (documentation here [10]).
The WFS specification also defines rules for feature editing, opening the door to building clients that can edit geographic data over the web. A WFS enabled for editing is known as a transactional WFS, or WFS-T. Beyond the standard WFS operations such as GetFeature, WFS-T supports an additional operation called Transaction, and may also support operations for locking features to prevent concurrent edits.
Creating web editing applications requires both a server that can handle the incoming transactions and a client that provides visual support for the edit sketching, vertex manipulation, attribute entrance into text boxes, and so forth. The client sketching operations must be wired up to the WFS-T requests. Unless you're ready to write a lot of JavaScript, it's best to start with existing samples, widgets, or web controls. Leaflet offers a Draw control [11] that can take care of the sketching part. See an example of what "Draw" [12] can do. The respec/leaflet.wfs-t [13] is an example of a plugin designed to wire up the Draw control to WFS-T requests. (Full disclosure: I have not tested this particular plugin, but there are other plugins based on the same concept).
OpenLayers also offers some basic buttons for sketching, modifying, deleting, and saving features. You can see them in action in this developer example [14].
Before you expose any dataset for editing over the web, you should carefully think through your quality control and data storage architectures. For example, perhaps you want to expose a copy of your database for web editing, then have an analyst review any changes before they are pushed to your master GIS database. If in-house editors are making changes to the master database, you'll also need a way to periodically push those to your web copy. In other cases, you may check incoming edits with a script to make sure they comply with certain topology rules or attribute schema requirements.
Both FOSS and proprietary GIS software offer spatial data processing functions such as buffer, union, contour, interpolate, and so on. You invoked some of these using QGIS and GDAL in earlier lessons. But what if you wanted to allow people to run these functions from a web browser? For example, suppose you wanted to allow users of your web map to draw a polygon and then see a calculation of the total population and number of health clinics within that polygon. You'd be able to expose the GIS to many people without them needing to install any GIS software.
OGC has released a specification for invoking spatial data processing through web services. It's called the Web Processing Service (WPS) specification. Like the other OGC services you've learned about, it offers a set list of operations you can call. These are: GetCapabilities, DescribeProcess, and Execute. Of course, the Execute operation is the one that launches the request to actually perform the processing. The server does the work to process the data and send back the result as a response. GML may be used to transfer information about vector features in either the request or the response.
As you are probably aware from running GDAL and ArcToolbox tools, spatial data processing functions can sometimes require many input parameters. For example, a buffer process might require you to specify the width of the buffer, whether it will be applied to both sides of the feature, whether the ends should be capped or rounded, etc. Each tool has its own set of parameters and syntax for describing them. Because the inputs can be so long and complex (especially if geometry is included), sometimes you can put the inputs in their own XML file and POST them to the server, rather than putting all the parameters into the URL as a GET request like you have seen with WMS and WFS in previous lessons. Some servers and browsers impost limits on the length of an HTTP GET request, whereas HTTP POST requests can typically be much longer.
The WPS spec itself doesn't say which types of spatial data processing operations a WPS must support; that's up to whoever builds and administers the service. There are hundreds of potential operations that can be included. When you first use a WPS, you can invoke the GetCapabilities operation to find out which processes are available.
GeoServer offers a WPS extension [15] that exposes a set of spatial processes from the FOSS JTS Topology Suite [16], as well as some other processes developed specifically by GeoServer. We will not install this extension in this course, but I encourage you to browse through the documentation if you think that you may use it in your workplace or other academic work.
The Zoo Open WPS Platform [17] and PyWPS [18] are other examples of FOSS WPS implementations. In the proprietary realm, Esri ArcGIS Server can serve a WPS from a ModelBuilder model that you create from ArcToolbox tools or scripts.
A few GUI-based WPS clients are available that allow you to select tools and supply their parameters in text boxes or dropdown lists. QGIS has a WPS plugin [19] that works this way, allowing you to call a WPS from the desktop.
When it comes to invoking a WPS directly from a web application, some web map APIs offer helper classes or libraries that can help you. Leaflet is not one of these (I will get to that in a minute). OpenLayers 2 supports WPS through the use of OpenLayers.WPSClient. When you instantiate this object, you supply the URL of the WPS server. You then set up JavaScript objects containing all the parameters of the process you want to invoke. Finally, you execute the process and specify what should be done with the results.
Even when you use a WPS client plugin, library, or object, you still need to be familiar with the process and its documentation, so that you can supply the correct syntax for the parameters. One mistake in syntax can derail the entire processing operation. Furthermore, WPS servers and clients are often either in the early stages of maturity or are designed for power users who are comfortable with a lack of a GUI and extensive documentation.
Many spatial processing operations are rather common, and there is a great desire from GIS web developers to invoke these without the overhead of a WPS. The Turf.js [20] library has gained popularity for allowing geoprocessing directly on GeoJSON vectors in a JavaScript environment. In other words, Turf differs from WPS because the client machine runs the processing code, not the server. Turf was developed at Mapbox, but is free to use and modify under the open MIT license [21]. Operations available through Turf.js include dissolving, unioning, intersecting, buffering, aggregating, calculating centroids, and so forth.
You can download Turf.js to your own machine or access it online via a CDN [22]. Because Turf.js works on GeoJSON, it fits very nicely with Leaflet. Most of the abundant Turf.js developer examples are shown using Mapbox's API (for obvious reasons), but the way you would invoke them from Leaflet is essentially the same. For example, the following snippet is from the Turf.js analysis walkthrough [23]:
// Using Turf, find the nearest hospital to library clicked
var nearestHospital = turf.nearest(e.layer.feature, hospitalFeatures);
In this situation, both parameters of the turf.nearest() function are GeoJSON objects: one representing a library building, and another one representing a set of hospital buildings. The function finds the hospital nearest the clicked library and returns it as GeoJSON. Fundamentally, it doesn't matter which API you use to display the output GeoJSON.
Before implementing any kind of spatial data processing on the web, consider ways that you might preprocess the data in order to eliminate the need for on-the-fly calculations. When you invoke spatial processing on the web, it makes your server busy and increases end-user wait times. In the above example, perhaps the nearest hospital to each library could be precalculated in a desktop GIS environment and stored in the library attribute table. Then your code would just need to read the attribute, rather than performing processing.
There will be some situations where precalculation doesn't make sense due to the wide range of analysis parameters and possibilities you want to expose. For example, if there were 20 different kinds of features that you wanted to allow people to find near a library, or if a person could type in the address of some library not in the original dataset. Therefore, the decision about whether to allow real-time spatial processing should be made on a case-by-case basis.
Unless you already have some previous exposure to WPS, I do not recommend integrating it into your term project given the short amount of time that remains in the course. A Turf.js implementation would be a better fit for the scope of the term project, since it can be implemented fairly quickly with less code.
Neither of these technologies is a requirement for the project, although they may provide useful functionality in many scenarios.
Working with web pages is not always easy, especially when it's not your main area of expertise. Code for laying out the page, iterating through elements, toggling classes, etc., can get clunky and bloated in a hurry. Browsers can interpret the same pieces of HTML and JavaScript differently, and debugging the pages can be a mysterious process involving multiple code files and complex hierarchies of stylesheets and overrides.
There is no magic solution for these challenges; however, there are some JavaScript helper libraries that can simplify your life and your code if you make the effort to learn them. Your pages will probably also become more functional and attractive. Some of these libraries offer mainly back-end functions, others specialize in front-end elements, and others offer both.
Many web developers like to use special JavaScript libraries that have been developed to simplify common functions and abstract away some of the idiosyncrasies between web browsers.
For example, if you select a few web pages at random and look at the source code, chances are pretty good that you'll see someone using the jQuery library. jQuery [24] provides functions to simplify navigating and manipulating the DOM elements on your page. For example, using jQuery you can populate a dropdown list or change the CSS class of an HTML element on the fly without writing a bunch of HTML markups.
Similar alternatives to jQuery are Prototype [25] and the Dojo toolkit [26], although the latter also offers some UI elements such as menus, buttons, and charts. All these libraries are build with the goal of simplifying JavaScript coding and reducing the work for you to handle differences between browsers.
In the previous lesson examples, you've learned how to embed your map in a web page. In the Lesson 7 walkthrough, you also briefly saw how to use the innerHtml property to manipulate a DOM element and thereby change a label on a page. But how do you craft a nice looking page around your map without devoting hundreds of hours to web design study? This is where a web presentation framework can come in handy.
Web presentation frameworks consist of JavaScript libraries, stylesheets, and "widgets" that work together to give you some convenient building blocks for nice looking pages. Some of these goodies include headers, buttons, calendar date pickers, menus, etc.
Web development frameworks are typically engineered so that your page works in a similar manner across browsers. They may also give you a head start with making your page easily localizable (i.e., flexible enough to be offered in various languages such as Spanish, Arabic, Chinese, etc.).
An example of a popular web presentation framework is Bootstrap [27], distributed by the Twitter development team under an open source license. In the Lesson 8 walkthrough, you'll use Bootstrap to put a nice header on your application and divide your page into sections. You'll also get the benefit of the Bootstrap stylesheets.
Similar frameworks to Bootstrap include Groundwork [28], Zurb's Foundation [29], and Yahoo's Pure [30]. The latter used to be called YUI (yoo'-ee) and this name is still often seen in the web world. Take a few minutes and follow some of the above links to see the different styles offered by these frameworks.
Some JavaScript libraries such as the Dojo toolkit and Sencha Ext JS [31] also offer layout elements that serve a web design function. The project jQuery UI [32] offers a helpful set of user interface elements such as buttons, menus, date pickers, etc.
Even more powerful, we have web application frameworks for supporting the design and implementation of complex web applications and services, like the popular Python-based Django [33] and JavaScript-based React [34], which also contain web presentation components.
You should be aware that web presentation frameworks sometimes require and use jQuery or other libraries to accomplish their functions.
When evaluating a web presentation framework for your project, you might consider:
A web presentation framework can help produce a nicer end product than you might be able to concoct on your own; however, a framework also introduces another level of complexity. You may have to override some of the framework's stylesheets in order to get other libraries (like Leaflet or OpenLayers) to behave in the expected way. Debugging such issues often involves multiple levels of stylesheets and a lot of time in the weeds. If you just need a very simple app, you might leave the framework aside and create your own stylesheet, or at least try to select the most simple framework available for the functionality you need.
In this walkthrough, you'll put together some of the things you've learned in this lesson to make a well-rounded informational map containing thematic styling and Leaflet controls. The map will use the Bootstrap web presentation framework so that it can be extended with supplementary content in an aesthetically pleasing way. You will construct some of this content on the fly by reading the attributes of selected map features.
For this exercise, we'll stick with the metro data from South America. The final application will allow users to click any metro and see some informational text and an image from Flickr. A static legend image is also brought into the layout.
Although this lesson is called "Going beyond 'dots on a map," I have deliberately chosen point data for this walkthrough so that you can appreciate the possibilities offered through Leaflet, especially when tied together with other frameworks. Although you may not feel like a JavaScript guru at this point, you should at least know that you don't have to settle for the 'red teardrop and popup' mashups that you see all over the Internet.
Before you add JavaScript, I will just point out a few things that are important in the layout. First, notice how Bootstrap is brought into the application through a reference to a JavaScript file (.js) and a CSS file (.css) in the bolded lines below. We are referencing the files from a CDN, but you could alternatively download and host (and tweak) them yourself.
<script src="http://code.jquery.com/jquery-latest.js"></script> . . . <script src="https://maxcdn.bootstrapcdn.com/bootstrap/2.2.1/js/bootstrap.min.js"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/2.2.1/css/bootstrap.min.css">
Also take note of the stylesheet override in the <style> tag. This will prevent our page content from crashing into the top banner.
<style> body { padding-top: 60px; padding-bottom: 40px; } </style>
If you don't like something about the Bootstrap (or Leaflet) default styles, the stylesheet overrides are where you can apply a change.
In the page body, observe how classes are used to organize the page into navigation bars (navbar-*), containers (container-fluid), and spans. The span5 and span7 classes divide the page vertically so that consistent proportions of the page area are devoted to the map (on the left) and the supplementary material (on the right). Don't worry about understanding all these classes at this point. Just be aware that web development frameworks like Bootstrap, Dojo, etc., may give you CSS classes for organizing page elements in a way that should behave consistently across browsers.
Finally, note that the legend image and the "Click a metro..." text are organized into an HTML table to keep them flush. The table borders are invisible by design.
<table> <tr> <td><img src="metro_legend.png"/></td> <td id = 'summaryLabel'><p>Click a metro rail system on the map to get more information.</p></td> </tr> </table>
A div awaits for a Flickr image, but this won't appear until someone clicks a metro symbol on the map.
<div id="metroImage"></div>
Now let's breathe some life into this app by adding some JavaScript code.
You'll use JavaScript to do three main things in this application: add the map, define the symbols, and handle the click events. Let's address these in order.
In your lesson8_walkthrough.html, find the script tags. You'll put all your code immediately after: <script type="text/javascript">.
Add the following lines of code to create the map and an initialization function. This function contains some variables that we'll use to handle selections throughout, similar to what you saw in Lesson 7:
var map; var metroLayer; function init() { // create map and set center and zoom level map = new L.map('mapid'); map.setView([-28,-62],3); var selection; var selectedLayer; var selectedFeature; . . . }
'https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png?api_key=xyz'Now replace the . . . in the code above with the following. Although it's a lot, all of this should look pretty familiar if you paid attention to the code snippets earlier in the lesson:
// create and add osm tile layer var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }); osm.addTo(map); // create osm humanitarian layer (not adding it to map) var osmHumanitarian = L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }); // create metro icons var metroLowIcon = L.icon({ iconUrl: 'metro_low.svg', iconSize: [25,25] }); var metroMediumIcon = L.icon({ iconUrl: 'metro_medium.svg', iconSize: [25,25] }); var metroHighIcon = L.icon({ iconUrl: 'metro_high.svg', iconSize: [25,25] }); var metroSelected = L.icon({ iconUrl: 'metro_selected.svg', iconSize: [25,25] }); // add the metro GeoJSON layer var metroLayer = L.geoJson(metroData,{ pointToLayer: function (feature, latlng) { return L.marker(latlng, {icon: iconByPassday(feature)}); }, onEachFeature: metrosOnEachFeature }); metroLayer.addTo(map); . . .
You may have noticed that the metroLayer relies on two functions: iconByPassday and metrosOnEachFeature. We'll tackle those in a few minutes. But first, the layer switcher...
Replace the . . . in the code above to add a layer switcher control to the map:
// define basemap and thematic layers and add layer switcher control var basemaps = { "OSM": osm, "OSM Humanitarian": osmHumanitarian }; var overlays = { "Metro stations": metroLayer }; L.control.layers(basemaps,overlays).addTo(map); . . .Again, the pattern here is to create a JavaScript object for the basemaps and one for the thematic layers, and then pass those two objects in as parameters when you create the control.
Now for those functions I mentioned. The first one, iconByPassday, looks at the number of passengers per day that travel through each metro system, then decides which icon to use. It's the key to classifying the layer and symbolizing it with the different colors. Replace the . . . in the code above with the following:
// define functions that right icon for a given feature function iconByPassday(feature) { var icon; if (feature.properties.PASSDAY >= 2000000) icon = metroHighIcon; else if (feature.properties.PASSDAY >= 1000000) icon = metroMediumIcon; else icon = metroLowIcon; return icon; } . . .
Now for that metrosOnEachFeature function. This one is a little more lengthy. It adds a click event listener to the metros layer and then defines a function to handle that event. Replace the . . . in the code above with the following:
// define function to handle click events on metro features function metrosOnEachFeature(feature, layer){ layer.on({ click: function(e) { // reset symbol of old selection if (selection) { if (selectedLayer === metroLayer) selection.setIcon(iconByPassday(selectedFeature)); } // apply yellow icon to newly selected metro and update selection variables e.target.setIcon(metroSelected); selection = e.target; selectedLayer = metroLayer; selectedFeature = feature; // using attributes, construct some HTML to write into the page var featureName = feature.properties.CITY || 'Unnamed feature'; var country = feature.properties.COUNTRY || '(Unknown)'; var year = feature.properties.YEAR || '(Unknown)'; var passengers = feature.properties.PASSDAY || '(Unknown)'; var stations = feature.properties.STATIONS || '(Unknown)'; var length = feature.properties.LENGTHKM || '(Unknown)'; var link = feature.properties.LINK || 'http://www.wikipedia.org'; var photoHtml = feature.properties.PHOTO || '<P>Photo not available</P>'; var titleHtml = '<p style="font-size:18px"><b>' + featureName + '</b></p>'; var descripHtml = '<p>The ' + featureName + ', ' + country + ' metro opened in ' + year + ' and currently serves ' + passengers + ' passengers a day. The network consists of ' + stations + ' stations spread over ' + length + ' kilometers.</p>'; var readmoreHtml = '<p><a href="' + link + '">Read more</a></p>'; document.getElementById('summaryLabel').innerHTML = titleHtml + descripHtml + readmoreHtml; document.getElementById('metroImage').innerHTML = photoHtml; L.DomEvent.stopPropagation(e); // stop click event from being propagated further } }); } . . .Examine the code comments above to understand what each section is doing. There's a lot of code, but most of it is constructing the informational HTML describing the metro system. Recall that the attribute table looks like this:
You should notice many of these attribute field names referenced in the code above. The field value is retrieved, using some special syntax || (logical OR operator that in JavaScript returns the first operand that is TRUE) to set a fallback value in case no attribute comes back. The rest of the function constructs some HTML strings, inserting the attributes where appropriate. After the HTML strings are constructed, the innerHTML is updated for the elements "summaryLabel" and "metroImage." This causes the new text and the photo to appear on the right side of the page.
The PHOTO field deserves some additional discussion here. Anticipating that this field would be used to embed a photo in an app, the entire iframe HTML code is placed in this field as a long string. Where do you get this code if you want to make a dataset like this? Right from the Flickr embed functionality:
Finally, add a click event handler to the map that will reset the selection. This is necessary if someone clicks the map, but not a metro icon. Replace the . . . in the code above with the following:
// define and register event handler for click events to unselect features when clicked anywhere else on the map map.addEventListener('click', function(e) { if (selection) { if (selectedLayer === metroLayer) selection.setIcon(iconByPassday(selectedFeature)); selection = null; document.getElementById('summaryLabel').innerHTML = '<p>Click a metro rail system on the map to get more information.</p>'; document.getElementById('metroImage').innerHTML = '' } });Notice that some code was added here to also clear out the informational HTML and the photo.
Test the application by opening lesson8_walkthrough.html in a web browser. (If you're just opening the HTML file directly from the file system, I recommend using Firefox to test. This is because the cross-origin request security implementations in Chrome and Internet Explorer only allow the GeoJSON to load if you're hosting both the HTML and GeoJSON files on a web server.)
You should be able to switch between different base layers (note that the Map Quest open layer shown below is no longer available). Click a metro icon to see the highlighted symbol, the Flickr image, and the descriptive text.
If the walkthrough does not function, check to ensure you are connected to the Internet and that your code matches the code below:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Lage metro rail systems in South America</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="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script src="metro.js"></script> <link rel="stylesheet" href="style.css" type="text/css"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/2.2.1/js/bootstrap.min.js"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/2.2.1/css/bootstrap.min.css"> <style> body { padding-top: 60px; padding-bottom: 40px; } </style> <script type="text/javascript"> var map; var metroLayer; function init() { // create map and set center and zoom level map = new L.map('mapid'); map.setView([-28,-62],3); var selection; var selectedLayer; var selectedFeature; // create and add osm tile layer var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }); osm.addTo(map); // create osm humanitarian layer (not adding it to map) var osmHumanitarian = L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }); // create metro icons var metroLowIcon = L.icon({ iconUrl: 'metro_low.svg', iconSize: [25,25] }); var metroMediumIcon = L.icon({ iconUrl: 'metro_medium.svg', iconSize: [25,25] }); var metroHighIcon = L.icon({ iconUrl: 'metro_high.svg', iconSize: [25,25] }); var metroSelected = L.icon({ iconUrl: 'metro_selected.svg', iconSize: [25,25] }); // add the metro GeoJSON layer var metroLayer = L.geoJson(metroData,{ pointToLayer: function (feature, latlng) { return L.marker(latlng, {icon: iconByPassday(feature)}); }, onEachFeature: metrosOnEachFeature }); metroLayer.addTo(map); // define basemap and thematic layers and add layer switcher control var basemaps = { "OSM": osm, "OSM Humanitarian": osmHumanitarian }; var overlays = { "Metro stations": metroLayer }; L.control.layers(basemaps,overlays).addTo(map); // define functions that right icon for a given feature function iconByPassday(feature) { var icon; if (feature.properties.PASSDAY >= 2000000) icon = metroHighIcon; else if (feature.properties.PASSDAY >= 1000000) icon = metroMediumIcon; else icon = metroLowIcon; return icon; } // define function to handle click events on metro features function metrosOnEachFeature(feature, layer){ layer.on({ click: function(e) { // reset symbol of old selection if (selection) { if (selectedLayer === metroLayer) selection.setIcon(iconByPassday(selectedFeature)); } // apply yellow icon to newly selected metro and update selection variables e.target.setIcon(metroSelected); selection = e.target; selectedLayer = metroLayer; selectedFeature = feature; // using attributes, construct some HTML to write into the page var featureName = feature.properties.CITY || 'Unnamed feature'; var country = feature.properties.COUNTRY || '(Unknown)'; var year = feature.properties.YEAR || '(Unknown)'; var passengers = feature.properties.PASSDAY || '(Unknown)'; var stations = feature.properties.STATIONS || '(Unknown)'; var length = feature.properties.LENGTHKM || '(Unknown)'; var link = feature.properties.LINK || 'http://www.wikipedia.org'; var photoHtml = feature.properties.PHOTO || '<P>Photo not available</P>'; var titleHtml = '<p style="font-size:18px"><b>' + featureName + '</b></p>'; var descripHtml = '<p>The ' + featureName + ', ' + country + ' metro opened in ' + year + ' and currently serves ' + passengers + ' passengers a day. The network consists of ' + stations + ' stations spread over ' + length + ' kilometers.</p>'; var readmoreHtml = '<p><a href="' + link + '">Read more</a></p>'; document.getElementById('summaryLabel').innerHTML = titleHtml + descripHtml + readmoreHtml; document.getElementById('metroImage').innerHTML = photoHtml; L.DomEvent.stopPropagation(e); // stop click event from being propagated further } }); } // define and register event handler for click events to unselect features when clicked anywhere else on the map map.addEventListener('click', function(e) { if (selection) { if (selectedLayer === metroLayer) selection.setIcon(iconByPassday(selectedFeature)); selection = null; document.getElementById('summaryLabel').innerHTML = '<p>Click a metro rail system on the map to get more information.</p>'; document.getElementById('metroImage').innerHTML = '' } }); } </script> </head> <body onload="init()"> <div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar-inner"> <div class="container-fluid"> <a class="brand" href="/">Large metro rail systems in South America</a> </div> </div> </div> <div class="container-fluid"> <div class="row-fluid"> <div class="span5"> <div id="mapid"> </div> </div> <div class="span7"> <div> <table> <tr> <td><img src="metro_legend.png"/></td> <td id = 'summaryLabel'><p>Click a metro rail system on the map to get more information.</p></td> </tr> </table> <div id="metroImage"></div> </div> </div> </div> </div> </body> </html>
If the amount of JavaScript in the walkthrough was intimidating, don't worry. You don't have to write that kind of code in the lesson assignment, although I hope some of it comes in handy in your term project.
For this week's assignment, we're going to do something a bit different. You will identify and report on some GIS FOSS that interests you. Your typical encounter with FOSS in the "real world" will be open-ended, with little direction other than what you can find online. Therefore, this assignment is designed to give you an experience of independent exploration.
First, read this article to get an idea of the many different FOSS products used with GIS, some of which we have not covered in this course. I have placed this article in the Lesson 8 module in Canvas:
With its many acronyms, this article approaches alphabet soup at times. But I think you are far enough along in this course that you will recognize some of them and have enough frame of reference to process the ones you don't know. Obviously, this article is several years old, but will help get you up to date on the major players in FOSS GIS. I am not aware of a more recent article as thorough as this.
Identify a FOSS solution for GIS that we have not covered in this course, download it (if applicable), and briefly experiment with it. Then use the software to accomplish something useful.
In fulfilling this requirement, keep in mind the following:
Use the Steiniger and Hunter article, as well as the Ramsey video from Lesson 1, if you need ideas about what to review.
Write a report of around 1000 words describing the following:
If you reviewed a cloud based service or a FOSS product created by a for-profit entity, please delineate which elements of the software are FOSS and which are not. Also describe how the FOSS portion fits into the business model of the provider.
Remember that free software and APIs are not always open source. The software you review for this assignment must have some portion where the source code is openly shared and can be reused or modified.
You should submit your report into the Lesson 8 assignment drop box on Canvas.
Links
[1] https://www.e-education.psu.edu/geog585/node/785
[2] http://data.buenosaires.gob.ar/dataset
[3] http://gis.stackexchange.com/questions/11106/tiny-js-discretization-library-for-choropleth-representation
[4] http://leafletjs.com/reference.html#control-layers
[5] http://leafletjs.com/reference.html#control-zoom
[6] http://workshops.boundlessgeo.com/geoserver-intro/overview/wfs.html
[7] http://opengeospatial.github.io/e-learning/wfs/text/basic-index.html
[8] https://gis.stackexchange.com/questions/64406/getting-wfs-data-from-geoserver-into-leaflet
[9] https://desktop.arcgis.com/en/arcmap/latest/extensions/data-interoperability/connecting-to-a-wfs-service.htm
[10] https://pro.arcgis.com/en/pro-app/help/data/services/add-wfs-services.htm
[11] https://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest.html
[12] https://leaflet.github.io/Leaflet.draw/docs/examples/full.html
[13] https://github.com/respec/leaflet.wfs-t
[14] https://openlayers.org/en/latest/examples/draw-features.html
[15] https://docs.geoserver.org/stable/en/user/services/wps/index.html
[16] https://github.com/locationtech/jts
[17] http://www.zoo-project.org/
[18] http://pywps.org/
[19] http://plugins.qgis.org/plugins/wps/
[20] http://turfjs.org/
[21] https://opensource.org/licenses/MIT
[22] https://npmcdn.com/@turf/turf/turf.min.js
[23] https://www.mapbox.com/help/analysis-with-turf/
[24] http://jquery.com/
[25] http://prototypejs.org/
[26] http://www.dojotoolkit.org
[27] http://www.getbootstrap.com
[28] https://github.com/moruzo/groundwork/blob/master/README.md
[29] http://foundation.zurb.com/
[30] http://purecss.io/
[31] http://www.sencha.com/products/extjs/
[32] https://jqueryui.com/
[33] https://www.djangoproject.com/
[34] https://reactjs.org/
[35] https://www.e-education.psu.edu/geog585/sites/www.e-education.psu.edu.geog585/files/lesson8/lesson8_walkthrough_leaflet.zip
[36] https://docs.stadiamaps.com/guides/migrating-from-stamen-map-tiles/