Section 1 of 9
The question the map answers
For every patch of land along the US West Coast and for every day of the year, the map answers one question:
If you stood here at sunset, could you watch the sun touch the ocean horizon, or does terrain get in the way first?
The map answers that question one small square at a time, and every square ends up in one of three states:
White: an unobstructed
sightline exists from this spot to the ocean horizon in the direction the sun
sets on the selected date.
Black: terrain (a hill,
ridge, headland, or the curve of the Earth itself) blocks that sightline.
Transparent: ocean or bay
water. There is no land to stand on.
Three assumptions are baked in, and it's worth knowing them up front:
- Your eyes are 2 m above the bare ground. A single configured observer height is used everywhere.
- Only bare terrain blocks the view. The elevation data is a bare-earth model. Buildings, trees, and forests do not exist in it, neither as obstructions nor as things to stand on.
- "Seeing the sunset" means seeing the sea-level ocean horizon in the sunset's compass direction. If you can see the line where the water meets the sky where the sun goes down, you can watch the sun set into it.
Section 2 of 9
The key idea: dates become directions
The map lets you scrub through 365 days, but nothing is computed per date. The only thing about a date that matters here is the compass direction of the sunset (its azimuth, measured in degrees clockwise from north). The sun sets due west (270°) at the equinoxes, swings north of west in summer and south of west in winter, and the swing depends on your latitude. Across the mapped coast (San Diego at 32.5°N to the Canadian border at 49°N) sunset azimuths span roughly 233° (a little north of southwest) to 309° (just shy of northwest) over the year.
For each whole-degree azimuth from 233° to 309°, which makes 77 directions, the pipeline computes a yes/no visibility answer for every land pixel. Each pixel ends up carrying 77 bits, one per direction, plus one flag bit distinguishing land from water. The heavy computation work is done once, offline, per direction instead of per date. When you pick a date in the browser, the site:
- Computes that date's sunset azimuth for your latitude with the SunCalc astronomy library (the azimuth at the moment the sun's upper edge touches the horizon, using the standard −0.833° refraction convention).
- Rounds it to the nearest whole degree, picking one of the 77 bits.
- Paints every pixel white or black by testing that one bit.
That's why scrubbing the date is instant. It's a bit-test per pixel in your browser, not a recomputation.
Section 3 of 9
The physics: seeing over a curved, refracting Earth
Whether you can see the ocean horizon is not just about whether a hill is "in the way" on a flat map. Two physical effects run the show.
The Earth curves away
Relative to a level line of sight, the Earth's surface drops away with distance, slowly at first and then fast. The drop is very close to
with R = 6,371,000 m (mean Earth radius) and k
a refraction coefficient explained below. That drop is about 6 cm at
1 km, 1.5 m at 5 km, 5.8 m at 10 km, 145 m at
50 km, and 581 m at 100 km. This is why a spot 10 km of
dead-flat land away from the water cannot see the ocean at all: the ocean is
below the bulge of the Earth in between.
The atmosphere bends light back down
Air gets thinner with altitude, so light rays travelling near-horizontally
refract slightly downward, letting you see a little past the geometric
horizon. The standard way to model this (used in surveying) is to shrink the
effective curvature by a factor (1 − 2k) with
k = 0.13. That is equivalent to pretending the Earth is
about 35% bigger (effective radius ≈ 8,609 km) while light travels
straight. Both constants are configuration values, not code:
refraction_coefficient_k = 0.13 and
earth_radius_m = 6371000, giving
c ≈ 5.81×10⁻⁸ per metre.
Horizon distance and "Horizon Reach"
From height h the sea horizon lies at the classical distance
So eyes at 2 m see the sea out to ≈ 5.9 km. A 100 m hill commands ≈ 41.5 km. A 650 m ridge commands ≈ 106 km.
The engine's visibility test is built on one number per terrain point,
called its Horizon Reach. Measure the distance x
of each point inland from the coastline (along the sightline direction). Then
is how far out to sea, beyond the coastline, that point's own horizon extends. A tall coastal cliff has a large reach. A tall point far inland may have a small or negative reach: its height is spent just seeing over the land in front of it.
The visibility rule
The rule the engine applies is a single comparison: you see the ocean horizon exactly when your own reach, eyes included, is at least as large as the reach of every terrain point between you and the sea.
Why is this correct? Flattening the curved Earth into a diagram turns the sea surface into a downward-curving parabola, and "seeing the sunset" means drawing a straight line from your eyes that grazes that parabola: the line that touches the sea at the horizon. Two tangent lines to the same downward parabola never cross on the seaward side of their touch points, so the line that touches farther out to sea is higher everywhere inland. Of all the hills between you and the coast, the only one that can matter is therefore the one whose own horizon extends farthest seaward (the maximum reach), and you clear it exactly when your reach meets it. One running maximum per sightline decides everything, which is what makes computing this for billions of samples tractable.
Section 4 of 9
The sweep: how one direction is computed
For one azimuth, the engine computes a full yes/no raster (an azimuth slice) like this:
- Cast parallel rays across the region along the reverse of the sunset azimuth, from the ocean toward land: sunset azimuth 280° means rays march inland on bearing 100°. Rays are spaced 1/3 arc-second apart (≈ 10.31 m), the same as the sampling step along each ray, so the sample grid is square in the ray's own frame.
- Find the coast. Each ray starts at the processing
region's western edge, which is required to lie offshore in the open
Pacific, and steps inland along its bearing, testing "is this point water?"
against the ocean mask until it first hits land. That first land point is
the ray's coastline crossing: the zero point for all distances
x, and the zero baseline of the running maximum reach. - March inland from the crossing, one 10.31 m step at a time. At each step: look up the terrain height (bilinear interpolation of the elevation model), update the running maximum reach, and record a finished visible/blocked verdict for that sample. The sample is visible exactly when its own reach, with 2 m of eye height added, meets the running maximum.
- Transfer verdicts to pixels. Every output pixel belongs to exactly one ray (the nearest one), and inherits the verdict of the nearest sample along it. Verdicts are computed at sample points and copied, not recomputed per pixel, so a visibility boundary can be off by up to about one cell (~10 m).
This is repeated for all 77 azimuths, and each pixel's 77 answers are packed into its bitmask.
Section 5 of 9
The input data
| What | Source | Notes |
|---|---|---|
| Terrain elevation | USGS 3DEP 1/3 arc-second DEM (~10 m grid) | Bare-earth model in 1°×1° GeoTIFF tiles (84 files, ~26 GB on disk). Sampled with bilinear interpolation. Pixels the USGS marks "no data" are water surface and become 0 m. Where the same tile exists in multiple acquisition dates, the newest wins. |
| Ocean / land boundary | OpenStreetMap water polygons (osmdata.openstreetmap.de ocean/coastline set) | Rasterized to the same 1/3 arc-second grid; looked up by nearest cell. Deliberately OSM rather than a separate coastline product, so the computed coastline matches the basemap you see. Ocean-connected water only: inland lakes are treated as land and get a normal verdict. |
| Sun position | SunCalc (in the browser) | Standard low-precision solar formulas; sunset = sun's upper limb at the horizon (altitude −0.833°, the conventional refraction and disc allowance). Sunset azimuth depends on date and latitude; longitude has no practical effect on it. |
| Basemap | OpenStreetMap via Protomaps | Presentation only; it plays no role in the visibility math. |
Section 6 of 9
From verdicts to map tiles
The engine writes each box as a GeoTIFF whose pixels are the 10-byte bitmasks (77 direction bits plus the land flag), with the azimuth window, step, bit count, and a format version stamped in the file's metadata. Downstream:
- Merge. The 11 boxes are pasted onto one common 1/3 arc-second lattice. Boxes deliberately overlap; in an overlap each pixel takes its bytes verbatim from whichever box it sits deepest inside (distance to that box's north, south, and east edges; the offshore west edge doesn't count), which splits each overlap cleanly down its midline. Bit patterns are never blended.
- Clip to the US. Pixels outside a US land polygon are zeroed (transparent), so a coast-anchored box reaching into Mexico or Canada doesn't publish verdicts there.
- Reproject and slice. The merged raster is reprojected to Web Mercator by nearest-neighbour at zoom 14 (≈ 3.1–4.0 m per pixel across the coverage, finer than the 10 m data, so cells are simply replicated rather than resampled away) and cut into 512×512-pixel tiles: gzipped raw bytes, no image format. All-zero tiles are skipped entirely.
- Build the zoom pyramid (zooms 13 down to 5) by OR-ing each 2×2 block of child pixels byte-wise. A zoomed-out pixel shows visible when any of the finer pixels under it is visible at that azimuth. This is exact for "is there a visible spot in here?" but it reads optimistic when zoomed far out. Zoom in for the truth at your feet.
The published tileset is 84,924 tiles totalling ≈ 1.5 GB,
covering 32.53°N–48.96°N. A tilejson.json sidecar
carries the wire-format contract (azimuth window 233–309, step 1, 77
bits, 10 bytes per pixel, format version 1). The frontend reads all parameters
from it and refuses a format version it doesn't understand, so the encoding
can never silently drift.
Section 7 of 9
What happens in your browser
- Colouring. For the selected date, the sunset azimuth is computed at the latitude of the map centre, rounded to its bit index, and every visible tile is repainted from raw bytes: flag bit clear → transparent; date's bit set → white; otherwise black. The overlay is drawn at 40% opacity with nearest-neighbour resampling (no smoothing: the crisp pixel edges are honest cell boundaries). Panning to a different latitude recomputes the bit index.
- Tapping a spot reads the actual 10-byte bitmask of that exact pixel from the sharpest tile loaded, and evaluates it at the spot's own latitude. So does the little year-strip: it is 365 bit-tests of the same pixel, one sunset azimuth per day.
- US land outside the computed boxes (far inland, plus coastal areas not yet covered) is painted black by a mask polygon, not by computation. For land well inland this is a safe assumption: seeing a sea-level horizon from 150 km inland would take about 1,300 m of elevation and a clear diagonal across every coastal range. Near the box edges, though, it is an assumption rather than a result. Non-US land and open ocean outside the tileset stay transparent.
Section 8 of 9
Honest accuracy: what was traded for a laptop
Every dataset like this chooses a point on the accuracy/compute curve. This one was deliberately budgeted so the entire coast can be recomputed on a single laptop. The full 11-box sweep evaluates on the order of 10¹¹ terrain samples (77 azimuths times roughly 3×10⁹ samples each) in roughly a day of machine time, and re-runs skip boxes whose inputs haven't changed. Here is where approximation enters, and which way each one leans:
Resolution choices (the compute budget)
| Choice | Value | Effect |
|---|---|---|
| Azimuth step | 1° (77 directions) | Your date's azimuth is rounded to the nearest whole degree to pick a bit, so up to ±0.5° of quantization. For scale, the sun's disc is itself ~0.53° wide. Verdicts can flip on dates whose azimuth sits near the edge of a visibility gap. Cost is linear in the number of directions. |
| Sample spacing | 1/3 arc-second ≈ 10.31 m | Matches the DEM's native grid; sampling finer than the data mostly buys smoother boundaries, not more truth. Terrain features narrower than ~10 m (a notch in a ridge, a narrow gap between rocks) can be missed or invented. Halving the spacing quadruples the work (rays × steps). |
| Verdict transfer | nearest-neighbour | Pixels inherit the nearest sample's verdict rather than being re-evaluated at their own centre; visibility boundaries can wobble by about one cell. |
| Zoom pyramid | 2×2 OR | Zoomed out, a coarse pixel is white if any 10 m cell within it is white: a deliberate "there's a visible spot in here" semantic that over-lights large pixels. The base zoom (fully zoomed in) has no such bias. |
Model simplifications
| Simplification | Direction of error |
|---|---|
| Bare-earth terrain. No buildings, no trees. A city block or a redwood stand in your sightline doesn't block it here; equally, you can't climb a building to see over a ridge. | Optimistic in cities and forests; the map answers "does the landscape allow it". |
| Fixed refraction, k = 0.13. Real near-horizon refraction swings with the temperature profile, especially over water at sunset (mirages, looming). Marginal sightlines, the ones grazing a ridge by a few metres over tens of kilometres, genuinely vary from day to day in reality. | Either way on borderline cases; standard value on average. |
| Fixed 2 m eye height on the ground surface. | Slightly pessimistic for overlooks with towers or decks, optimistic if you're sitting down. |
| Spherical Earth, parabolic drop. Mean radius 6,371 km (no ellipsoid), and the drop c·x² is the parabolic approximation of the sphere; the difference is under 10 cm even at 100 km. Sea level is the DEM's zero datum; tides are not modelled. | Negligible against the ~10 m data. |
| Weather. Fog, marine layer, clouds, haze: not modelled, obviously. | The map is a geometry answer, not a forecast. |
Data limitations
| Limitation | Consequence |
|---|---|
| DEM vertical error (varies by survey source; metre-scale) | A sightline that clears a ridge by a metre or two at long range is within the data's noise, so treat near-boundary pixels as a maybe. |
| OSM coastline vs USGS terrain disagree structurally in places (up to ~100 m on steep coasts like Big Sur) | The white/black field and the drawn coastline can be offset near
cliffs. The ray's coastline zero-point carries the same uncertainty,
which shifts x for everything on that ray equally, so
verdicts are barely affected; the visible seam is cosmetic. |
| Lakes are land | A lake surface gets an opaque verdict like the ground around it, rather than rendering as water. |
| Box seams | Where two boxes overlap, each pixel's verdict comes wholesale from one box. The two boxes compute the same physics on differently-anchored sample grids, so pixels within ~1 cell of a visibility boundary may resolve differently on each side of a seam. |
Frontend approximations
- The whole-map tint uses one azimuth, computed at the map centre's latitude. Across a tall viewport the true azimuth varies slightly with latitude (well under the 1° bin at city scale; the tap-a-spot verdict always uses the spot's exact latitude).
- SunCalc's azimuth is good to a few tenths of a degree, below the 1° quantization it feeds.
Section 9 of 9
Check it yourself
The rendered map involves no hidden server logic. Everything the browser shows is derivable from public, static files you can fetch and decode:
- Read the contract:
tiles/tilejson.jsondeclares the azimuth window (233–309°), step (1°), bit count (77), bytes per pixel (10), tile size (512), zoom range (5–14), and format version (1). - Fetch any tile:
tiles/{z}/{x}/{y}.bin(standard XYZ Web Mercator scheme). Each is a gzip stream of 512 × 512 × 10 bytes, row-major from the tile's northwest corner, 10 bytes per pixel, no header. - Decode a pixel: for pixel (px, py), the bytes are at
offset
(py·512 + px)·10. Biti(packed LSB-first: bytei >> 3, mask1 << (i & 7)) is the verdict for azimuth233 + idegrees; bit 77 is the land flag. Compare against what the map shows for a date with that sunset azimuth. - Spot-check the physics against reality: stand somewhere the map calls white at today's azimuth and watch. The authors' own regression suite pins known cases (Ocean Beach in San Francisco must never be black, for example), the C++ packer and the JavaScript reader are held to the same bit layout by a shared test-vector file, and a trace tool prints every ~10 m sample of the exact ray any chosen pixel inherited its verdict from.
Reference
Appendix: every constant in one table
| Parameter | Value | Meaning |
|---|---|---|
refraction_coefficient_k | 0.13 | Standard terrestrial refraction coefficient; curvature scaled by (1−2k) |
earth_radius_m | 6,371,000 | Mean Earth radius R |
| c = (1−2k)/(2R) | ≈ 5.81×10⁻⁸ m⁻¹ | Effective curvature; drop = c·x²; horizon = √(h/c) |
observer_eye_height_m | 2 | Eye height above bare ground |
coast_obstruction_skip_m | 0 | Passable-foreshore depth (disabled) |
cell_per_degree | 10,800 | Output grid: 1/3 arc-second cells |
sample_spacing_arcsec | 1/3″ (≈ 10.31 m) | Ray spacing and march step (one knob) |
meters_per_degree_lat | 111,320 | Flat-frame metric |
azimuth_min/max/step | 233° / 309° / 1° | 77 swept sunset directions |
| bits per pixel | 77 + 1 flag = 78 → 10 bytes | LSB-first; bit i = azimuth 233+i; bit 77 = land flag |
strip_height_deg | 0.1° | Latitude strip per freeze-and-sweep pass |
west_edge_max_land_frac | 0.05 | Preflight: max land fraction on a box's offshore west edge |
coast_march_max_km | 999 | Per-ray coast-search give-up (preflight-checked to cover each box) |
| Processing boxes | 11 | Coast coverage, 32.53–48.96°N; deliberate overlaps, deepest-interior merge |
| Tile pyramid | z5–z14, 512 px, gzip raw | 84,924 tiles ≈ 1.5 GB; base zoom ≈ 3–4 m/px; OR-downsampled |
| Sunset definition | upper limb at −0.833° | SunCalc's standard sunset; its azimuth picks the bit |