← Back to the map

Sunset Scout · under the hood

How the map knows where you can watch the sunset

The physics and the algorithm behind every pixel, including every shortcut that lets the whole thing be computed on one laptop.

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?

SEES THE SUNSET BLOCKED PACIFIC INLAND
Fig 1Same sunset, two answers. Two people stand a short walk apart. The one on top of the ridge is high enough that their line of sight passes over the coastal hill and reaches the ocean horizon, so they see the sunset. The one down in the valley behind that same hill has the hill in the way, so they don't. The map runs this comparison for every patch of land and every sunset direction.

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:

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:

  1. 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).
  2. Rounds it to the nearest whole degree, picking one of the 77 bits.
  3. Paints every pixel white or black by testing that one bit.
N 233° · WINTER SOLSTICES 270° · EQUINOXES, DUE WEST 309° · SUMMER SOLSTICES YOU THE 77 PRECOMPUTED DIRECTIONS COVER EVERY MAPPED LATITUDE'S YEAR date + latitude → azimuth → one bit e.g. Jul 19 at 37.8°N → 297° → bit 64 BIT 0 · 233° A NEARER RIDGE CUTS A GAP BIT 76 · 309° LAND FLAG
Fig 2Top: through a year, the sunset direction sweeps a fan; 77 precomputed one-degree directions cover the whole fan at every mapped latitude. Bottom: one pixel's actual answer is a row of 77 bits (white = visible), and a date simply selects a bit. Note the dark gap in the middle of the strip: a nearer ridge can interrupt the visible range, and the bitmask records that faithfully.

That's why scrubbing the date is instant. It's a bit-test per pixel in your browser, not a recomputation.

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

drop(x) = cx2 where c= 12k 2R

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.

EYES AT 2 M YOUR SEA HORIZON: 5.9 KM 36 M 93 M SHIP AT 20 KM: HULL ALREADY HIDDEN 100 M HILL AT 48 KM: ABOUT 3 M SHORT LEVEL LINE SIGHTLINE TO THE HORIZON 25 KM 40 KM
Fig 3The sea falls away by c·x² (vertical scale exaggerated, numbers real). From 2 m eyes the sea horizon is 5.9 km out. A ship 20 km out is already hull-down behind the curve, and a 100 m hill 48 km out falls about 3 m short of peeking over the horizon at all.

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

dhorizon = hc = 2Rh 12k 4.15km ×h (h in metres)

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

reach(x,h) = hc x

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.

hyou +2m c xyou max terrain west of you [ hic xi ]
206 M HILL, 3.2 KM INLAND 654 M RIDGE, 20 KM INLAND ITS HORIZON: 56 KM OUT RIDGE'S HORIZON: 86 KM OUT HILL'S REACH · 56 KM RIDGE'S REACH · 86 KM ⇒ IT SEES OVER THE HILL THE SEA, FLATTENED: y = −c·x² 0 25 KM 50 KM 75 KM SEAWARD ←
Fig 4The tangent test, drawn to scale with real numbers from a case on the Peninsula coast. In the flattened frame the sea is a downward parabola. The hill's own sightline grazes the sea 56 km out; the ridge's grazes it 86 km out, so the ridge's sightline passes far above the hill and the ridge sees the sunset, even though the hill completely hides the near-shore water from it. Only the largest reach matters.

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.

The sweep: how one direction is computed

For one azimuth, the engine computes a full yes/no raster (an azimuth slice) like this:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
OFFSHORE WEST EDGE: EVERY RAY SEEDS IN WATER COASTLINE CROSSING (x = 0) RIDGE SHADOWS THE LEE SUNSET AZIMUTH 280° ⇒ RAYS MARCH ON BEARING 100° WATER SAMPLE CROSSING VISIBLE SAMPLE BLOCKED SAMPLE
Fig 5One azimuth slice from above. Parallel rays seed at the box's offshore west edge, find the coastline, then march inland recording a verdict at every ~10 m sample. Behind the ridge a run of samples is shadowed, and farther back, higher ground can become visible again. Every output pixel copies the verdict of its nearest sample.

This is repeated for all 77 azimuths, and each pixel's 77 answers are packed into its bitmask.

The input data

WhatSourceNotes
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.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
OR FOUR ZOOM-14 CELLS ONE ZOOM-13 CELL ONE VISIBLE CHILD IS ENOUGH: THE PARENT STAYS WHITE THE RULE REPEATS UP EVERY ZOOM
Fig 6Downsampling is a byte-wise OR of each 2×2 block. That keeps "somewhere in here you can see the sunset" exact at every zoom, at the price of optimism when zoomed way out.

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.

What happens in your browser

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)

ChoiceValueEffect
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

SimplificationDirection 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

LimitationConsequence
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

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:

  1. Read the contract: tiles/tilejson.json declares 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).
  2. 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.
  3. Decode a pixel: for pixel (px, py), the bytes are at offset (py·512 + px)·10. Bit i (packed LSB-first: byte i >> 3, mask 1 << (i & 7)) is the verdict for azimuth 233 + i degrees; bit 77 is the land flag. Compare against what the map shows for a date with that sunset azimuth.
  4. 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.
one pixel = 10 bytes, at offset (py·512 + px)·10 b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 0 1 2 3 4 5 6 7 233° 240° ... BYTE 0 = BITS 0–7 (LSB FIRST) 72 73 74 75 76 77 0 0 305° 309° BIT 77: LAND FLAG BITS 78–79: UNUSED BYTE 9 = BITS 72–79
Fig 7The wire format of a single pixel, exactly as it sits in a downloaded tile. Anyone with a gzip decompressor can audit any pixel on the map against these ten bytes.

Appendix: every constant in one table

ParameterValueMeaning
refraction_coefficient_k0.13Standard terrestrial refraction coefficient; curvature scaled by (1−2k)
earth_radius_m6,371,000Mean Earth radius R
c = (1−2k)/(2R)≈ 5.81×10⁻⁸ m⁻¹Effective curvature; drop = c·x²; horizon = √(h/c)
observer_eye_height_m2Eye height above bare ground
coast_obstruction_skip_m0Passable-foreshore depth (disabled)
cell_per_degree10,800Output grid: 1/3 arc-second cells
sample_spacing_arcsec1/3″ (≈ 10.31 m)Ray spacing and march step (one knob)
meters_per_degree_lat111,320Flat-frame metric
azimuth_min/max/step233° / 309° / 1°77 swept sunset directions
bits per pixel77 + 1 flag = 78 → 10 bytesLSB-first; bit i = azimuth 233+i; bit 77 = land flag
strip_height_deg0.1°Latitude strip per freeze-and-sweep pass
west_edge_max_land_frac0.05Preflight: max land fraction on a box's offshore west edge
coast_march_max_km999Per-ray coast-search give-up (preflight-checked to cover each box)
Processing boxes11Coast coverage, 32.53–48.96°N; deliberate overlaps, deepest-interior merge
Tile pyramidz5–z14, 512 px, gzip raw84,924 tiles ≈ 1.5 GB; base zoom ≈ 3–4 m/px; OR-downsampled
Sunset definitionupper limb at −0.833°SunCalc's standard sunset; its azimuth picks the bit