Measuring the Walk Potential of Large Amenities

Measuring the Walk Potential of Large Amenities
Ped Shed for a large park

To calculate the Walk Potential around most amenities, we find the center of amenity, and generate a 10-minute walk radius around it. This area called a "ped shed" or more technically an isochrone.

That works well for a small downtown store front, but it's not great if the amenity is Central Park in Manhattan, which is 2.5 miles long. Central Park is an interesting destination even if you walk to see one end of it and never touch the center.

With that in mind, I'm updating to the Walk Potential algorithm for v4.0 to better account for that. Here I'll show the effects of the calculations before and after the change.

Testing the Walk Potential of Elver Park

The first location we'll look at is Elver Park, in Madison, Wisconsin:

Elver Park

Before, the first step would is find the center:

import fs from 'fs';
import center from '@turf/center';

const geojson  = JSON.parse(fs.readFileSync('/dev/stdin').toString());
fs.writeFileSync('/dev/stdout', JSON.stringify(center(geojson)));

Here you can see the calculated center of Elver Park:

Elver Park with center marker

Next we would generate an isochrone for a 10-minute walk from the center. In the code, this is done by making a call to an OpenTripPlanner server with coordinates, using a URL that returns GeoJSON like this:

${URL}/otp/routers/central/isochrone?fromPlace=43.037080849999995,-89.50670265&mode=WALK&date=2023-03-16&time=8:00am&cutoffSec=600

And here's the result, presented with the park border and the center. We can see the isochrone barely reaches outside the park.

Elver Park with center and isochroneent

An updated approach

So how we can do better? The park has multiple entrances. Ideally, we could find nearly places that are within a 10-minute walk of any of the entrances. Here's the updated approach to be used in Walk Potential 4.0:

  1. Create a bounding box (rectangle) around the amenity
  2. Within that bounding box, create a grid of hexs, of the same size that we'll use for future calculations. These have a diameter of 0.1km.
  3. Find the center or each hex.
  4. "Negative buffer" the the polygon for the park by the diameter of one hex cell. In other words, scale the park shape down to make a smaller interior version of it.
  5. Finally check, if any of these hex center points are outside the interior shape while being inside the amenity boundary itself.

The result of those steps should be a series of points that are near the edge of the amenity. Running those calculations against Elver Park, here is the resulting set of points:

Points around the edge of the park better represent potential walk destinations

Now calculate the Walk Potential for the park by building isochrones representing a 10 minute walk from each of these locations and merging them together. Here's what that looks like:

The updated "ped shed" for Elver Park: Locations within a 10 minute walk from it. 

There's doughnut because there's a lack of paths in that area to route walks on.  This looks like a much better representation of places within a 10-minute walk from the park!

For a second test, let's try another challenging park shape: Switchyard Park in Bloomington, Indiana is long and narrow. Here it is with the center marked:

Switchyard Park in Bloomington, Indiana and 10 minute "ped shed" isochrone

And here's the updated calculation of of the places that are within a 10 minute walk from Switchyard Park, using the more advanced calculation. We see that even though the park is rather narrow, it was still wide enough so that some points were found within the narrow parts using this method.

Places within a 10-minute walk from Switchyard Park, using a union of isochrones from many points near the edge.

A park that was much narrower would be very close to just being a trail, and the Walk Potential metric is focused on potential destinations without regard for whether their are sidewalks or trails on the way there. It's different in that regard than a "walkability" metric which might try to score based on walking facilities. In other words: it's not intended to highlight trails, so it's OK this doesn't work or parks narrower than .1km.

Code to Calculate those points

The complex part of the logic here is calculating those points. Here's the code I used to generate the above points using Turf.js.

You'll see that that we only use this more advanced calculation if the area of the amenity we are checking is larger than the area of our hexes, which have a width of .1km. Also, if the advanced calculation fails to detect any extra points inside the polygon, then we also fallback to using the center point in that case as well.

This would not just be used for parks, but for all large amenities, for example a business with a sprawling building footprint would also be handled this way when calculating Walk Potential.

/*
Given GeoJSON for a polygon on STDIN, print to STDOUT points inside the polygon and near
the border.

Visualize the results on https://geojson.io or your favorite mapping tools.

This can be used for testing the algorithm.

Ex: node test/find-expanded.mjs <test/data/bloomington-in-switchard-park.geojson
*/
import fs from 'fs';
import area from '@turf/area';
import buffer from '@turf/buffer';
import hexGrid from '@turf/hex-grid';
import center from '@turf/center';
import { getCoords, getType } from '@turf/invariant';
import boolPointInPolygon from '@turf/boolean-point-in-polygon';
import calcBbox from '@turf/bbox';

// Length of one of the six hex sides
const HEX_SIDE = 0.05;// in km
// "diameter" of a hex
const HEX_WIDTH = 2 * HEX_SIDE;
const HEX_AREA = 6495; // square meters

function getHexGrid(mask, bbox) {
  const options = {
    units: 'kilometers',
    mask,
  };
  return hexGrid(bbox, HEX_SIDE, options);
}

const featureCollection = JSON.parse(fs.readFileSync('/dev/stdin').toString());

const maybeExpandedFeatures = featureCollection.features.flatMap((feature) => {
  if ((getType(feature) === 'Polygon') && (area(feature) > HEX_AREA)) {
    // Find the bbox for the polygon.
    const bbox = calcBbox(feature);
    // Generate hexes that fit in the polygon
    const grid = getHexGrid(feature, bbox);
    // Return the centers of those hexes
    if (grid.features.length) {
      // Keep the point if it's within one hex width of the edge
      // To calculate that, buffer one hex width smaller and check if the points are outside that.
      const smallerPoly = buffer(feature, -HEX_WIDTH); // Turf.js defaults to km
      return grid.features
        .map(center)
        .filter((centerPoint) => {
          // The negative buffer process can cause an undefined result here, so reality-check
          if (smallerPoly && getType(smallerPoly) === 'Polygon') {
            return boolPointInPolygon(getCoords(centerPoint), feature)
              && !boolPointInPolygon(getCoords(centerPoint), smallerPoly);
          }
          // Small polygons that failed negative buffering don't need filtering.
          return true;
        });
    // If no hexes fit in the polygon, fallback to using the center.
    }
    return center(feature);
  }
  return feature;
});

fs.writeFileSync('/dev/stdout', JSON.stringify(maybeExpandedFeatures));

Staying in Touch

You can provide feedback by emailing me at mark@stosberg.com or find me on Mastodon at @markstos@urbanists.social.

If you use the Subscribe feature on this blog, you'll find the option to subscribe to only mapping-related posts.

Also, if you create an account Gitlab, subscribing to new release notifications of Walk Potential is one of the custom notification options.

Walk Potential v4.0 has since been released. See Release Notes.