GeoJSON is a standardized format for representing geographic data in JSON. There's a lot of great tooling out there for visualizing GeoJSON data. And GeoJSON is good for more than just storing points: it can represent points, lines, polygons, and collections.

Points

A GeoJSON point looks like this:

{
  "type": "Point",
  "coordinates": [-80.1347334, 25.7663562]
}

This point represents a park in Miami Beach, FL. One easy way to visualize this point on a map is using geojson.io.

It is important to note that coordinates is in the format [lng, lat]. Longitude comes before latitude in GeoJSON. That is because longitude represents east-west position (x axis on a typical map), and latitude represents north-south position (y axis on a typical map), and the authors of the GeoJSON spec wanted to keep x, y coordinate order.

A common use case for GeoJSON points is geocoding: converting an address like "429 Lenox Ave, Miami Beach, FL" to latitude and longitude coordinates. For example, suppose you use the Mapbox geocoding API. You would make an HTTP request to the below endpoint:

https://api.mapbox.com/geocoding/v5/mapbox.places/429%20lenox%20ave%20miami.json?access_token=pk.eyJ1IjoibWF0dGZpY2tlIiwiYSI6ImNqNnM2YmFoNzAwcTMzM214NTB1NHdwbnoifQ.Or19S7KmYPHW8YjRz82v6g&cachebuster=1581993735895&autocomplete=true

And get the below result:

{"type":"FeatureCollection","query":["429","lenox","ave","miami"],"features":[{"id":"address.8052276751051244","type":"Feature","place_type":["address"],"relevance":1,"properties":{"accuracy":"rooftop"},"text":"Lenox Avenue","place_name":"429 Lenox Avenue, Miami Beach, Florida 33139, United States","center":[-80.139145,25.77409],"geometry":{"type":"Point","coordinates":[-80.139145,25.77409]}, ...}

If you look closely, features[0].geometry in the above JSON output is a GeoJSON Point:

{"type":"Point","coordinates":[-80.139145,25.77409]}

Mapbox's static maps API is a great way to render points on a map. Below is a script that geocodes a given string, and returns a URL to an image that displays the first search result.

const axios = require('axios');

async function search(str) {
  const geocoderUrl = 'https://api.mapbox.com/geocoding/v5/mapbox.places/' +
    encodeURIComponent(str) +
    '.json?access_token=' +
    'pk.eyJ1IjoibWF0dGZpY2tlIiwiYSI6ImNqNnM2YmFoNzAwcTMzM214NTB1NHdwbnoifQ.Or19S7KmYPHW8YjRz82v6g';

  const res = await axios.get(geocoderUrl).then(res => res.data);
  const point = res.features[0].geometry;

  return 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/' +
    'pin-l-1+333(' + point.coordinates[0] + ',' + point.coordinates[1] + ')/' +
    point.coordinates[0] + ',' + point.coordinates[1] +
    ',14.25,0,0/600x600/' +
    '?access_token=pk.eyJ1IjoibWF0dGZpY2tlIiwiYSI6ImNqNnM2YmFoNzAwcTMzM214NTB1NHdwbnoifQ.Or19S7KmYPHW8YjRz82v6g';
}

search('429 Lenox Ave, Miami Beach').then(res => console.log(res));

LineStrings

A GeoJSON line string is an array of coordinates representing a line on a map. Below is a GeoJSON line string that represents the approximate border between the US states of California and Oregon:

{
  "type": "LineString",
  "coordinates": [[-124.2, 42], [-120, 42]]
}

Line strings pop up when you use a navigation API like Mapbox's to get turn-by-turn directions between two points. One way to represent the driving directions from [-80.139145,25.77409] (a WeWork in Miami Beach) to [-80.2752743,25.7938434] (Miami International Airport) is with a GeoJSON line string:

{
  "type": "LineString",
  "coordinates": [
    [-80.139153, 25.774281],
    [-80.13829, 25.774307],
    [-80.142029, 25.774479],
    [-80.148438, 25.772148],
    [-80.151237, 25.772232],
    [-80.172043, 25.78116],
    [-80.177322, 25.787195],
    [-80.185326, 25.787212],
    [-80.189804, 25.785891],
    [-80.19268, 25.785954],
    [-80.202301, 25.789175],
    [-80.207954, 25.788721],
    [-80.223, 25.782646],
    [-80.231026, 25.78261],
    [-80.238007, 25.784889],
    [-80.246025, 25.784403],
    [-80.249611, 25.785175],
    [-80.253166, 25.786049],
    [-80.259262, 25.786324],
    [-80.264038, 25.786186],
    [-80.264221, 25.787256],
    [-80.264214, 25.791618],
    [-80.264221, 25.792633],
    [-80.264069, 25.795443],
    [-80.263397, 25.795652],
    [-80.263786, 25.794928],
    [-80.267723, 25.794926],
    [-80.271141, 25.794859],
    [-80.273163, 25.795704],
    [-80.275009, 25.796482],
    [-80.277481, 25.796461],
    [-80.278435, 25.795622],
    [-80.278061, 25.794088],
    [-80.275276, 25.793804]
  ]
}

Routing LineStrings can be pretty complex. The above line string represents an easy 15 minute drive! Here's what the above line string looks like on a map:

Below is a simple script that returns a GeoJSON line string representing the directions between 2 points using Mapbox's directions API:

const axios = require('axios');

async function directions(fromPt, toPt) {
  const fromCoords = fromPt.coordinates.join(',');
  const toCoords = toPt.coordinates.join(',');
  const directionsUrl = 'https://api.mapbox.com/directions/v5/mapbox/driving/' +
    fromCoords + ';' + toCoords + '?' +
    'geometries=geojson&' +
    'access_token=pk.eyJ1IjoibWF0dGZpY2tlIiwiYSI6ImNqNnM2YmFoNzAwcTMzM214NTB1NHdwbnoifQ.Or19S7KmYPHW8YjRz82v6g';

  const res = await axios.get(directionsUrl).then(res => res.data);
  return res.routes[0].geometry;
}

const wework = { type: 'Point', coordinates: [-80.139145,25.77409] };
const airport = { type: 'Point', coordinates: [-80.2752743,25.7938434] };

directions(wework, airport).then(res => {
  console.log(res);
});

Polygons

GeoJSON polygons represent closed shapes on a map, like triangles, squares, dodecagons, or any shape with a fixed number of sides. For example, the below GeoJSON polygon represents (approximately) the US state of Colorado:

{
  "type": "Polygon",
  "coordinates": [[
    [-109, 41],
    [-102, 41],
    [-102, 37],
    [-109, 37],
    [-109, 41]
  ]]
}

GeoJSON polygons can be pretty complex. For example, for a while Uber was using a single GeoJSON polygon to capture all 3 major San Francisco Bay Area airports:

However, note that GeoJSON polygons cannot represent a circle or ellipse.

What are GeoJSON polygons used for? Usually for geofences. For example, suppose you're working for Uber or Lyft, and you want to show a special screen to customers requesting a ride from the airport. To do that, you need to check whether the GeoJSON point representing where the customer requested a ride from is within the GeoJSON polygon representing the airport (or airports, as was the case in the previous image).

One way to check whether a GeoJSON point is within a polygon is using the Turf npm module. The @turf/boolean-point-in-polygon module checks whether a given point is in a given polygon.

const pointInPolygon = require('@turf/boolean-point-in-polygon').default;

const colorado = {
  "type": "Polygon",
  "coordinates": [[
    [-109, 41],
    [-102, 41],
    [-102, 37],
    [-109, 37],
    [-109, 41]
  ]]
};

const denver = {
  "type": "Point",
  "coordinates": [-104.9951943, 39.7645187]
};

const sanFrancisco = {
  "type": "Point",
  "coordinates": [-122.4726194, 37.7577627]
};

// true
console.log(pointInPolygon(denver, colorado));

// false
console.log(pointInPolygon(sanFrancisco, colorado));

Turf lets you check whether a point is in a polygon from Node.js. But what about database queries? MongoDB's built-in $geoIntersects operator handles GeoJSON, so you can write a single query to check what US state a given point is in.

const mongoose = require('mongoose');

run().catch(err => console.log(err));

async function run() {
  await mongoose.connect('mongodb://localhost:27017/geotest', {
    useNewUrlParser: true,
    useUnifiedTopology: true
  });
  await mongoose.connection.dropDatabase();

  const State = mongoose.model('State', mongoose.Schema({
    name: String,
    location: mongoose.Schema({
      type: String,
      coordinates: [[[Number]]]
    })
  }));

  const colorado = await State.create({
    name: 'Colorado',
    location: {
      "type": "Polygon",
      "coordinates": [[
        [-109, 41],
        [-102, 41],
        [-102, 37],
        [-109, 37],
        [-109, 41]
      ]]
    }
  });

    const denver = {
    "type": "Point",
    "coordinates": [-104.9951943, 39.7645187]
  };

  const sanFrancisco = {
    "type": "Point",
    "coordinates": [-122.4726194, 37.7577627]
  };

  // What state is Denver in?
  let res = await State.findOne({
    location: {
      $geoIntersects: { $geometry: denver }
    }
  });
  res.name; // 'Colorado'

  // What state is San Francisco in?
  res = await State.findOne({
    location: {
      $geoIntersects: { $geometry: sanFrancisco }
    }
  });
  res; // null
}

Moving On

GeoJSON lets you go much further with geo data than storing simple points. You can store navigation directions, check when users enter a geofence, and even generate isochrones. There's some great tooling around GeoJSON: geojson.io for simple visualizations, Mapbox for sophisticated APIs, Turf for in-memory geospatial calculations, and MongoDB for geospatial queries. If you're storing points as coordinate pairs, you're missing out on some excellent developer tools.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus