Skip to content

Commit 5d94919

Browse files
committed
fix: rewrite download-style to match old manage.sh — per-style style.json + sprites from GH Pages, unified under tile-styles data type
1 parent 32de944 commit 5d94919

6 files changed

Lines changed: 132 additions & 35 deletions

File tree

packages/core/src/services/capabilities.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const WELL_KNOWN_CAPABILITIES: ReadonlySet<string> = new Set([
3737
// assets"). The corresponding produces TYPES (the actual on-disk format)
3838
// live in WELL_KNOWN_DATA_TYPES below: `osm-data` covers `osm-pbf` /
3939
// `osm-pbf-bz2`; `gtfs-data` covers `gtfs`; `tile-asset-data` covers
40-
// `tile-fonts` / `tile-sprites` / `tile-style`.
40+
// `tile-fonts` / `tile-styles`.
4141
"osm-data",
4242
"gtfs-data",
4343
"tile-asset-data",
@@ -49,8 +49,7 @@ export const WELL_KNOWN_DATA_TYPES: ReadonlySet<string> = new Set([
4949
"osm-pbf-bz2",
5050
"gtfs",
5151
"tile-fonts",
52-
"tile-sprites",
53-
"tile-style",
52+
"tile-styles",
5453
"pelias-placeholder-data",
5554
"pelias-whosonfirst-data",
5655
]);

packages/core/src/services/types.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,7 @@ export interface ResolutionResult {
271271
* aligned with the data-manager service's `produces:` declarations and with
272272
* `services/data-manager/src/state.ts` where this is mirrored for the service.
273273
*/
274-
export type DatasetType =
275-
| "osm-pbf"
276-
| "osm-pbf-bz2"
277-
| "gtfs"
278-
| "tile-fonts"
279-
| "tile-sprites"
280-
| "tile-style";
274+
export type DatasetType = "osm-pbf" | "osm-pbf-bz2" | "gtfs" | "tile-fonts" | "tile-styles";
281275

282276
export interface DatasetMetadata {
283277
type: DatasetType;

services/data-manager/service.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
{ "type": "osm-pbf", "sourceDir": "data/osm" },
3030
{ "type": "gtfs", "sourceDir": "data/gtfs" },
3131
{ "type": "tile-fonts", "sourceDir": "data/tile-fonts" },
32-
{ "type": "tile-sprites", "sourceDir": "data/tile-sprites" },
32+
{ "type": "tile-styles", "sourceDir": "data/tile-styles" },
3333
{ "type": "osm-pbf-bz2", "sourceDir": "data/osm-bz2" }
3434
],
3535
"bindMounts": [{ "source": "@infra:data", "target": "/data", "readOnly": false }],
Lines changed: 126 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,146 @@
1-
import { mkdirSync, statSync } from "node:fs";
1+
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
22
import { join } from "node:path";
33
import { execa } from "execa";
44
import type { StateStore } from "../state.js";
55
import { curlAtomic } from "./atomic-download.js";
66

7+
export interface StyleEntry {
8+
/** Style slug — becomes the subdirectory name under `tile-styles/`. */
9+
id: string;
10+
/** GitHub `<owner>/<repo>`, used for both raw style.json and sprite GH-Pages base URL. */
11+
repo: string;
12+
/** Branch to pull `style.json` from. */
13+
branch: string;
14+
/**
15+
* Base name / path of the sprite files on the repo's GH Pages deployment,
16+
* relative to `https://<owner>.github.io/<repo>/`. Most styles put them at
17+
* the root (`sprite`), but osm-liberty puts them at `sprites/osm-liberty`.
18+
* We'll fetch `<basePath>.json`, `<basePath>.png`, `<basePath>@2x.{json,png}`
19+
* and save them locally as `sprite.{json,png}` / `sprite@2x.{json,png}`.
20+
*/
21+
spritePath?: string;
22+
}
23+
724
export interface StyleAssetUrls {
825
fonts: string;
9-
sprites: string;
10-
styles: { id: string; url: string }[];
26+
styles: StyleEntry[];
1127
}
1228

1329
export function resolveStyleAssetUrls(): StyleAssetUrls {
1430
return {
31+
// openmaptiles/fonts — pre-built PBF glyph stacks (Metropolis, Noto Sans,
32+
// Open Sans, etc.) that tileserver-gl serves at /fonts/<stack>/<range>.pbf.
1533
fonts: "https://github.com/openmaptiles/fonts/releases/latest/download/v2.0.zip",
16-
sprites:
17-
"https://github.com/openmaptiles/osm-bright-gl-style/releases/latest/download/sprite.zip",
34+
// Per-style fetch: style.json from raw GitHub + sprite.{json,png}(@2x)
35+
// from the style repo's GitHub Pages deployment. Matches the historical
36+
// `infra/docker/manage.sh` behaviour — there is no release-asset zip with
37+
// sprites, and tileserver-gl expects `styles/<name>/style.json` alongside
38+
// the matching `sprite.*` files in the same folder.
1839
styles: [
19-
{ id: "osm-bright", url: "https://api.maptiler.com/maps/openstreetmap/style.json" },
20-
{ id: "positron", url: "https://api.maptiler.com/maps/positron/style.json" },
21-
{ id: "dark-matter", url: "https://api.maptiler.com/maps/darkmatter/style.json" },
40+
{ id: "osm-bright", repo: "openmaptiles/osm-bright-gl-style", branch: "master" },
41+
{ id: "dark-matter", repo: "openmaptiles/dark-matter-gl-style", branch: "master" },
42+
{ id: "positron", repo: "openmaptiles/positron-gl-style", branch: "master" },
43+
{
44+
id: "osm-liberty",
45+
repo: "maputnik/osm-liberty",
46+
branch: "gh-pages",
47+
spritePath: "sprites/osm-liberty",
48+
},
2249
],
2350
};
2451
}
2552

53+
const SPRITE_SUFFIXES = [".json", ".png", "@2x.json", "@2x.png"];
54+
2655
export interface DownloadStyleOptions {
2756
dataDir: string;
2857
store: StateStore;
2958
}
3059

60+
/**
61+
* Rewrite a fetched MapLibre style so tileserver-gl serves it from local
62+
* resources instead of MapTiler Cloud / GitHub Pages:
63+
*
64+
* - every `sources[].type === "vector"` → `{ type: "vector", url: "mbtiles://{openmapx}" }`
65+
* (tileserver-gl resolves `{openmapx}` to the MBTiles data source in
66+
* `config.json`'s `data` block)
67+
* - `glyphs = "{fontstack}/{range}.pbf"` → served from the mounted fonts volume
68+
* - `sprite = "{styleJsonFolder}/sprite"` → sibling sprite files next to style.json
69+
*/
70+
function patchStyleForLocal(styleJson: Record<string, unknown>): Record<string, unknown> {
71+
const patched = { ...styleJson };
72+
const sources = (patched.sources as Record<string, Record<string, unknown>>) ?? {};
73+
const newSources: Record<string, Record<string, unknown>> = {};
74+
for (const [name, src] of Object.entries(sources)) {
75+
if (src.type === "vector") {
76+
newSources[name] = { type: "vector", url: "mbtiles://{openmapx}" };
77+
} else {
78+
newSources[name] = src;
79+
}
80+
}
81+
patched.sources = newSources;
82+
patched.glyphs = "{fontstack}/{range}.pbf";
83+
patched.sprite = "{styleJsonFolder}/sprite";
84+
return patched;
85+
}
86+
87+
async function downloadOneStyle(
88+
entry: StyleEntry,
89+
targetDir: string,
90+
): Promise<{ styleJson: string; spriteFiles: string[] }> {
91+
mkdirSync(targetDir, { recursive: true });
92+
93+
// style.json from raw.githubusercontent.com
94+
const styleUrl = `https://raw.githubusercontent.com/${entry.repo}/${entry.branch}/style.json`;
95+
const styleJsonPath = join(targetDir, "style.json");
96+
await curlAtomic(styleUrl, styleJsonPath);
97+
98+
// Patch in place so tileserver-gl can serve the style from local paths.
99+
try {
100+
const raw = JSON.parse(readFileSync(styleJsonPath, "utf-8")) as Record<string, unknown>;
101+
const patched = patchStyleForLocal(raw);
102+
writeFileSync(styleJsonPath, `${JSON.stringify(patched, null, 2)}\n`, "utf-8");
103+
} catch (err) {
104+
throw new Error(`style ${entry.id}: failed to patch style.json (${(err as Error).message})`);
105+
}
106+
107+
// sprite.* from GH Pages. Each suffix is best-effort — @2x variants
108+
// occasionally 404. The base path defaults to `sprite` (root of the gh-pages
109+
// deployment), but some styles (e.g. osm-liberty) nest them under
110+
// `sprites/<name>`; we still save them locally as `sprite.{json,png}` so
111+
// the patched `sprite: "{styleJsonFolder}/sprite"` reference resolves.
112+
const [owner, repoName] = entry.repo.split("/");
113+
const remoteBase = entry.spritePath ?? "sprite";
114+
const spriteOrigin = `https://${owner}.github.io/${repoName}`;
115+
const downloaded: string[] = [];
116+
for (const suffix of SPRITE_SUFFIXES) {
117+
const url = `${spriteOrigin}/${remoteBase}${suffix}`;
118+
const target = join(targetDir, `sprite${suffix}`);
119+
try {
120+
await curlAtomic(url, target);
121+
downloaded.push(`sprite${suffix}`);
122+
} catch {
123+
// best-effort
124+
}
125+
}
126+
if (!downloaded.includes("sprite.json") || !downloaded.includes("sprite.png")) {
127+
throw new Error(
128+
`style ${entry.id}: required sprite files not reachable at ${spriteOrigin}/${remoteBase}.{json,png}`,
129+
);
130+
}
131+
132+
return { styleJson: styleJsonPath, spriteFiles: downloaded };
133+
}
134+
31135
export async function downloadStyle(opts: DownloadStyleOptions): Promise<void> {
32-
// Asset families live at top-level paths under data/ so they don't collide
33-
// with any consumer service's data dir (e.g. `data/tileserver/` is the
34-
// tileserver consumer's hardlink target dir).
35136
const fontsDir = join(opts.dataDir, "tile-fonts");
36-
const spritesDir = join(opts.dataDir, "tile-sprites");
37137
const stylesDir = join(opts.dataDir, "tile-styles");
38138
mkdirSync(fontsDir, { recursive: true });
39-
mkdirSync(spritesDir, { recursive: true });
40139
mkdirSync(stylesDir, { recursive: true });
41140

42141
const urls = resolveStyleAssetUrls();
43142

143+
// Fonts (shared across every style).
44144
const fontsZip = join(opts.dataDir, "tile-fonts.zip");
45145
await curlAtomic(urls.fonts, fontsZip);
46146
await execa("unzip", ["-qo", fontsZip, "-d", fontsDir], { stdio: "inherit" });
@@ -53,7 +153,17 @@ export async function downloadStyle(opts: DownloadStyleOptions): Promise<void> {
53153
path: fontsDir,
54154
});
55155

56-
const spritesZip = join(opts.dataDir, "tile-sprites.zip");
57-
await curlAtomic(urls.sprites, spritesZip);
58-
await execa("unzip", ["-qo", spritesZip, "-d", spritesDir], { stdio: "inherit" });
156+
// One subdirectory per style, each with its own style.json + sprite files.
157+
for (const entry of urls.styles) {
158+
const styleDir = join(stylesDir, entry.id);
159+
const result = await downloadOneStyle(entry, styleDir);
160+
opts.store.upsert({
161+
type: "tile-styles",
162+
id: entry.id,
163+
url: `https://raw.githubusercontent.com/${entry.repo}/${entry.branch}/style.json`,
164+
sizeBytes: statSync(result.styleJson).size,
165+
downloadedAt: new Date().toISOString(),
166+
path: styleDir,
167+
});
168+
}
59169
}

services/data-manager/src/state.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,7 @@ import { dirname, join } from "node:path";
44
// Kept in sync with packages/core/src/services/types.ts `DatasetType`. When
55
// the data-manager is allowed to depend on @openmapx/core directly (post-Dockerfile
66
// refactor), we can import from there and delete this local copy.
7-
export type DatasetType =
8-
| "osm-pbf"
9-
| "osm-pbf-bz2"
10-
| "gtfs"
11-
| "tile-fonts"
12-
| "tile-sprites"
13-
| "tile-style";
7+
export type DatasetType = "osm-pbf" | "osm-pbf-bz2" | "gtfs" | "tile-fonts" | "tile-styles";
148

159
export interface DatasetMetadata {
1610
type: DatasetType;

services/tileserver/service.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"provides": ["tile-server"],
1818
"consumes": [
1919
{ "type": "tile-fonts", "mountAt": "/data/fonts", "readOnly": true, "required": true },
20-
{ "type": "tile-sprites", "mountAt": "/data/sprites", "readOnly": true, "required": true }
20+
{ "type": "tile-styles", "mountAt": "/data/styles", "readOnly": true, "required": true }
2121
],
2222
"bindMounts": [{ "source": "config/config.json", "target": "/data/config.json" }],
2323
"exposure": {

0 commit comments

Comments
 (0)