GetBounds() in the Era of Pitch-able and Rotate-able Maps

Chris Whong
NYC Planning Tech
Published in
4 min readMay 3, 2018

Web maps can be more than just a canvas for visualizing spatial data. Sometimes they can serve as an interactive UI that allows the user to specify an area of interest on the earth. This is useful for “Clip and Ship” data access tools, which allow the user to download only a subset of a larger spatial dataset, using the bounds of the web map to specify the extent to be clipped. I’ve built several of these Clip and Ship tools for NYC Open Datasets as hobby projects, such as this PLUTO data downloader.

Web mapping libraries will usually include a getBounds() method, which will quickly get you an object containing the lat/lon coordinates for the southwest and northeast corners of the map. Here it is in leaflet.js:

These coordinates can then be used to build a spatial query in PostGIS to select everything from a dataset that lies within the bounding box. One way to do this is to use the ST_MakeEnvelope() function, which creates a rectangle which we can then use with ST_Intersects() to filter the data.

const bounds = {
_southWest: {
lat: 40
lng: -72
},
_northEast: {
lat: 41
lng: -73
},
};
const SQL = `
SELECT *
FROM somelargetable
WHERE ST_Intersects(
the_geom,
ST_MakeEnvelope(
${_southWest.lat},
${_southWest.lng},
${_northEast.lat},
${_northEast.lng},
4326
)
)
`;

So at NYC Planning Labs we’ve been using MapboxGL for our web mapping apps, and recently encountered the Clip and Ship problem. MapboxGL does indeed have a getBounds() method, so we used the technique above to pass queries to PostGIS but noticed something fishy going on when the view wasn’t “north-up”.

MapboxGL allows the user to pitch and rotate the map canvas, which allows for really fitting a study area into view, or aligning a map to a street grid, etc. However, getBounds() still returns just two sets of coordinates, which is only enough to describe a “north-up” rectangle.

Before we move on to the solution, a quick visual will demonstrate the difference between the rectangle defined by getBounds() and the actual map view. This GL map starts with a rotated map aligned to the Manhattan grid, with Central Park in view. The red rectangle represents the results of getBounds(), while the blue quadrangle shows extent of the map view.

Notice how as the map is rotated to north-up, the two polygons slowly merge into one, but once pitch is added they begin to stray apart again. Essentially, getBounds() is doing what it always has, unprojecting the bottom-left and top-right corners of the map view, but since rotation and pitch are in the mix, the resulting rectangle doesn’t accurately capture the view. (The widget GIF’d below is live on github pages if you want to try it out on your own)

So, how do we clip and ship in the 21st century, when a user’s view might be a rotated rectangle or funky trapezoid? The answer, which is outlined in this github issue, is to get the dimensions of the canvas element that mapboxGL is drawing on, and then call map.unproject() on each of the four corners.

A pitched and rotated MapboxGL view extent is rendered in blue, while the results of getBounds() is shown in red.

This function, which is used in the example above, does just this, and then munges the coordinates for the four corners into a geoJSON polygon.

// get a geojson rectangle for the current map's view
const buildViewBoundsGeoJSON = (map) => {
const canvas = map.getCanvas();
let { width, height } = canvas;
const cUL = map.unproject([0, 0]).toArray();
const cUR = map.unproject([width, 0]).toArray();
const cLR = map.unproject([width, height]).toArray();
const cLL = map.unproject([0, height]).toArray();
return {
type: 'Polygon',
coordinates: [[cUL, cUR, cLR, cLL, cUL]],
crs: {
type: 'name',
properties: {
name: 'EPSG:4326',
},
},
};
};

The resulting geoJSON polygon can then be used wherever geoJSON is useful! Here’s how we use the geoJSON to build an ST_Intersects() query in PostGIS to clip and ship:

const SQL = `
SELECT *
FROM somelargetable
WHERE ST_Intersects(
the_geom,
ST_GeomFromGeoJSON(
'${JSON.stringify(geoJSON)}'
)
)
`;

We generally build these queries in the client and feed them directly to the Carto Maps API, which will trigger a shapefile or geoJSON download for the resulting clipped data.

Happy mapping!

Did you find this useful? Let us know by tweeting to @nycplanninglabs

--

--

Chris Whong
NYC Planning Tech

Urbanist, Mapmaker, & Data Junkie. Outreach Engineer at Qri.io