Geocode the camps of Black Rock City. Takes the official Burning Man placement PDF and the camp roster JSON, walks you through some interactive corrections, and emits two enriched JSON files that pair each camp with its real-world location on the playa.
For every camp in camps.json that could be matched to a polygon on the map,
the camp's location object gains:
-
location.centroid— a GeoJSONPointthat indicates where the camp should be indicated on a map for navigation. This might be at the center of the frontage, or at a corner for a corner camp. In some cases where camps have weird shapes, the centroid just ends up at the geographical center of the camp."centroid": { "type": "Point", "coordinates": [-119.21450503439554, 40.77861635238121] }
-
location.border— a GeoJSONPolygonoutlining the camp's footprint:"border": { "type": "Polygon", "coordinates": [[[-119.2142, 40.7785], [-119.2143, 40.7784], ...]] }
Two output files are written:
2025/camps-geocoded.json— camps +centroidonly.2025/camps-geocoded-with-borders.json— camps +centroid+border.
Camps that don't appear on the map come through unchanged (no centroid, no border) in both files.
We use two input files:
2025/input.pdf— the official BRC placement map2025/camps.json— the camp roster as an array of objects, from the Burning Man API
Data for each year of burning man is only released to the public when the gate opens. If you need earlier access to build your app and have it ready in time for the burn, join the org's Innovate community -- you will have to sign a nondisclosure agreement.
npm installRequires Node ≥ 18 (uses native ES modules), plus tesseract on your PATH
(brew install tesseract) for any year whose PDF ships its labels as outlines
rather than text — see below.
The map is redrawn every year and the artists rename and restructure the PDF's
layers as they go. layers.js holds one profile per year and is the only place
that knows the difference; everything downstream speaks the 2025 vocabulary
(Camp Outlines, Camp Names, Listed Name Callouts, Base Map Things).
What changed in 2026:
- Layer names.
Camp OutlinesbecameListed Theme Camp outlinesplusSupport camps;Camp NamesbecameListed Camp Names/Unlisted Camp Names;Listed Name CalloutsbecameCallouts. - No text at all. Every label was converted to outlines before export, so
the names arrive as ~15k glyph-shaped paths.
ocr.jsclusters those glyphs back into labels — using the camp outlines themselves to decide which glyphs belong together — renders each one upright, reads it with tesseract, and snaps the result to the closest name incamps.json. It gets ~97% of the listed camps; the rest fall into the usual unmatched-camp naming step. Readings are cached in<year>/ocr-cache.json, so only the first run is slow (~20s). Delete that file to re-read from scratch. - Camp outlines are filled ribbons. Each camp is a stroke converted to
outlines, i.e. two rings about 8 real feet apart.
layers.jspulls them back to the single centreline the artist actually drew, keeping the curves intact so frontage detection still recognises a camp facing a circular street. A camp drawn with just one ring was never closed; those come through open so the client still flags them for you to fix. - Callouts are filled artwork — a dot on the camp and an outlined leader — rather than a stroked two-point line, so they get converted back.
To work on a new year, add a profile to layers.js and point YEAR in
server.js at it.
npm startThen open http://localhost:3000.
The server extracts the four target layers from input.pdf at startup
(takes a few seconds for the full city) and serves the interactive app from a
single page.
A compass in the top-right toggles between 12:00-up (the PDF's native orientation) and true-north-up.
- Pan: drag with the left mouse button.
- Zoom: mouse wheel or trackpad pinch.
- Fit to window: 0 or f.
- Toggle orientation: click the compass icon in the top-right.
- Hover a camp: tooltip shows the camp's name (bold),
location_string,dimensions, andexact_locationfrom the roster.
The app guides you through the calibration steps in order. A status bar at the bottom left corner prompts for each one.
-
Set the Golden Spike. Click
YES, then aim at the dead center of the man and press Enter (red guides snap to the BRC clock-face radials to help you align). Confirm withACCEPT. -
Calibrate the map scale. Translucent green BRC streets are overlaid on top of the PDF. Use + / − to grow/shrink them and < / > to refine/coarsen the step size until the green rings sit on the actual streets. Press Enter to accept.
-
Enter the spike's lat/lon. Paste the published coordinates of the Golden Spike for the current year (e.g.
40.786958, -119.202994). Get this from the Burning Man datasets page -
Fix unclosed polygons. The camp outlines on the PDF map were hand drawn by placement team members nad sometimes they are not closed polygons due to user error. They will show up in red. For each one, you have three options: CLOSE AUTOMATICALLY (snaps the last vertex back to the first), DELETE (if the whole polygon is an artifact), or REDRAW (let's you draw a fresh polygon to replace the wrong one in the map, by clicking vertices — Esc cancels, Enter or clicking the start vertex closes).
-
Add missing polygons. Occasionally you'll see a camp name in the map that doesn't actually have any polygon around it representing its borders. Click
ADDand draw a polygon for each — orDONEto skip. -
Name unmatched camps. Polygons that the auto-matcher couldn't tie to a roster entry are highlighted pink. Pick the right name from the dropdown (or type one in) and
ACCEPT. The dropdown lists camps from the roster that aren't already on the map.
All corrections are saved as you go to 2025/corrections.json, so you can
quit and resume without losing progress.
Once every step is done — spike + scale + lat/lon set, no unclosed polygons, add-polygons step finished, no unmatched camps — the status bar disappears and the two output files are written automatically. The browser console logs:
Wrote camps-geocoded.json (N/total centroids) and camps-geocoded-with-borders.json (M borders)
Re-emission happens on every load (and on every correction once you're past the all-done threshold), so the files always reflect the latest state.
The data directory is named for the event year. It's set by the YEAR
constant at the top of server.js — change that one line to work on a
different year, and rename/create the directory to match.
.
├── 2025/ (named by YEAR in server.js)
│ ├── input.pdf (you supply)
│ ├── camps.json (you supply)
│ ├── corrections.json (created/updated as you work)
│ ├── ocr-cache.json (cached label readings, outlined-text years)
│ ├── camps-geocoded.json (output)
│ └── camps-geocoded-with-borders.json (output)
├── public/
│ └── app.js (browser-side logic)
├── extract.js (pdf.js layer extraction)
├── layers.js (per-year PDF profiles + normalization)
├── ocr.js (reads camp names off outlined labels)
├── server.js (Node HTTP server + endpoints)
└── package.json