Until recently, developing geospatial apps beyond a 2D map required a comprehensive GIS (Geospatial Information System) service such as ArcGIS, Nokia Here, or Google Maps. While these APIs are powerful, they are also expensive, onerous to learn and lock the map developer to a single solution. Fortunately, there are now a wealth of useful, open-source JavaScript tools for handling advanced cartography and geospatial analysis.
Everyone knows and loves maps, but what’s GIS, you say:
Geospatial Information Systems (GIS) is an area of cartography and information technology concerned with the storage, manipulation, analysis, and presentation of geographic and spatial data. You are probably most familiar with GIS services that produce dynamic, two-dimensional tile maps which have been prominent on the web since the days of MapQuest.
In this article, I’ll examine how to implement GIS techniques with JavaScript and HTML, focusing on lightweight tools for specific tasks. Many of the tools I’ll cover are based on services such as Mapbox, CloudMade, and MapZen, but these are all modular libraries that can be added as packages to Node.js or used for analysis in a web browser.
Note: The CodePen examples embedded in this post are best viewed on CodePen directly.
It is especially useful to have small, focused libraries that perform distance measurement, and conversion operations, such as finding the area of a geo-fence or converting miles to kilometers. The following libraries work with GeoJSON formatted objects representing geographic space.
While the above libraries work well for 2D projections of geography, three-dimensional GIS is an exciting and expansive field—which is natural because we live 3D space. Fortunately, WebGL and the HTML5 canvas have also opened up new 3D techniques for web developers.
Here’s an example of how to display GeoJSON Features on a 3D object:
CSS JSResult Skip Results Iframe
EDIT ON
// Render GeoJSON features on a spherical object.
// Create Three.js scene, camera, & light
var WIDTH = window.innerWidth - 30,
HEIGHT = window.innerHeight - 30;
var angle = 75,
aspect = WIDTH / HEIGHT,
near = 0.5,
far = 1000;
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(angle, aspect, near, far);
// Renderer the canvas
var renderer = new THREE.WebGLRenderer();
renderer.setSize( WIDTH, HEIGHT);
renderer.setClearColor( 0x555555, 1);
document.body.appendChild(renderer.domElement);
// create a point light (goes in all directions)
scene.add(new THREE.AmbientLight(0x71ABEF));
var pointLight = new THREE.PointLight(0x666666);
// set its position
pointLight.position.x = 60;
pointLight.position.y = 50;
pointLight.position.z = 230;
scene.add(pointLight);
// Create a sphere to make visualization easier.
var geometry = new THREE.SphereGeometry(10,32,32);
var material = new THREE.MeshPhongMaterial({
color: 0xDDDDDD,
wireframe: false,
transparent: true
});
var sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);
sphere.castShadow = true;
sphere.receiveShadow = true;
//Draw the GeoJSON at THREE.ParticleSystemMaterial
var countries = $.getJSON("https://s3-us-west-2.amazonaws.com/s.cdpn.io/230399/countries_states.geojson", function (data) {
drawThreeGeo(data, 10, 'sphere', {
color: 0x8B572A,
skinning: true
});
});
var rivers = $.getJSON("https://s3-us-west-2.amazonaws.com/s.cdpn.io/230399/rivers.geojson", function (data) {
drawThreeGeo(data, 10, 'sphere', {
color: '#4A90E2'
});
});
//Set the camera position
camera.position.z = 20;
//Enable controls
controls = new THREE.TrackballControls(camera);
// Slow down zooming
controls.zoomSpeed = 0.1;
//Render the image
function render() {
controls.update();
requestAnimationFrame(render);
renderer.render(scene, camera);
}
render();
You can also check out Byron Houwen’s article on WebGL and JavaScript, which shows how to create a terrain map of earth with Three.js
Much of the work in GIS involves dealing with points, shapes, symbols, and other features. The most basic task is to add a shape or point features to a map. The well-established Leaflet library and newcomer Turf.js make this much easier and allow users to work with feature collections.
Leaflet is simply the best option for working with the display of points, symbols, and all types of features on web and mobile devices. The library supports rectangles, circles, polygons, points, custom markers, and a wide variety of layers. It performs quickly and handles a variety of formats. The library also has a rich ecosystem of third-party plug-ins.
Turf.js is a library from Mapbox for geospatial analysis. One of the great things about Turf.js is that you can create a collection of features and then spatially analyze, modify (geoprocess), and simplify them, before using Leaflet to present the data. Like Geolib, Turf.js will calculate the path length, feature center, points inside a feature.
Simple Map D3 creates choropleths and other symbology by simply defining a GeoJSON object and data attribute.
The following is an example of using Turf.js to calculate the population density of all counties in the state of California and then displaying the results as a Leaflet choropleth map.
// Public auth for initializing the map
var mapMain = 'stevepepple.lkojhhoh';
L.mapbox.accessToken = 'pk.eyJ1Ijoic3RldmVwZXBwbGUiLCJhIjoiTmd4T0wyNCJ9.1-jWg2J5XmFfnBAhyrORmw';
// Quantile or Jenks classification
var type = $("#type").val();
var breaks;
// Color scale using chroma.js
var scale = chroma.scale('YlOrBr');
// Create the base map using Mapbox
map = L.mapbox.map('map', mapMain, {
attributionControl: false,
maxZoom: 12,
minZoom: 5
}).setView([34.0261899, -118.2455643], 8);
// Calculate population density for each county
_.each(counties.features, function(feature) {
// Population from 2010 Census
var pop = feature.properties.POP;
// Calcualate desnity as popluation / square meters
var area = turf.area(feature.geometry);
var sq_miles = area * 0.000000386102158542;
feature.properties.pop_density = (pop / sq_miles);
});
updateMap();
// Show the Choropleth of Population Density
function updateMap() {
// Make a Turf collection for analysis
collection = turf.featurecollection(counties.features);
// Basic UI for select Jenks or Quantile classification
if (type == "jenks") {
breaks = turf.jenks(collection, "pop_density", 8);
} else {
breaks = turf.quantile(collection, "pop_density", [25, 50, 75, 99]);
}
// Get the color when adding to the map
var layer = L.geoJson(counties, {
style: getStyle
})
layer.addTo(map);
// Fit to map to counties and set the legend
map.fitBounds(layer.getBounds());
updateLegend();
function getStyle(feature) {
var pop = feature.properties.POP;
var pop_density = feature.properties.pop_density;
return {
fillColor: getColor(pop_density),
fillOpacity: 0.7,
weight: 1,
opacity: 0.2,
color: 'black'
}
}
function updateLegend() {
$(".breaks").html();
for (var i = 0; i < breaks.length; i++) {
var density = Math.round(breaks[i] * 100) / 100;
var background = scale(i / (breaks.length));
$(".breaks").append("<p><div class='icon' style='background: " + background + "'></div><span> " + density + " <span class='sub'>pop / mi<sup>2</sup></span></span></p>");
}
}
}
/* Get color depending on population density value */
function getColor(d) {
// Select a color scale from Color Brewer
var color = scale(0);
// Place the feature based upon breaks
for (var i = breaks.length - 1; i >= 0; i--) {
if (d < breaks[i]) {
// Automatic way to select the color by class
var percentage = (i / (breaks.length));
color = scale(percentage);
}
}
return color;
}
A key concept in Turf.js is a collection of geographic features, such as polygons. These feature are typically GeoJSON features that you want to analyze, manipulate, or display on a map. You start with a GeoJSON object with an array of county features. Then, create a collection from this object:
collection = turf.featurecollection(counties.features);
With this collection you can perform many useful operations. You can transform one or more collections with joins, intersections, interpolation, and exclusion. You can calculate descriptive statistics, classifications, and sample distributions.
In the case of population density, you can calculate the natural breaks (Jenks optimization) or quantile classifications for population density:
breaks = turf.jenks(collection, "pop_density", 8);
The population density (population divided by area) value was calculated and stored as a property of each county, but the operation works for any feature property.
Points are a special type of geographic feature representing a latitude-longitude coordinate (and associated data). These features are frequently used in web applications, e.g. to display a set of nearby businesses on a map.
Turf.js provides a number of different operations for points, including finding the centroid point in a feature and creating a rectangle or polygon that encompasses all points. You can also calculate statistics from points, such as the average based on a data value for each point.
There are also extensions for Leaflet.js that help when dealing with a large number of points:
Routing, geocoding, and reverse geocoding locations requires an online service, such as Google or Nokia Here, but recent libraries have made the implementation easier. There are also suitable open source alternatives.
The HTML5 Geolocation API provides a simple method of getting a device’s GPS location (with user permission):
navigator.geolocation.getCurrentPosition(function(result){
// do something with result.coords
);
Location-aware web applications can use Turf.js spatial analysis methods for advanced techniques such as geofencing a location inside or outside of a map feature. For instance, you can take the result from the above example and use the turf.inside method to see if that coordinate is within the boundaries of a given neighborhood.
As with geocoding, there are myriad routing and direction services, but they will cost you. A reliable, open source alternative is the Open Source Routing Machine (OSRM) service by MapZen. It provides a free service for routing car, bicycle, and pedestrian directions. Transit Mix cleverly uses the OSRM Routing tool for creating routes in their transportation planning tool.
I’ve mentioned a few spatial analysis methods you can implement with Turf.js and other libraries, but I’ve only covered a small part of a vast world. I’ve created an example application that illustrates several of the techniques I’ve introduced.
/* Calculate the center of all points */
function findCenter() {
var centroidPt = turf.centroid(points);
var marker = L.geoJson(centroidPt)
markers.push(marker)
marker.addTo(map);
}
/* Find the rectangular boundary of all points */
function findBounds() {
bbox = turf.extent(points);
bboxPolygon = turf.bboxPolygon(bbox);
var bounds = L.geoJson(bboxPolygon);
map.fitBounds(bounds.getBounds());
}
// Show a pseudo-random sample of points in the box
function randomSample() {
var sample = L.geoJson(turf.sample(points, 100), {
style: circle_options
})
sample.addTo(map);
markers.push(sample);
}
/* Find the Most Central POI by Neighborhood */
function findCentralPOIs() {
hoods = turf.featurecollection(neighborhoods.features);
display = L.geoJson(hoods, {
style: hood_options
}).addTo(map);
_.each(hoods.features, function(feature) {
try {
var center = turf.centroid(feature);
var nearest = turf.nearest(center, points);
var marker = L.geoJson(center)
markers.push(marker)
marker.addTo(map);
} catch (e) {
console.log(e)
}
});
}
/* Find the Most Central POI by Neighborhood */
function hoodByPOI() {
hoods = turf.featurecollection(neighborhoods.features);
display = L.geoJson(hoods, {
style: hood_options
}).addTo(map);
_.each(hoods.features, function(feature) {
try {
console.log(feature)
var collection = turf.featurecollection(feature);
//var aggregated = turf.sum( hoods, points, 'population', 'sum');
var counted = turf.count(collection, points, 'pt_count');
console.log(counted)
} catch (e) {
console.log(e)
}
});
}
/* Triangular Irregular Network */
function makeTIN() {
// THis collection was pre-made using the spatial join (turf.tag) feature
var collection = westlake;
var tin = turf.tin(collection);
//var markers = L.geoJson(collection).addTo(map);
var display = L.geoJson(tin, tin_options);
display.addTo(map);
markers.push(display);
map.fitBounds(display.getBounds());
_.each(collection.features, function(x) {
// Convert lat/long to Leaflet coord
var coord = L.latLng(x.geometry.coordinates[1], x.geometry.coordinates[0]);
var radius = 30;
var circle = L.circle(coord, radius, circle_options).addTo(map);
markers.push(circle);
});
}
// Heat map of all points using Leaflet Heatmap
function makeHeatMap() {
coords = []; //define an array to store coordinates
heat_options = {
radius: 14,
gradient: {
0.4: '#80cdc1',
0.65: '#b8e186',
1.0: '#d01c8b'
}
}
_.each(points.features, function(feature) {
coords.push([feature.geometry.coordinates[1], feature.geometry.coordinates[0]]);
});
var scale = chroma.scale('PuBuGn');
try {
var heat = L.heatLayer(coords, heat_options)
markers.push(heat);
heat.addTo(map);
map.setZoom(14);
} catch (e) {
console.log(e)
}
}
// Show or Hide LA Neighborhoods
function showNeighborhoods(hide) {
if (hide == true) {
map.removeLayer(display_hoods)
} else {
display_hoods = L.geoJson(hoods, {
style: hood_options
});
display_hoods.addTo(map);
}
}
// Util function to clear all features/markers
function clearMap() {
for (i = 0; i < markers.length; i++) {
map.removeLayer(markers[i])
}
}
// UI for calling the different functions
function bindActions() {
doAnalysis(showNeighborhoods);
$("#show_hoods").change(function() {
var checked = $(this).prop("checked");
if (checked == true) {
showNeighborhoods();
} else {
showNeighborhoods(true);
}
});
}
function doAnalysis(which, arg) {
clearMap();
which.call(this, arg);
}
/* Map Setup, which is less interesting */
var mapMain = 'osaez.kp2ddba3';
L.mapbox.accessToken = 'pk.eyJ1Ijoib3NhZXoiLCJhIjoiOExKN0RWQSJ9.Hgewe_0r7gXoLCJHuupRfg';
map = L.mapbox.map('map', mapMain, {
attributionControl: false,
maxZoom: 14,
minZoom: 5
}).setView([34.0261899, -118.2455643], 10);
var markers = [];
var layers = [];
var hoods, points, display_hoods;
var circle_options = {
color: '#053275',
opacity: 0,
weight: .5,
fillColor: '#3490E7 ',
fillOpacity: .5
};
var hood_options = {
color: "#9013FE",
weight: .4,
opacity: 0.65,
fillColor: "#9013FE",
fillOpacity: .01
};
var tin_options = {
color: "#417505",
weight: .4,
opacity: 0.65,
fillColor: "#417505 ",
fillOpacity: .1
};
//Convert Shapefile to GeoJSON using Shapefile.js
shp("https://s3-us-west-2.amazonaws.com/s.cdpn.io/230399/la-poi-only.zip").then(function(geojson) {
json = geojson;
points = turf.featurecollection(json.features);
hoods = turf.featurecollection(neighborhoods.features);
prepAnalysis();
});
function prepAnalysis() {
// Show a box around all possible points of interest
hull = turf.convex(points);
layer = L.geoJson(hull);
bindActions();
}
In this article, I hope to have provided a comprehensive overview of the tools which are available to perform geospatial analysis and geoprocessing with JavaScript. Are you using these libraries in your projects already? Did I miss any out? Let me know in the comments.
If you want to go even further with geospatial analysis and geoprocessing with JavaScript, here are a few more resources and utilities:
Also published on: https://www.sitepoint.com/javascript-geospatial-advanced-maps/