neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
Log in to mark this repository.
Point the basemaps at a configurable tile server
a640fb2 · Gabriel Morell · 2026-08-28 21:11
Message
{commit_body(@commit)}
Files changed
added
CUSTOM_MAPS.md
+147
−0
| 1 | +# Custom map tiles | |
| 2 | + | |
| 3 | +Every Leaflet map in room_sanctum draws its basemap from one tile server, | |
| 4 | +chosen by configuration. Unset, it is CARTO's keyless greyscale basemaps — | |
| 5 | +which is what every map used before this was configurable. Set, it is whatever | |
| 6 | +server you point it at. | |
| 7 | + | |
| 8 | +The moving parts: | |
| 9 | + | |
| 10 | +| Where | What | | |
| 11 | +| --- | --- | | |
| 12 | +| `config/runtime.exs` | reads `TILE_*` from the environment | | |
| 13 | +| `apps/room_sanctum/lib/room_sanctum/basemap.ex` | drops the blank keys, names the meta tags | | |
| 14 | +| `<.basemap_meta />` in `CoreComponents`, in both root layouts | writes `<meta name="basemap-*">` into `<head>` | | |
| 15 | +| `apps/room_sanctum/assets/js/leaflet/basemap.js` | resolves the source once per page, falls back to CARTO | | |
| 16 | + | |
| 17 | +Meta tags rather than an attribute per map because maps are created from half a | |
| 18 | +dozen places — including the `<leaflet-map>` web component, inside a shadow | |
| 19 | +root — and every one of them draws from the same server. | |
| 20 | + | |
| 21 | +## Configuration | |
| 22 | + | |
| 23 | +Read at runtime, so a release is repointed by restarting it rather than | |
| 24 | +rebuilding it. | |
| 25 | + | |
| 26 | +| Variable | Meaning | | |
| 27 | +| --- | --- | | |
| 28 | +| `TILE_URL` | base tiles. The only one required to switch servers | | |
| 29 | +| `TILE_URL_DARK` | drawn instead under a dark theme (defaults to `TILE_URL`) | | |
| 30 | +| `TILE_LABELS_URL` | label-only overlay, drawn *above* the theme wash | | |
| 31 | +| `TILE_LABELS_URL_DARK` | dark counterpart (defaults to `TILE_LABELS_URL`) | | |
| 32 | +| `TILE_ATTRIBUTION` | attribution HTML for the corner of the map | | |
| 33 | +| `TILE_SUBDOMAINS` | e.g. `abcd`, only if the URL contains `{s}` | | |
| 34 | +| `TILE_MAX_ZOOM` | deepest zoom the server has tiles for | | |
| 35 | + | |
| 36 | +Set none of them and nothing is emitted into `<head>` at all, which the | |
| 37 | +frontend reads as "use your default" rather than as "draw nothing". | |
| 38 | + | |
| 39 | +Light and dark are chosen per theme by `isDarkTheme()`, which reads daisyUI's | |
| 40 | +`--b1` off the map container — so the maps follow the theme picker, not the OS. | |
| 41 | +A caller can pin one with `variant: 'light' | 'dark'`; the web component takes | |
| 42 | +that from its `basemap` attribute. | |
| 43 | + | |
| 44 | +`TILE_MAX_ZOOM` wins over the `maxZoom` a caller passes `addBasemap`. Callers | |
| 45 | +pass the zoom the view wants, which a server without tiles that deep can only | |
| 46 | +answer with 404s. | |
| 47 | + | |
| 48 | +Retina is opt-in: put `{r}` in `TILE_URL` only for a server that really serves | |
| 49 | +`@2x` tiles. Leaflet's `detectRetina` has two modes and only one is safe to | |
| 50 | +assume — with `{r}` it asks for the @2x tile, without it it requests the *next | |
| 51 | +zoom down at half size*, which is four times the tiles and nothing at all at | |
| 52 | +the server's deepest zoom. | |
| 53 | + | |
| 54 | +## tiles.neiam.org | |
| 55 | + | |
| 56 | +Our own renderd server, and what the deployment points at — see the `TILE_*` | |
| 57 | +block in `app.yml`. Its landing page is a live layer switcher that prints the | |
| 58 | +template of whichever layer is showing, so it is its own reference. | |
| 59 | + | |
| 60 | +``` | |
| 61 | +TILE_URL=https://tiles.neiam.org/grayscale/{z}/{x}/{y}.png | |
| 62 | +TILE_URL_DARK=https://tiles.neiam.org/dark/{z}/{x}/{y}.png | |
| 63 | +TILE_LABELS_URL=https://tiles.neiam.org/grayscale_only_labels/{z}/{x}/{y}.png | |
| 64 | +TILE_LABELS_URL_DARK=https://tiles.neiam.org/dark_only_labels/{z}/{x}/{y}.png | |
| 65 | +TILE_MAX_ZOOM=20 | |
| 66 | +``` | |
| 67 | + | |
| 68 | +| Layer | Style | | |
| 69 | +| --- | --- | | |
| 70 | +| `/grayscale/{z}/{x}/{y}.png` | Positron-like | | |
| 71 | +| `/dark/{z}/{x}/{y}.png` | Dark Matter-like | | |
| 72 | +| `/tile/{z}/{x}/{y}.png` | standard OSM colour | | |
| 73 | +| `/night/{z}/{x}/{y}.png` | warm dark | | |
| 74 | +| `/blueprint/{z}/{x}/{y}.png` | blueprint | | |
| 75 | +| `/grayscale_only_labels/…`, `/dark_only_labels/…` | transparent label overlays | | |
| 76 | + | |
| 77 | +256px tiles, no `@2x`, no `{s}`, north-america-only import. The base styles are | |
| 78 | +opaque palette PNGs; the two `_only_labels` layers carry a `tRNS` chunk and | |
| 79 | +render place names over nothing, which is what makes them usable as an overlay. | |
| 80 | +Only the five base styles are listed on the landing page — the label layers are | |
| 81 | +served but undocumented there, so this file is their reference. | |
| 82 | + | |
| 83 | +The built-in default in `basemap.js` is still CARTO; the deployment overrides | |
| 84 | +it. Two reasons to leave the fallback where it is: | |
| 85 | + | |
| 86 | +- **North america only.** Anywhere else renders as empty tiles, not an error. | |
| 87 | + The landing page's four cities are all inside the import. | |
| 88 | +- **Cold tiles do not render reliably.** Measured 2026-08-28: an uncached tile | |
| 89 | + hangs ~10s and then returns 404, 502 or 503 depending on how the request | |
| 90 | + fails, and the same tile can keep failing across retries for minutes before | |
| 91 | + it starts serving — NYC at z10–z12 did this on every layer while z4 and | |
| 92 | + z19–z20 at the same point served immediately. Leaflet does not re-request a | |
| 93 | + 404, so a failed tile is a permanent hole in that page's map rather than a | |
| 94 | + slow one. This is the thing to fix on the server side; it is more visible to | |
| 95 | + a user than anything else on this page. | |
| 96 | + | |
| 97 | +## The label-only layer | |
| 98 | + | |
| 99 | +CARTO publishes each basemap three ways: `light_all`, `light_nolabels` and | |
| 100 | +`light_only_labels`. The default draws `_nolabels` under the theme tint and | |
| 101 | +`_only_labels` above it, in a Leaflet pane at z-index 350 — above the wash | |
| 102 | +(250), below the overlay (400) and marker (600) panes. | |
| 103 | + | |
| 104 | +That split is not decoration. The tint is a `mix-blend-mode: color` wash over | |
| 105 | +the whole basemap, and type tinted along with everything behind it loses most | |
| 106 | +of its contrast against the roads. Drawing the labels above the wash is what | |
| 107 | +keeps place names readable on a tinted map. The cost is a second tile request | |
| 108 | +per tile, which `labels: false` opts out of. | |
| 109 | + | |
| 110 | +tiles.neiam.org now serves `grayscale_only_labels` and `dark_only_labels`, so | |
| 111 | +the deployment gets the same split. The frontend turns the label pane on as | |
| 112 | +soon as **both** a light and a dark label URL are configured — both are | |
| 113 | +required, because the theme can flip at any moment and a pane that existed | |
| 114 | +under one theme and not the other would have labels appearing and disappearing | |
| 115 | +with it. | |
| 116 | + | |
| 117 | +### Still open: is the base style label-free? | |
| 118 | + | |
| 119 | +CARTO's `_nolabels` exists so the base and the overlay do not both draw type. | |
| 120 | +Our server has no `_nolabels` variant that we could find, which leaves the | |
| 121 | +question of whether `/grayscale` and `/dark` bake their labels in. If they do, | |
| 122 | +every place name is drawn twice — once under the wash, once above it. Because | |
| 123 | +both come from the same style at the same placement they land on top of each | |
| 124 | +other, so it reads as heavier type rather than as doubled type, but it is not | |
| 125 | +what the design intends and it costs a second render of every label. | |
| 126 | + | |
| 127 | +Worth checking against a rendered city tile once the server is serving cold | |
| 128 | +tiles reliably (see above — this could not be confirmed while writing this, as | |
| 129 | +`/grayscale` was returning 503 for every zoom that was not already cached). | |
| 130 | +Two ways out if it turns out labels are baked in: | |
| 131 | + | |
| 132 | +- publish `grayscale_nolabels` / `dark_nolabels` and point `TILE_URL` at those, | |
| 133 | + which is the CARTO shape and needs no code change; or | |
| 134 | +- drop `TILE_LABELS_URL*` and accept the tint over the labels, which is what | |
| 135 | + the single-layer path already does. | |
| 136 | + | |
| 137 | +## Remaining gaps | |
| 138 | + | |
| 139 | +- **Extend the import past north america**, or accept that the instance is | |
| 140 | + regional. | |
| 141 | +- **`@2x` tiles**, then add `{r}` to `TILE_URL` — one line of config, and the | |
| 142 | + retina path in `basemap.js` already handles it. | |
| 143 | + | |
| 144 | +Once cold-tile rendering is dependable, flipping the built-in default in | |
| 145 | +`basemap.js` from CARTO to our server is a small change: `CARTO` in that file | |
| 146 | +is just the fallback `tileSource()` returns when no `basemap-url` meta tag is | |
| 147 | +present. |
modified
README.md
+3
−0
| 38 | 38 | Postgres is expected on **port 54321** (not 5432) as `postgres`/`postgres`, database |
| 39 | 39 | `room_sanctum_dev` — see `config/dev.exs`. `docker-compose.yml` brings one up. |
| 40 | 40 | |
| 41 | +Maps draw from CARTO unless a tile server is configured — see | |
| 42 | +[CUSTOM_MAPS.md](CUSTOM_MAPS.md). | |
| 43 | + | |
| 41 | 44 | #### First-time setup |
| 42 | 45 | |
| 43 | 46 | ```sh |
modified
app.yml
+24
−0
| 109 | 109 | - name: PHX_HOST |
| 110 | 110 | value: ac.gmp.io |
| 111 | 111 | |
| 112 | + # Basemap tiles, from our own renderd server rather than CARTO. | |
| 113 | + # See CUSTOM_MAPS.md. The label-only overlays are what let the | |
| 114 | + # theme tint wash the basemap without washing the place names. | |
| 115 | + - name: TILE_URL | |
| 116 | + value: https://tiles.neiam.org/grayscale/{z}/{x}/{y}.png | |
| 117 | + | |
| 118 | + - name: TILE_URL_DARK | |
| 119 | + value: https://tiles.neiam.org/dark/{z}/{x}/{y}.png | |
| 120 | + | |
| 121 | + - name: TILE_LABELS_URL | |
| 122 | + value: https://tiles.neiam.org/grayscale_only_labels/{z}/{x}/{y}.png | |
| 123 | + | |
| 124 | + - name: TILE_LABELS_URL_DARK | |
| 125 | + value: https://tiles.neiam.org/dark_only_labels/{z}/{x}/{y}.png | |
| 126 | + | |
| 127 | + - name: TILE_MAX_ZOOM | |
| 128 | + value: '20' | |
| 129 | + | |
| 130 | + - name: TILE_ATTRIBUTION | |
| 131 | + value: >- | |
| 132 | + © <a | |
| 133 | + href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | |
| 134 | + contributors | |
| 135 | + | |
| 112 | 136 | name: apollos-crib |
| 113 | 137 | ports: |
| 114 | 138 | - containerPort: 4000 |
modified
apps/room_sanctum/assets/js/leaflet/basemap.js
+90
−25
| 1 | 1 | import L from 'leaflet' |
| 2 | 2 | |
| 3 | −// CARTO's greyscale basemaps. Keyless, and monochrome by design rather than | |
| 4 | −// desaturated after the fact -- a CSS grayscale() filter over standard OSM | |
| 5 | −// tiles flattens the colour-coded road hierarchy into near-identical greys, | |
| 6 | −// whereas these are drawn for it, so labels and roads keep their separation | |
| 7 | −// once a tint is laid over them. | |
| 3 | +// CARTO's greyscale basemaps, the default when no server is configured. | |
| 4 | +// Keyless, and monochrome by design rather than desaturated after the fact -- | |
| 5 | +// a CSS grayscale() filter over standard OSM tiles flattens the colour-coded | |
| 6 | +// road hierarchy into near-identical greys, whereas these are drawn for it, so | |
| 7 | +// labels and roads keep their separation once a tint is laid over them. | |
| 8 | 8 | // CARTO publishes each basemap split into a label-free layer and a label-only |
| 9 | 9 | // layer as well as the combined `_all`. Drawing the labels as a separate layer |
| 10 | 10 | // above the wash is what keeps type readable: tinted along with everything |
| 11 | 11 | // else, place names lose most of their contrast against the roads behind them. |
| 12 | 12 | // The cost is a second tile request per tile, which `labels: false` opts out |
| 13 | 13 | // of by falling back to the combined layer. |
| 14 | −const TILE_URL = 'https://{s}.basemaps.cartocdn.com/' | |
| 14 | +const CARTO_URL = 'https://{s}.basemaps.cartocdn.com/' | |
| 15 | +const cartoUrl = (style) => CARTO_URL + style + '/{z}/{x}/{y}{r}.png' | |
| 15 | 16 | |
| 16 | −const STYLES = { | |
| 17 | − light: { all: 'light_all', base: 'light_nolabels', labels: 'light_only_labels' }, | |
| 18 | − dark: { all: 'dark_all', base: 'dark_nolabels', labels: 'dark_only_labels' } | |
| 17 | +const CARTO = { | |
| 18 | + light: { | |
| 19 | + all: cartoUrl('light_all'), | |
| 20 | + base: cartoUrl('light_nolabels'), | |
| 21 | + labels: cartoUrl('light_only_labels') | |
| 22 | + }, | |
| 23 | + dark: { | |
| 24 | + all: cartoUrl('dark_all'), | |
| 25 | + base: cartoUrl('dark_nolabels'), | |
| 26 | + labels: cartoUrl('dark_only_labels') | |
| 27 | + }, | |
| 28 | + subdomains: 'abcd', | |
| 29 | + maxZoom: null, | |
| 30 | + retina: true, | |
| 31 | + attribution: | |
| 32 | + '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors ' + | |
| 33 | + '© <a href="https://carto.com/attributions">CARTO</a>' | |
| 19 | 34 | } |
| 20 | 35 | |
| 21 | −const tileUrl = (style) => TILE_URL + style + '/{z}/{x}/{y}{r}.png' | |
| 36 | +// The server the page was served with, written into <head> as | |
| 37 | +// <meta name="basemap-*"> by RoomSanctum.Basemap. Meta tags rather than an | |
| 38 | +// attribute on each map: the maps are created from half a dozen places, | |
| 39 | +// including a web component inside a shadow root, and all of them draw from | |
| 40 | +// the same server. | |
| 41 | +function meta(name) { | |
| 42 | + const el = document.querySelector('meta[name="basemap-' + name + '"]') | |
| 43 | + const value = el && el.content.trim() | |
| 44 | + return value || null | |
| 45 | +} | |
| 46 | + | |
| 47 | +// Resolved once per page. Only `basemap-url` is required to switch servers: | |
| 48 | +// a server with a single style is drawn under both themes, and one with no | |
| 49 | +// separate label layer simply takes the wash over its labels. | |
| 50 | +let source = null | |
| 51 | + | |
| 52 | +function tileSource() { | |
| 53 | + if (source) return source | |
| 54 | + | |
| 55 | + const url = meta('url') | |
| 56 | + if (!url) { | |
| 57 | + source = CARTO | |
| 58 | + return source | |
| 59 | + } | |
| 60 | + | |
| 61 | + const dark = meta('dark-url') || url | |
| 62 | + const labels = meta('labels-url') | |
| 63 | + const darkLabels = meta('dark-labels-url') || labels | |
| 64 | + const maxZoom = parseInt(meta('max-zoom'), 10) | |
| 22 | 65 | |
| 23 | −const ATTRIBUTION = | |
| 24 | − '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors ' + | |
| 25 | − '© <a href="https://carto.com/attributions">CARTO</a>' | |
| 66 | + source = { | |
| 67 | + light: { all: url, base: url, labels: labels }, | |
| 68 | + dark: { all: dark, base: dark, labels: darkLabels }, | |
| 69 | + subdomains: meta('subdomains') || '', | |
| 70 | + maxZoom: Number.isFinite(maxZoom) ? maxZoom : null, | |
| 71 | + // Leaflet's detectRetina has two modes, and only one of them is safe | |
| 72 | + // to assume. With `{r}` in the template it asks the server for the @2x | |
| 73 | + // tile; without it, it requests the *next zoom down* at half size -- | |
| 74 | + // four times the tiles, and nothing at all at the server's deepest | |
| 75 | + // zoom. A plain renderd-style server has neither @2x nor the headroom, | |
| 76 | + // so retina rendering is opt-in via `{r}` in TILE_URL. | |
| 77 | + retina: url.includes('{r}'), | |
| 78 | + attribution: meta('attribution') || | |
| 79 | + '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' | |
| 80 | + } | |
| 81 | + return source | |
| 82 | +} | |
| 26 | 83 | |
| 27 | 84 | // Which daisyUI variable the tint is taken from, and how far the wash is |
| 28 | 85 | // pushed. |
| 94 | 151 | } |
| 95 | 152 | |
| 96 | 153 | /** |
| 97 | − * Add a monochrome basemap to `map` and wash it in the current theme colour. | |
| 154 | + * Add a basemap to `map` and wash it in the current theme colour. | |
| 98 | 155 | * |
| 99 | 156 | * Returns a handle with `refresh()` (re-read the theme now) and `remove()`. |
| 100 | 157 | * |
| 105 | 162 | * blend mix-blend-mode for the wash (default 'color') |
| 106 | 163 | * labels false to use the combined basemap and let the wash cover the |
| 107 | 164 | * labels too -- one tile request per tile instead of two, at the |
| 108 | − * cost of much less readable type (default true) | |
| 165 | + * cost of much less readable type (default true; ignored when the | |
| 166 | + * configured server publishes no label-only layer) | |
| 109 | 167 | * styleFrom element to resolve custom properties against; defaults to the |
| 110 | 168 | * map container, which matters inside a shadow root where the |
| 111 | 169 | * host may sit under a different data-theme than <html> |
| 112 | − * maxZoom passed to the tile layer (default 20) | |
| 170 | + * maxZoom passed to the tile layer (default 20), unless the configured | |
| 171 | + * server declares a lower limit of its own | |
| 113 | 172 | */ |
| 114 | 173 | export function addBasemap(map, opts = {}) { |
| 115 | 174 | const container = map.getContainer() |
| 118 | 177 | const tintOpt = opts.tint || DEFAULT_TINT_VAR |
| 119 | 178 | const strength = opts.strength == null ? DEFAULT_STRENGTH : Number(opts.strength) |
| 120 | 179 | |
| 121 | − const withLabels = opts.labels !== false | |
| 122 | − const styleFor = () => STYLES[variant] || (isDarkTheme(styleFrom) ? STYLES.dark : STYLES.light) | |
| 123 | − const baseUrl = () => tileUrl(withLabels ? styleFor().base : styleFor().all) | |
| 180 | + const src = tileSource() | |
| 181 | + // A server that publishes no label-only layer has nothing to draw above | |
| 182 | + // the wash, so `labels` can only ever turn the second request off. | |
| 183 | + const withLabels = opts.labels !== false && !!(src.light.labels && src.dark.labels) | |
| 184 | + const styleFor = () => src[variant] || (isDarkTheme(styleFrom) ? src.dark : src.light) | |
| 185 | + const baseUrl = () => (withLabels ? styleFor().base : styleFor().all) | |
| 124 | 186 | |
| 125 | 187 | const tileOpts = { |
| 126 | − subdomains: 'abcd', | |
| 127 | − maxZoom: opts.maxZoom || 20, | |
| 128 | − detectRetina: true | |
| 188 | + subdomains: src.subdomains, | |
| 189 | + // A configured server's own limit wins over the caller's: callers pass | |
| 190 | + // the zoom the view wants, which a server without tiles that deep can | |
| 191 | + // only answer with 404s. | |
| 192 | + maxZoom: src.maxZoom || opts.maxZoom || 20, | |
| 193 | + detectRetina: src.retina | |
| 129 | 194 | } |
| 130 | 195 | |
| 131 | − const tiles = L.tileLayer(baseUrl(), Object.assign({ attribution: ATTRIBUTION }, tileOpts)) | |
| 196 | + const tiles = L.tileLayer(baseUrl(), Object.assign({ attribution: src.attribution }, tileOpts)) | |
| 132 | 197 | .addTo(map) |
| 133 | 198 | |
| 134 | 199 | // The wash has to be a Leaflet pane, not a child of the container. |
| 165 | 230 | const labelPane = map.createPane(LABEL_PANE) |
| 166 | 231 | labelPane.style.zIndex = LABEL_Z |
| 167 | 232 | labelPane.style.pointerEvents = 'none' |
| 168 | − labelTiles = L.tileLayer(tileUrl(styleFor().labels), | |
| 233 | + labelTiles = L.tileLayer(styleFor().labels, | |
| 169 | 234 | Object.assign({ pane: LABEL_PANE }, tileOpts)).addTo(map) |
| 170 | 235 | } |
| 171 | 236 |
| 173 | 238 | // setUrl no-ops when the template is unchanged, so this only refetches |
| 174 | 239 | // tiles when the theme actually crossed the light/dark line. |
| 175 | 240 | tiles.setUrl(baseUrl()) |
| 176 | − if (labelTiles) labelTiles.setUrl(tileUrl(styleFor().labels)) | |
| 241 | + if (labelTiles) labelTiles.setUrl(styleFor().labels) | |
| 177 | 242 | |
| 178 | 243 | const color = tintOpt === 'none' ? null : asColor( |
| 179 | 244 | tintOpt.startsWith('--') ? cssVar(tintOpt, styleFrom) : tintOpt |
added
apps/room_sanctum/lib/room_sanctum/basemap.ex
+80
−0
| 1 | +defmodule RoomSanctum.Basemap do | |
| 2 | + @moduledoc """ | |
| 3 | + Which tile server the Leaflet basemaps draw from. | |
| 4 | + | |
| 5 | + Nothing is configured by default: the frontend falls back to CARTO's keyless | |
| 6 | + greyscale basemaps, which is what every map used before this existed. An | |
| 7 | + installation that would rather not send its users' viewports to a third party | |
| 8 | + -- or that runs its own server -- points these at it instead. | |
| 9 | + | |
| 10 | + Set in `config/runtime.exs` from the environment, so a release is repointed | |
| 11 | + without a rebuild: | |
| 12 | + | |
| 13 | + TILE_URL template for the base tiles, e.g. | |
| 14 | + "https://tiles.example/styles/light/{z}/{x}/{y}.png" | |
| 15 | + TILE_URL_DARK same, drawn instead when the theme is dark | |
| 16 | + (defaults to TILE_URL -- one style for both themes) | |
| 17 | + TILE_LABELS_URL optional label-only overlay, drawn above the theme | |
| 18 | + wash so place names keep their contrast | |
| 19 | + TILE_LABELS_URL_DARK dark counterpart of TILE_LABELS_URL | |
| 20 | + TILE_ATTRIBUTION attribution HTML for the corner of the map | |
| 21 | + TILE_SUBDOMAINS e.g. "abcd", only if the URL contains `{s}` | |
| 22 | + TILE_MAX_ZOOM deepest zoom the server has tiles for | |
| 23 | + | |
| 24 | + Only `TILE_URL` is required to switch servers; the rest refine it. The values | |
| 25 | + reach the browser as `<meta name="basemap-*">` tags, read by | |
| 26 | + `assets/js/leaflet/basemap.js`. | |
| 27 | + | |
| 28 | + Our own server publishes exactly the pair this expects -- a Positron-like | |
| 29 | + grayscale and a Dark Matter-like dark, the same two CARTO styles the default | |
| 30 | + draws from: | |
| 31 | + | |
| 32 | + TILE_URL=https://tiles.neiam.org/grayscale/{z}/{x}/{y}.png | |
| 33 | + TILE_URL_DARK=https://tiles.neiam.org/dark/{z}/{x}/{y}.png | |
| 34 | + TILE_MAX_ZOOM=20 | |
| 35 | + | |
| 36 | + It is not the default, because it is not a drop-in replacement for CARTO in | |
| 37 | + two ways an installation has to decide about rather than inherit: its import | |
| 38 | + is north america only, so anywhere else renders as empty tiles rather than an | |
| 39 | + error, and it publishes no label-only layer, so the theme wash goes over the | |
| 40 | + place names instead of under them. Neither is visible until someone pans off | |
| 41 | + the continent or reads a tinted label. | |
| 42 | + | |
| 43 | + It serves no `@2x` tiles either, which needs nothing here -- retina is opt-in | |
| 44 | + via `{r}` in `TILE_URL`, see the note in `assets/js/leaflet/basemap.js`. | |
| 45 | + """ | |
| 46 | + | |
| 47 | + @keys [ | |
| 48 | + url: "url", | |
| 49 | + dark_url: "dark-url", | |
| 50 | + labels_url: "labels-url", | |
| 51 | + dark_labels_url: "dark-labels-url", | |
| 52 | + attribution: "attribution", | |
| 53 | + subdomains: "subdomains", | |
| 54 | + max_zoom: "max-zoom" | |
| 55 | + ] | |
| 56 | + | |
| 57 | + @doc """ | |
| 58 | + The configured basemap as `{meta_name, value}` pairs, blank entries dropped. | |
| 59 | + | |
| 60 | + Empty when nothing is configured, which is the signal to the frontend to use | |
| 61 | + its built-in default rather than an explicit instruction to draw nothing. | |
| 62 | + """ | |
| 63 | + def meta_tags do | |
| 64 | + config = Application.get_env(:room_sanctum, :basemap, []) | |
| 65 | + | |
| 66 | + for {key, name} <- @keys, | |
| 67 | + value = present(config[key]), | |
| 68 | + do: {name, value} | |
| 69 | + end | |
| 70 | + | |
| 71 | + defp present(nil), do: nil | |
| 72 | + defp present(value) when is_integer(value), do: Integer.to_string(value) | |
| 73 | + | |
| 74 | + defp present(value) when is_binary(value) do | |
| 75 | + case String.trim(value) do | |
| 76 | + "" -> nil | |
| 77 | + trimmed -> trimmed | |
| 78 | + end | |
| 79 | + end | |
| 80 | +end |
modified
apps/room_sanctum/lib/room_sanctum_web/components/core_components.ex
+14
−0
Click to load diff…
modified
apps/room_sanctum/lib/room_sanctum_web/components/layouts/root.html.heex
+1
−0
Click to load diff…
modified
apps/room_sanctum/lib/room_sanctum_web/templates/layout/root.html.heex
+1
−0
Click to load diff…
added
apps/room_sanctum/test/room_sanctum/basemap_test.exs
+54
−0
Click to load diff…
modified
config/config.exs
+6
−0
Click to load diff…
modified
config/runtime.exs
+12
−0
Click to load diff…
Parents: 8aca56e