Working with GeoJSON in the Browser

By Raymond Camden | 15 June 2020

I've been a bit enamored lately with GeoJSON and I thought it might be nice to look at how you can work with GeoJSON in the browser itself. Obviously it works well with Data Hub and you should consider storing your GeoJSON data there, but you can also work directly with GeoJSON via client-side JavaScript code. Let's consider some examples.

Accessing your Data

Before you begin, you obviously need your data. It's important to remember that GeoJSON is no different than JSON when it comes to usage within your browser. This means that if your data is hosted on a different server than your own, you need to ensure CORS is setup correctly before you can work with it. CORS (Cross-Origin Resource Sharing) is a fairly complex topic. At the simplest level, it's the mechanism by which your client-side code on your site can directly access data on another site. By default this is blocked for security reasons, but by using the appropriate headers, the other site can enable usage by other sites and their JavaScript code. Most APIs will have this header so that they can be used in JavaScript but it isn't guaranteed to be available.

Let's consider an example. I've often used the National Parks System GeoJSON data in blog posts and presentations. You can find the URL for that data here:

https://www.nps.gov/lib/npmap.js/4.0.0/examples/data/national-parks.geojson

Now let's consider a simple JavaScript request directly to it:

let resp = await fetch('https://www.nps.gov/lib/npmap.js/4.0.0/examples/data/national-parks.geojson');

If you run this in your browser, you'll get the following error:

Access to fetch at 'https://www.nps.gov/lib/npmap.js/4.0.0/examples/data/national-parks.geojson' from origin 'http://localhost:3333' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 

You've got two possible solutions to this problem. Reach out to the National Parks System's web site administrators and ask if they can add this header (a long shot probably), or host the data yourself. In this case you can literally take the URL and save the result to your own web site.

Don't forget you need to ensure you can do so legally. In this case it is public government data. But also note that by doing so, you won't know if and when the data ever changes.

The other solution would be to use server-side code to simply proxy a request from your server to their server. Specifics of that aren't relevant to this blog post, but it's a fairly common use case for serverless functions.

As I said, CORS is a rather complex topic and you can read more about it over at the excellent MDN site: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

Before moving on, one more quick mention of headers. GeoJSON has a specific content-type header. While you don't have to use it, if you do end up hosting your own GeoJSON data you should ensure it uses a content-type header of application/geo+json.

Working With Your Data

So assuming you've got your data and it's accessible, you can then work with it like any other JSON-related information. Let's look at a complete, simple example:

<html>
<head>
</head>

<body>

<h2>National Parks</h2>
<div id="results">
<i>Loading...</i>
</div>

<script>
document.addEventListener('DOMContentLoaded', init, false);

async function init() {
    console.log('init');
    let resp = await fetch('./national-parks.geojson');
    let data = await resp.json();
    let s = '<ul>';
    data.features.forEach(f => {
        s += `
        <li>${f.properties.Name} located at ${f.geometry.coordinates[1]}, ${f.geometry.coordinates[0]}</li> 
        `;
    });
    s += '</ul>';
    document.querySelector('#results').innerHTML = s;
    console.log(data);
}
</script>
</body>
</html>

In this example, I wait for the page to load, use the Fetch API to load my geojson, parse it, and then iterate over it. I've described the "structure" of GeoJSON before, but as a refresher, remember that GeoJSON consists of:

  • An array of features, the things you are mapping.
  • Each feature consists of a geometry field which is it's location and properties, a generic JavaScript object of custom data.

Don't forget that GeoJSON supports much more than just locations as points, but also supports polygons, lines, and multiple locations per features. Also don't forget that the values in properties are random. For the National Parks System data it consists of the name of the park and a code value for it. Don't forget that JavaScript is case-sensitive and you'll see that since my source data used Name, I have to use Name in my code. Finally, GeoJSON stores point data in an array where longitude is first, then latitude. Most people expect data in latitude,longitude form so I wrote it that way above. You can see the result below:

image1

Notice that I didn't do any kind of mapping with the information and that's ok. Geocoded data doesn't always need to be something you map, but as I feel guilty not including a map with that information, here's another version of the demo that adds markers across a map based on the same file.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Test</title>
        <meta name="description" content="">
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
        <script src="https://js.api.here.com/v3/3.1/mapsjs-core.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://js.api.here.com/v3/3.1/mapsjs-service.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://js.api.here.com/v3/3.1/mapsjs-mapevents.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://js.api.here.com/v3/3.1/mapsjs-ui.js" type="text/javascript" charset="utf-8"></script>
        <link rel="stylesheet" type="text/css" href="https://js.api.here.com/v3/3.1/mapsjs-ui.css" />
        <style>
            #mapContainer {
                width: 800px;
                height: 800px;
            }
        </style>
    </head>
    <body>

        <div id="mapContainer"></div>

        <script>
        document.addEventListener('DOMContentLoaded', init, false);

        async function init() {

            const KEY = 'c1LJuR0Bl2y02PefaQ2d8PvPnBKEN8KdhAOFYR_Bgmw';
            var platform = new H.service.Platform({
                'apikey': KEY
            });

            // Obtain the default map types from the platform object:
            var defaultLayers = platform.createDefaultLayers();

            var map = new H.Map(
                document.getElementById('mapContainer'),
                defaultLayers.vector.normal.map,
                {
                    zoom: 4,
                    center: { lat: 30.22, lng: -92.02 },
                    pixelRatio: window.devicePixelRatio || 1
                }
            );


            var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));

            // Create the default UI:
            var ui = H.ui.UI.createDefault(map, defaultLayers);

            var icon = new H.map.Icon('park.png');

            let resp = await fetch('./national-parks.geojson');
            let data = await resp.json();
            data.features.forEach(f => {
                map.addObject(new H.map.Marker({lat:f.geometry.coordinates[1], lng:f.geometry.coordinates[0]}, { icon:icon}));
            });

        }

        </script>
    </body>
</html>

This one makes use of our JavaScript Maps API to render a map. As before, we load in the GeoJSON file but this time as we iterate, we add markers to the map. Here's how it looks:

image2

Let's consider one more example. A browser can determine (with the user's permission of course) the user's location. Given we know where the user is, what if we could filter out the GeoJSON data to results that were nearby.

Doing this requires a few modifications to our previous example. First, we make use of the Geolocation API. This is where the browser can ask the user's permission and then retrieve their position. I've added this as a function:

async function getLocation() {
    return new Promise((resolve, reject) => {
        navigator.geolocation.getCurrentPosition(pos => {
            resolve(pos.coords);
        }, e => {
            reject(e);
        });
    });
}

Most of the code above is syntax sugar so I can make calling it easier with the await keyword. Once you know the user's position, you could then determine the distance from them to another location with the haversine formula. I found a simple to use one on StackOverflow:

function calcCrow(lat1, lon1, lat2, lon2)  {
    var R = 6371; // km
    var dLat = toRad(lat2-lat1);
    var dLon = toRad(lon2-lon1);
    var lat1 = toRad(lat1);
    var lat2 = toRad(lat2);

    var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
        Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2); 
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
    var d = R * c;
    return d;
}

// Converts numeric degrees to radians
function toRad(Value) {
    return Value * Math.PI / 180;
}

As hinted at by the function name, this isn't a "real" distance determination because it assumes you're travelling as "the crow flies", or a straight line, but it's good enough for now. So now that we can figure out the distance, here's a complete implementation:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Test</title>
        <meta name="description" content="">
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
        <script src="https://js.api.here.com/v3/3.1/mapsjs-core.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://js.api.here.com/v3/3.1/mapsjs-service.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://js.api.here.com/v3/3.1/mapsjs-mapevents.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://js.api.here.com/v3/3.1/mapsjs-ui.js" type="text/javascript" charset="utf-8"></script>
        <link rel="stylesheet" type="text/css" href="https://js.api.here.com/v3/3.1/mapsjs-ui.css" />
        <style>
            #mapContainer {
                width: 800px;
                height: 800px;
            }
        </style>
    </head>
    <body>

        <div id="mapContainer"></div>

        <script>
        document.addEventListener('DOMContentLoaded', init, false);

        async function init() {

            let position = await getLocation();
            console.log(position);

            const KEY = 'c1LJuR0Bl2y02PefaQ2d8PvPnBKEN8KdhAOFYR_Bgmw';
            var platform = new H.service.Platform({
                'apikey': KEY
            });

            // Obtain the default map types from the platform object:
            var defaultLayers = platform.createDefaultLayers();

            var map = new H.Map(
                document.getElementById('mapContainer'),
                defaultLayers.vector.normal.map,
                {
                    zoom: 6,
                    center: { lat: position.latitude, lng: position.longitude },
                    pixelRatio: window.devicePixelRatio || 1
                }
            );


            var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));

            // Create the default UI:
            var ui = H.ui.UI.createDefault(map, defaultLayers);

            var icon = new H.map.Icon('park.png');

            let resp = await fetch('./national-parks.geojson');
            let data = await resp.json();
            data.features.forEach(f => {
                let dist = calcCrow(position.latitude, position.longitude, f.geometry.coordinates[1], f.geometry.coordinates[0]); 
                if(dist < 500) {
                    map.addObject(new H.map.Marker({lat:f.geometry.coordinates[1], lng:f.geometry.coordinates[0]}, { icon:icon}));
                }
            });

        }

        async function getLocation() {
            return new Promise((resolve, reject) => {
                navigator.geolocation.getCurrentPosition(pos => {
                    resolve(pos.coords);
                }, e => {
                    reject(e);
                });
            });
        }

        // credit: https://stackoverflow.com/a/18883819/52160
        //This function takes in latitude and longitude of two location and returns the distance between them as the crow flies (in km)
        function calcCrow(lat1, lon1, lat2, lon2)  {
            var R = 6371; // km
            var dLat = toRad(lat2-lat1);
            var dLon = toRad(lon2-lon1);
            var lat1 = toRad(lat1);
            var lat2 = toRad(lat2);

            var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
                Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2); 
            var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
            var d = R * c;
            return d;
        }

        // Converts numeric degrees to radians
        function toRad(Value) {
            return Value * Math.PI / 180;
        }
        </script>
    </body>
</html>

Now our demo waits for the browser to report it's location. It then centers the map on that location and zooms in closer than before. It loads up the GeoJSON and filters the markers to those within 500km (an arbitrary decision on my part). Here's the result:

image3

As a quick aside, I've already mentioned Data Hub before, but this is a place where moving your geospatial data into Data Hub would be hugely beneficiation. In the demo above, all of the data needs to be loaded even though only a small portion of the features are rendered. Data Hub's APIs would allow you to pass in a location and radial distance and then return just those features within that circle!

I hope this quick introduction to using GeoJSON in the browser helps you understand how easy, and flexible, the format is. With browsers advancing at a rapid pace and JavaScript itself evolving, it's a great time to bet on the web! Check out my other GeoJSON posts here: