mirror of
https://github.com/maputnik/editor.git
synced 2026-08-23 21:47:26 +00:00
f14eeae38b
## Launch Checklist This PR increases coverage by adding unit tests to lib folde, replace the skipped end to end placeholder with actual tests and adds more end to end tests. This was mostly done by AI (Claude opus 4.8) and I reviewed it and requested changes where needed. - [x] Briefly describe the changes in this PR. - [x] Link to related issues. - [x] Include before/after visuals or gifs if this PR includes visual changes. - [x] Write tests for all new functionality. - [x] Add an entry to `CHANGELOG.md` under the `## main` section. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
150 lines
4.2 KiB
TypeScript
150 lines
4.2 KiB
TypeScript
import {derefLayers} from "@maplibre/maplibre-gl-style-spec";
|
|
import type {StyleSpecification, LayerSpecification} from "maplibre-gl";
|
|
import tokens from "../config/tokens.json";
|
|
import type {StyleSpecificationWithId} from "./definitions";
|
|
|
|
// Empty style is always used if no style could be restored or fetched
|
|
const emptyStyle = ensureStyleValidity({
|
|
version: 8,
|
|
sources: {},
|
|
layers: [],
|
|
});
|
|
|
|
function generateId() {
|
|
return Math.random().toString(36).substring(2, 9);
|
|
}
|
|
|
|
function ensureHasId(style: StyleSpecification & { id?: string }): StyleSpecificationWithId {
|
|
if(!("id" in style) || !style.id) {
|
|
style.id = generateId();
|
|
}
|
|
return style as StyleSpecificationWithId;
|
|
}
|
|
|
|
function ensureHasNoInteractive(style: StyleSpecificationWithId) {
|
|
const changedLayers = style.layers.map(layer => {
|
|
const changedLayer: LayerSpecification & { interactive?: any } = { ...layer };
|
|
delete changedLayer.interactive;
|
|
return changedLayer;
|
|
});
|
|
|
|
return {
|
|
...style,
|
|
layers: changedLayers
|
|
};
|
|
}
|
|
|
|
function ensureHasNoRefs(style: StyleSpecificationWithId) {
|
|
return {
|
|
...style,
|
|
layers: derefLayers(style.layers)
|
|
};
|
|
}
|
|
|
|
function ensureStyleValidity(style: StyleSpecification): StyleSpecificationWithId {
|
|
return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style)));
|
|
}
|
|
|
|
function getAccessToken(sourceName: string, mapStyle: StyleSpecification, opts: {allowFallback?: boolean}) {
|
|
const metadata = mapStyle.metadata || {} as any;
|
|
let accessToken = metadata[`maputnik:${sourceName}_access_token`];
|
|
|
|
if(opts.allowFallback && !accessToken) {
|
|
accessToken = tokens[sourceName as keyof typeof tokens];
|
|
}
|
|
|
|
return accessToken;
|
|
}
|
|
|
|
function replaceSourceAccessToken(mapStyle: StyleSpecification, sourceName: string, opts={}) {
|
|
const source = mapStyle.sources[sourceName];
|
|
if(!source) return mapStyle;
|
|
if(!("url" in source) || !source.url) return mapStyle;
|
|
|
|
let authSourceName = sourceName;
|
|
if(sourceName === "thunderforest_transport" || sourceName === "thunderforest_outdoors") {
|
|
authSourceName = "thunderforest";
|
|
}
|
|
else if (("url" in source) && source.url?.match(/\.stadiamaps\.com/)) {
|
|
// The code currently usually assumes openmaptiles == MapTiler,
|
|
// so we need to check the source URL.
|
|
authSourceName = "stadia";
|
|
}
|
|
else if (("url" in source) && source.url?.match(/\.locationiq\.com/)) {
|
|
authSourceName = "locationiq";
|
|
}
|
|
|
|
const accessToken = getAccessToken(authSourceName, mapStyle, opts);
|
|
|
|
if(!accessToken) {
|
|
// Early exit.
|
|
return mapStyle;
|
|
}
|
|
|
|
let sourceUrl: string;
|
|
if (authSourceName == "stadia") {
|
|
// Stadia Maps does not always require an API key,
|
|
// so there is no placeholder in our styles.
|
|
// We append it at the end of the URL when exporting if necessary.
|
|
sourceUrl = `${source.url}?api_key=${accessToken}`;
|
|
} else {
|
|
sourceUrl = source.url.replace("{key}", accessToken);
|
|
}
|
|
|
|
const changedSources = {
|
|
...mapStyle.sources,
|
|
[sourceName]: {
|
|
...source,
|
|
url: sourceUrl
|
|
}
|
|
};
|
|
const changedStyle = {
|
|
...mapStyle,
|
|
sources: changedSources
|
|
};
|
|
return changedStyle;
|
|
}
|
|
|
|
function replaceAccessTokens(mapStyle: StyleSpecification, opts={}) {
|
|
let changedStyle = mapStyle;
|
|
|
|
Object.keys(mapStyle.sources).forEach((sourceName) => {
|
|
changedStyle = replaceSourceAccessToken(changedStyle, sourceName, opts);
|
|
});
|
|
|
|
if (mapStyle.glyphs && (mapStyle.glyphs.match(/\.tilehosting\.com/) || mapStyle.glyphs.match(/\.maptiler\.com/))) {
|
|
const newAccessToken = getAccessToken("openmaptiles", mapStyle, opts);
|
|
if (newAccessToken) {
|
|
changedStyle = {
|
|
...changedStyle,
|
|
glyphs: mapStyle.glyphs.replace("{key}", newAccessToken)
|
|
};
|
|
}
|
|
}
|
|
|
|
return changedStyle;
|
|
}
|
|
|
|
function stripAccessTokens(mapStyle: StyleSpecification) {
|
|
const changedMetadata = {
|
|
...mapStyle.metadata as any
|
|
};
|
|
delete changedMetadata["maputnik:openmaptiles_access_token"];
|
|
delete changedMetadata["maputnik:thunderforest_access_token"];
|
|
delete changedMetadata["maputnik:stadia_access_token"];
|
|
delete changedMetadata["maputnik:locationiq_access_token"];
|
|
return {
|
|
...mapStyle,
|
|
metadata: changedMetadata
|
|
};
|
|
}
|
|
|
|
export {
|
|
ensureStyleValidity,
|
|
emptyStyle,
|
|
generateId,
|
|
getAccessToken,
|
|
replaceAccessTokens,
|
|
stripAccessTokens,
|
|
};
|