Skip to content

A bot tweeting from every mile of Vermont's Long Trail, Northbound.

License

Notifications You must be signed in to change notification settings

wboykinm/every-mile

 
 

Repository files navigation

Every Mile LT

A twitter bot that sequentially posts every mile of Vermont's Long Trail to Twitter.

99.2% the work of the amazing Jason Sanford.

Active Bots

Path Distance
Long Trail 249
Twitter Follow badge

How it works

Prepping a bot

A series of scripts are run on a GeoJSON file representing the entire length of a trail. The following steps assume you have:

  1. added a new trail's full geometry as all.geojson in the /geom/{identifier} directory, where identifier is a short string representing the trail's name (lt => Long Trail).
  2. Added constants for this new trail in /src/constants.ts including total trail distance, Mapbox map id, and mile indicator buffer (seen on images).

01_create_mile_sections

This walks the entire trail geometry mile-by-mile and creates .geojson files for each, represented as a GeoJSON Feature. Initially the properties for this feature is an empty object, to be populated by subsequent scripts.

A trail identifier must be passed as a parameter when running this script.

node dist/scripts/01_create_mile_sections.js at

02_add_city_property

This iterates every mile of the trail by reading its GeoJSON Feature (/geom/{identifier}/mile_{number}.geojson). Once the trail is loaded we use Mapbox's reverse geocode API to convert the first coordinate's (start of mile) latitude and longitude into a set of place identifiers. The API returns an array of definitions for how to describe this place (country, city, park, zip code, etc). Instead of parsing and processing all of these definitions in this step we just save them all to be processed at time-of-post. Async's series is used together with setTimeout to (hopefully) keep from hammering the API and getting a bad response.

A trail identifier must be passed as a parameter when running this script.

node dist/scripts/02_add_city_property.js at

03_add_elevations

Again, iterating each mile one at a time we series and setTimeout to rate-limit requests to the Mapbox API. This is the most complex of the scripts. It iterates over each coordinate in the LineString representing the mile and adds a new elevations member of properties representing the elevation for each coordinate. This works by using Mapbox's Terrain-RGB data set and doing some clever tile math over in /src/elevation.ts. The cleverness wasn't mine, but I borrowed it from this repo. I simply added the ability to cache tiles so that we didn't make an API request for every single coordinate over a trail thousands of miles long.

A trail identifier must be passed as a parameter when running this script.

node dist/scripts/03_add_elevations.js at

04_add_elevation_stats

This step simply uses the elevation data from the previous step and does some simple math to find minimum elevation, maximum elevation, and total elevation gain (or loss).

A trail identifier must be passed as a parameter when running this script.

node dist/scripts/04_add_elevation_stats.js at

05_generate_maps

Using the same rate-limiting in the previous steps, iterate through each mile and use the Mapbox API to create a static map image and save it to disk. The only interesting thing we are doing here is buffering the LineString representation of a mile to create a Polygon that highlights this specific mile. Each map image is saved to disk under /images/{identifier}/mile_{number}.png. It's easier to go ahead and pre-render these so that each Github action job runs quickly. After this step is done we're almost ready to post.

A trail identifier must be passed as a parameter when running this script.

node dist/scripts/05_generate_maps.js at

06_generate_profiles

The elevation data that's already been collected in step 4 can be used to create a D3.js elevation profile, which we screengrab using the node puppeteer module and place in the same folder as the mile_* maps, to be posted alongside them in each tweet. For silly http server reasons we activate this script from within its folder:

cd dist/profile/
bash 06_generate_profiles.sh lt
cd ../../

Credentials

With all these files in hand, it's time to set up the github actions. You'll need 4 credentials for each bot, stored in a .env file. A .env-sample file is provided as an example. These values will automatically be read into environment variables at runtime for use in the Twitter client. Note the _{identifier} pattern after each of the 4 credentials. These should match the trail identifier value used in scripts and configuration files.

TWITTER_APP_KEY_lt=
TWITTER_APP_SECRET_lt=
TWITTER_ACCESS_TOKEN_lt=
TWITTER_ACCESS_SECRET_lt=

These credentials variables must be stored as separate repository secrets on github - in Settings --> Secrets --> New Repository Secret

Posting

This step assumes you have set up a new GitHub workflow at /.github/workflows/{identfier}.yml representing the new trail.

The tweet_a_mile script takes care of posting to Twitter. This walks through each mile for a trail until it finds one where trail.properties.has_tweeted is falsey, which indicates this is the next trail to be posted. Once this mile section is determined other properties are checked for displaying location and elevation information. Then we find the corresponding map image (or gif, see below) that should be associated with the tweet, located in /images/{identifier}/mile_{number}.png.

After a successful post, trail.properties.has_tweeted is set to true so we know not to tweet this mile again. Finally, the Commit it step in the GitHub workflow runs to commit this change, ensuring this mile is marked as having been tweeted and setting us up with clean repo to run again on schedule. See the GitHub actions workflow files (/.github/workflows/{identifier}.yml) for more details about this step.

Extra

For each post the bot looks for a gif before the png. Some mile sections are particularly interesting and look great when animated. If a gif is found it takes priority and is shown instead of the png.

Generating animations

MILE=<3-digit mile number>
cd animate
echo "const feature = " > feature.js
cat ../geom/lt/mile_${MILE}.geojson >> feature.js
static-server -p 8000

Then hit http://localhost:8000/ and wait for the download prompt!

Then turn it into a gif and commit it

ffmpeg -itsscale 0.3 \
  -i gifs/mile_${MILE}.mp4 \
  -vf "fps=10,scale=600:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
  -loop -1 \
  gifs/mile_${MILE}.gif

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 65.1%
  • HTML 29.0%
  • JavaScript 3.9%
  • Shell 2.0%