Hands On

Great Guide to Generating Good GeoJSON

By Raymond Camden | 30 September 2019

Let me begin by apologizing for the title. I saw a chance for alliteration and I had to take it. In my last article, I described what GeoJSON is and how it can be used. Today I wanted to demonstrate a few examples of how you can serve up existing data in GeoJSON. As always, I'd love to hear your own take on this and if you have an example you can share, drop me a comment below. With that out of the way, let's look at our examples.

"Good" Data

In our example, we're going to work with "good" data, which is simply my way of saying data that is 100% ready to be converted to GeoJSON. Our data lives in a MongoDB object store and consists of National Parks data. (As an FYI, the National Parks System already uses GeoJSON and you can find their data here). Our Mongo collection consists of a name, a park code, and a longitude and latitude value.

parkob

So in theory, all we need to do is get our data and then output in the right format. While the spec will give you specifics, in general our GeoJSON file is going to look like so:

{
    "type": "FeatureCollection",
    "features": [
        // lots of features here
             {
                "type": "Feature",
                "geometry": {
                    "type": "Point", //GeoJSON supports other types
                    "coordinates": [longitude, latitude]
                },
                "properties": {
                    // ad hoc properties
                }
            }
    ]
}

Given this basic form, let's look at a script that demonstrates this.

const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017';
const dbName = 'national-parks';
const client = new MongoClient(url, { useNewUrlParser: true, useUnifiedTopology: true });

client.connect(function (err) {

    const db = client.db(dbName);

    const collection = db.collection('parks');
    collection.find({}).toArray( (err, docs) => {

        let result = {
            "type": "FeatureCollection",
            "features": []
        };

        docs.forEach(d => {

            let feature = {
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [d.longitude, d.latitude]
                },
                "properties": {
                    "code":d.code,
                    "name": d.name
                }
            };

            result.features.push(feature);
        });

        console.log(JSON.stringify(result));
        client.close();

    }); 
});

Alright, the top portion of the script is simply setup stuff so that I can communicate with a local MongoDB server. I connect to the database and then ask for every document. I set up a result object and as I iterate over each document, I add to the features of it. That's it. Notice how the longitude and latitude values go into the coordinates array and then the rest of my data simply become properties. If my object was more complex I could simply loop over each property and output that dynamically, ignoring the location values.

And that's it. My script simply outputs to the console but in the real world it could be used within an Express route or a serverless function rather easily.

Here's an example of the output with most of the data stripped out:

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [
                    -71.13112956925647,
                    42.32550867371509
                ]
            },
            "properties": {
                "code": "FRLA",
                "name": "Frederick Law Olmsted National Historic Site"
            }
        },
        // a heck of a lot more
    ]
}

"Not So Good" Data

For the next version, let's look at bad… I mean "not quite as good" data. Our previous example was easy since we had location data already. But what if we don't? Our next set of data is a collection of cats. Each cat object consists of a name, a gender, and an address string.

catob

In order to generate GeoJSON, we need a way to get a precise address from the string based address. For that we're going to use HERE's Geocoding API. This API lets you translate string based addresses into precise locations (as well as the reverse, and other features). In order to do so you can simply pass the address string as searchtext to the API.

https://geocoder.api.here.com/6.2/geocode.json?searchtext=someAddress&app_id=YOUR_APP_ID&app_code=YOUR_APP_CODE

Let's look an example similar to the previous one, but that now adds calls to the geocoding API to get the data it needs.

const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017';
const dbName = 'cats';
const client = new MongoClient(url, { useNewUrlParser:true, useUnifiedTopology: true });
const fetch = require('node-fetch');

const app_id = 'sdwyQs1YA25FUX6cI7Ko';
const app_code = 'SX9_A9lBaqJYoWX_DIO1ag';

async function getLocation(s) {

    let url = `https://geocoder.api.here.com/6.2/geocode.json?searchtext=${encodeURIComponent(s)}&app_id=${app_id}&app_code=${app_code}`;
    let httpResult = await fetch(url);
    let result = await httpResult.json();
    return result.Response.View[0].Result[0].Location;
}

function getCats() {

    return new Promise((resolve, reject) => {

        client.connect(err => {
            const db = client.db(dbName);
            const collection = db.collection('cats');

            collection.find({}).toArray((err, docs) => {
                if(err) reject(err);
                client.close();
                resolve(docs);
            });

        });

    });
}

(async () => {

    let cats = await getCats();

    for(let i=0;i<cats.length;i++) {
        let gpsLocation = await getLocation(cats[i].location);
        cats[i].coordinates = [gpsLocation.NavigationPosition[0].Longitude, gpsLocation.NavigationPosition[0].Latitude]
    }

    let result = {
        "type": "FeatureCollection",
        "features": []
    };

    cats.forEach( c => {
        let feature = {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": c.coordinates
            },
            "properties": {
                "name": c.name,
                "gender": c.gender
            }
        };

        result.features.push(feature);

    });

    console.log(JSON.stringify(result));
})();

This script is a bit more complex as we've got a couple different asynchronous operations in play. First I get my array of cats. Then I iterate over each and call out to the API to 'translate' their location. The use of async and await make this somewhat less painful to write. Once the data is retrieved the script follows the same format as before - iterate and add features. Here's the output, again with some trimming.

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [
                    -92.05749,
                    30.20666
                ]
            },
            "properties": {
                "name": "Juniper Montage",
                "gender": "female"
            }
        },
        {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [
                    -97.73512,
                    30.3581
                ]
            },
            "properties": {
                "name": "Thor",
                "gender": "male"
            }
        }
    ]
}

To be clear, this script is not optimized for repeated calls. In production I'd use a script that added longitude and latitude to the object store, or cached it elsewhere, so that you would not need to 're-geocode' the same address more than once. But hopefully this gives you enough of an example to get started!

Use that Data for $$$

So now that you know how to take your data and create GeoJSON, it would be a perfect time to take a look at the Mapathon we are running over the next two months. All you have to do is take your awesome data, create a cool map, and enter for a chance to win cold hard cash!