# Vendored: protomaps-leaflet

- **Package:** `protomaps-leaflet`
- **Version:** 5.1.0
- **Licence:** BSD-3-Clause (Copyright 2021-2024 Protomaps LLC) — the full text is in
  `licences/protomaps-leaflet-bsd-3-clause.txt`.
- **Source:** `https://registry.npmjs.org/protomaps-leaflet/-/protomaps-leaflet-5.1.0.tgz`
  (267,768 bytes, sha256 `0b45b08d0e6f7d39816415dbe6821d0556e7997ec716ed0123305cc1fe5a3fd8`)
- **Retrieved:** 2026-09-08
- **Map data:** this library ships no data. The archive it reads on this site,
  `https://startrailstacker.com/basemap/world-z13.pmtiles`, is derived from **OpenStreetMap**
  and Natural Earth and is published under the **Open Database Licence (ODbL)**. Commercial
  use is permitted and **attribution is required**: `© OpenStreetMap contributors · Protomaps`,
  which the map's own attribution strip carries, with both halves as links. See
  `docs/startrail-basemap-provenance.md` for the archive's build command and digest, and
  `/light-pollution-map/how-it-works/` for where a visitor reads the credit and what the ODbL
  does and does not reach.

## Why it is vendored, and why this library rather than a GL one

The light pollution map drew its base map from Carto's anonymous raster tiles. Those began
coming back **watermarked "API KEY REQUIRED"** — HTTP 200, `image/png`, permitted by the CSP,
no console error — Carto's free tier is written for non-commercial use while this site is
commercial, and their raster line is being retired. The replacement is our own 35.57 GB
PMTiles archive of the whole world, z0–z13, served from this origin.

**This is the library that can read it under our CSP.** It is a Leaflet plugin — an
`L.GridLayer` subclass that fetches PMTiles over HTTP range requests and paints vector tiles
into one `<canvas>` per tile. Over both its UMD and ESM builds:

```
$ grep -cE 'new Worker|createObjectURL|innerHTML|document\.write|eval\(' dist/protomaps-leaflet.js dist/esm/index.js
dist/protomaps-leaflet.js:0
dist/esm/index.js:0
```

Every WebGL basemap engine — MapLibre GL, Mapbox GL, Google's loader — reaches a
`createScriptURL` or a worker sink and throws under `require-trusted-types-for 'script'`,
which the fleet edge sends. So this choice is what lets `lpmap.js`'s default Trusted Types
policy stay as narrow as it is: `createHTML` still admits exactly one string
(Leaflet's `"<svg/>"`), and `createScript`/`createScriptURL` still refuse everything.

Production `script-src` is `'self'`, so a CDN is not an option and the file is served from
this origin, exactly as `vendor/leaflet` and `vendor/tz-lookup` are.

## The first local modification: the export line

`protomaps-leaflet.js` is the upstream UMD build, **byte-identical for its first 128,171
bytes** (sha256 `26af014f7b1af308ec120b791cff76657bb9c3383633b52033e6edf9e5e4cdb5`), with
exactly one line appended:

```js
export default protomapsL;
```

The UMD opens with `var protomapsL = (() => { ... })()`. Under a classic script tag that
`var` becomes a global; under a **module** it does not, and `window.protomapsL` is never
defined. This page has no bundler and no classic script tag — `lpmap.js` must install its
Trusted Types policy before anything else evaluates, so every dependency on this route is an
ES module — so the value is exported instead. This is the same treatment, and for the same
reason, that `vendor/tz-lookup/tz.js` gets.

BSD-3-Clause permits redistribution "with or without modification" provided the notice
travels with it, which it does.

## The second local modification: the parser is told which layers to decode

`parseTile` walks a tile's own layer table and turns **every feature of every layer** into JS
geometry before a single paint or label rule is consulted. Nothing upstream can stop it: a
rule decides what is *drawn*, never what is decoded, and there is no option, callback or
subclass hook anywhere between `leafletLayer`'s options and `parseTile` — the chain is
`sourcesToViews → sourceToViews → new TileCache(source, tileSize) → source.get(coords,
tileSize) → parseTile(buffer, tileSize)`, and none of those five carries a layer filter.

On this site's archive that is where the map's time goes. Counted on a fixed keyboard tour of
zooms 5, 7, 9 and 11, 68 tiles decoded: **81,890 features and 4,627,628 vertices**, of which
`landuse` alone is 96% of the vertices at zoom 9 and 90% at zoom 11. Measured on GPU-backed
headed Chrome, one 30-second pan-and-zoom storm at 1728x1080 DPR 2, two runs each:

```
shipped                                                     84.00 s   84.75 s CPU
landuse's paint rules dropped, decode untouched              84.18 s   82.38 s
landuse dropped from the paint rules AND from the decode     36.97 s   37.24 s
```

Dropping the rules alone buys nothing, because the geometry is still built and then thrown
away. Dropping the decode as well halves the map. That is the entire reason this patch exists.

So `parseTile` takes an optional third argument — a `Set` of layer names to keep — and it is
threaded through `PmtilesSource.get`, `ZxySource.get`, `TileCache` (as a public `keepLayers`
property) and `sourceToViews` (as a `keepLayers` option). Eight replacements in all; every one
of them is listed as an `[upstream, ours]` pair in `test/lp-basemap.test.js`, which **reverses
the patch and requires the result to be the published UMD build's 128,171 bytes and digest**.
So "this file is upstream's bytes plus documented edits" stays a checked claim rather than a
sentence here, and a vendor refresh that quietly absorbs one of the edits still fails.

`src/engine/lp-basemap-rules.js` builds the set from the paint and label rules the two base-map
layers actually carry, so deleting a style rule stops the decode instead of leaving it paid
for, and a hardcoded list cannot rot. Passing no set leaves the upstream behaviour exactly as
it was.

**What it saves today is almost nothing, and that is not an accident.** Every layer this
archive carries except `pois` is named by some rule in the shipped style, so the set skips 203
features of 81,890 on that tour. The saving above needs the landuse rules to go, which changes
the picture where the light-pollution overlay is off, and that is a separate decision.

`test/lp-basemap.test.js` also asserts the bundle still contains none of the sinks listed
above.

The trailing `//# sourceMappingURL=protomaps-leaflet.js.map` is left in place and the map
file is not shipped, exactly as `vendor/leaflet/leaflet.js` does. Browsers fetch a source map
only when devtools are open.

## Files kept

- `protomaps-leaflet.js` — the UMD build (128,171 bytes upstream; ~37.7 KB gzipped over the
  wire), plus the one export line and the eight-replacement decode patch above. This is the
  only file the site loads.
- `licences/protomaps-leaflet-bsd-3-clause.txt` — the licence as published in the tarball.
  Named with an extension because the fleet edge rewrites any extensionless path to its
  trailing-slash canonical, which turns a licence object into `301 → 404`; `build.mjs`
  refuses an extensionless object for that reason.

`dist/esm/`, `dist/cjs/`, `src/`, `README.md` and `package.json` are dropped. The ESM build
carries bare specifiers (`@mapbox/point-geometry`, `@protomaps/basemaps`, `color2k`, `pbf`,
`pmtiles`, `rbush`, …) and would need a bundler; the UMD build has all ten dependencies
inlined, which is the whole difference between its 128 KB and the ESM build's 47 KB.

## The API this site actually uses

Read from `src/frontends/leaflet.ts` in the tarball rather than from any README:

```js
protomapsL.leafletLayer({
  url: "/basemap/world-z13.pmtiles",  // a .pmtiles path selects PmtilesSource
  flavor: "dark",                     // namedFlavor(): light | dark | white | grayscale | black
  lang: "en",                         // which name: field the label rules read
  maxDataZoom: 13,                    // DEFAULTS TO 15, which our archive does not have
  attribution: "",                    // this page draws its own; see lpmap.js
})
```

Three things about it are not obvious and each one costs a wrong picture:

- **`maxDataZoom` defaults to 15**, the upstream planet build's ceiling, not ours. Left at
  the default, every display zoom above 14 asks for a data tile the archive does not contain
  and paints nothing. Set to 13 the `View` overzooms z13 geometry instead, which is why the
  map's `maxZoom` could go to 15 at all.
- **It reads one zoom coarser than it draws.** `levelDiff` is 1, so display zoom 8 fetches
  data tile z7 and stretches it over 512 CSS px. Anything that builds a fixture, a cache
  warmer or a coverage estimate from the display zoom will be off by one.
- **`L` is a free variable resolved when `leafletLayer()` is called**, not when the module is
  imported — but `lpmap.js` imports it after Leaflet anyway rather than leave that to chance.

`namedFlavor` itself is *not* exported from the UMD; `paintRules` and `labelRules` are, so a
tuned palette would be built by passing those two rather than `flavor`. Measured on this
build, the stock `dark` flavor shifts the composited picture under the light-pollution
overlay by a mean of 4.5–5.1 per channel and never more than 23, so no tuning was needed.

## No npm dependency

`sites/startrail/package.json` declares no runtime dependency. The file is committed the way
Leaflet, `tz.js` and `rawlab.wasm` are, so CI installs nothing to build or test this route.

## BSD 3-Clause License

The text exactly as published in the tarball's `LICENSE`:

```
Copyright 2021-2024 Protomaps LLC

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
