2025-06-27 18:36:02 +02:00
|
|
|
import { RasterLayerProperties } from "./RasterLayerProperties"
|
|
|
|
import { AvailableRasterLayers, RasterLayerPolygon } from "./RasterLayers"
|
|
|
|
import { Utils } from "../Utils"
|
|
|
|
import * as eli from "../../public/assets/data/editor-layer-index.json"
|
|
|
|
import * as layers_global from "../../src/assets/global-raster-layers.json"
|
|
|
|
import eli_global from "../../src/assets/generated/editor-layer-index-global.json"
|
|
|
|
import bing from "../../src/assets/bing.json"
|
|
|
|
import Constants from "../../src/Models/Constants"
|
|
|
|
import ThemeConfig from "../../src/Models/ThemeConfig/ThemeConfig"
|
|
|
|
import { ThemeConfigJson } from "../../src/Models/ThemeConfig/Json/ThemeConfigJson"
|
|
|
|
import SpecialVisualizations from "../../src/UI/SpecialVisualizations"
|
|
|
|
import ValidationUtils from "../../src/Models/ThemeConfig/Conversion/ValidationUtils"
|
2025-07-10 22:09:58 +02:00
|
|
|
import {
|
|
|
|
QuestionableTagRenderingConfigJson
|
|
|
|
} from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
2025-06-27 18:36:02 +02:00
|
|
|
import { LayerConfigJson } from "../../src/Models/ThemeConfig/Json/LayerConfigJson"
|
|
|
|
|
|
|
|
export interface ServerSourceInfo {
|
|
|
|
url: string
|
|
|
|
description: string
|
|
|
|
category: "core" | "feature" | "maplayer"
|
|
|
|
selfhostable?: boolean | string
|
|
|
|
sourceAvailable?: boolean | string
|
|
|
|
openData?: boolean | string
|
|
|
|
trigger?: ("always" | "specific_theme" | "specific_feature" | "clear_consent" | string)[]
|
2025-07-10 18:26:31 +02:00
|
|
|
moreInfo?: string[]
|
2025-06-27 18:36:02 +02:00
|
|
|
logging?: "yes" | "probably" | "no"
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates what URLs the webapp might access, and why
|
|
|
|
*/
|
|
|
|
export class SourceOverview {
|
|
|
|
private eliUrlsCached: ServerSourceInfo[]
|
|
|
|
|
2025-07-10 18:26:31 +02:00
|
|
|
public async getOverview(
|
|
|
|
layout?: ThemeConfig,
|
|
|
|
layoutJson?: ThemeConfigJson
|
|
|
|
): Promise<(ServerSourceInfo | string)[]> {
|
2025-06-27 18:36:02 +02:00
|
|
|
const apiUrls: (ServerSourceInfo | string)[] = [
|
|
|
|
...Constants.allServers,
|
|
|
|
<ServerSourceInfo>{
|
|
|
|
url: "https://data.mapcomplete.org/nsi",
|
|
|
|
description: "Contains a copy and the logos of the Name Suggestion Index",
|
|
|
|
category: "core",
|
|
|
|
},
|
|
|
|
]
|
|
|
|
|
|
|
|
apiUrls.push(...(await this.eliUrls()))
|
|
|
|
|
|
|
|
for (const sv of SpecialVisualizations.specialVisualizations) {
|
|
|
|
if (typeof sv.needsUrls === "function") {
|
|
|
|
// Handled below
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
apiUrls.push(...(sv.needsUrls ?? []))
|
|
|
|
}
|
|
|
|
|
|
|
|
const usedSpecialVisualisations = layoutJson?.layers?.flatMap((l) =>
|
|
|
|
ValidationUtils.getAllSpecialVisualisations(
|
2025-07-10 18:26:31 +02:00
|
|
|
<QuestionableTagRenderingConfigJson[]>(<LayerConfigJson>l).tagRenderings ?? []
|
|
|
|
)
|
|
|
|
)
|
2025-06-27 18:36:02 +02:00
|
|
|
|
|
|
|
for (const usedSpecialVisualisation of usedSpecialVisualisations ?? []) {
|
|
|
|
if (typeof usedSpecialVisualisation === "string") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const neededUrls = usedSpecialVisualisation.func.needsUrls ?? []
|
|
|
|
if (typeof neededUrls === "function") {
|
2025-07-10 22:09:58 +02:00
|
|
|
const needed: string | string[] | ServerSourceInfo | ServerSourceInfo[] = neededUrls(
|
2025-07-10 18:26:31 +02:00
|
|
|
usedSpecialVisualisation.args
|
|
|
|
)
|
2025-06-27 18:36:02 +02:00
|
|
|
if (Array.isArray(needed)) {
|
|
|
|
apiUrls.push(...needed)
|
|
|
|
} else {
|
|
|
|
apiUrls.push(needed)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const geojsonSources: string[] = layout?.layers?.map((l) => l.source?.geojsonSource) ?? []
|
|
|
|
|
2025-07-10 18:26:31 +02:00
|
|
|
return Utils.NoNull(apiUrls.concat(...geojsonSources)).filter((item) => {
|
2025-06-27 18:36:02 +02:00
|
|
|
if (typeof item === "string") {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return item.url?.trim()?.length > 0
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private async eliUrls(): Promise<ServerSourceInfo[]> {
|
|
|
|
if (this.eliUrlsCached) {
|
|
|
|
return this.eliUrlsCached
|
|
|
|
}
|
|
|
|
let urls: ServerSourceInfo[] = []
|
|
|
|
const regex = /{switch:([^}]+)}/
|
|
|
|
const rasterLayers: { properties: RasterLayerProperties }[] = [
|
|
|
|
AvailableRasterLayers.defaultBackgroundLayer,
|
|
|
|
...eli.features,
|
|
|
|
bing,
|
|
|
|
...eli_global.map((properties) => ({ properties })),
|
|
|
|
...layers_global.layers.map((properties) => ({ properties })),
|
|
|
|
]
|
|
|
|
for (const feature of rasterLayers) {
|
|
|
|
const f = <RasterLayerPolygon>feature
|
|
|
|
const url = f.properties.url
|
|
|
|
const match = url.match(regex)
|
|
|
|
|
2025-07-10 22:09:58 +02:00
|
|
|
const packageInInfo: (url: (string | string[])) => ServerSourceInfo[] = (urls: string | string[]) => {
|
|
|
|
if (typeof urls === "string") {
|
|
|
|
urls = [urls]
|
2025-06-27 18:36:02 +02:00
|
|
|
}
|
2025-07-10 22:09:58 +02:00
|
|
|
return urls.map(url => <ServerSourceInfo>{
|
2025-06-27 18:36:02 +02:00
|
|
|
url,
|
2025-07-10 18:26:31 +02:00
|
|
|
description:
|
|
|
|
"Background layer source or supporting sources for " + f.properties.id,
|
2025-06-27 18:36:02 +02:00
|
|
|
trigger: ["specific_feature"],
|
|
|
|
category: "maplayer",
|
2025-07-10 18:26:31 +02:00
|
|
|
moreInfo: Utils.NoEmpty([
|
|
|
|
"https://github.com/osmlab/editor-layer-index",
|
|
|
|
f.properties?.attribution?.url,
|
|
|
|
]),
|
2025-07-10 22:09:58 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const packageInInfoD: (url: (string | string[])) => (ServerSourceInfo[]) = (url: string | string[]) => {
|
|
|
|
if (!url) {
|
|
|
|
return []
|
2025-07-10 18:26:31 +02:00
|
|
|
}
|
2025-07-10 22:09:58 +02:00
|
|
|
return packageInInfo(url)
|
2025-06-27 18:36:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
const domains = match[1].split(",")
|
|
|
|
const subpart = match[0]
|
2025-07-10 22:09:58 +02:00
|
|
|
const info: ServerSourceInfo[] = domains.flatMap((d) =>
|
2025-07-10 18:26:31 +02:00
|
|
|
packageInInfo(url.replace(subpart, d))
|
|
|
|
)
|
2025-06-27 18:36:02 +02:00
|
|
|
urls.push(...info)
|
|
|
|
} else {
|
2025-07-10 22:09:58 +02:00
|
|
|
urls.push(...packageInInfo(url))
|
2025-06-27 18:36:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (f.properties.type === "vector") {
|
|
|
|
// We also need to whitelist eventual sources
|
|
|
|
urls.push(...(f.properties["connect-src"] ?? []).map(packageInInfo))
|
|
|
|
let url = f.properties.url
|
|
|
|
if (url.startsWith("pmtiles://")) {
|
|
|
|
url = url.substring("pmtiles://".length)
|
|
|
|
}
|
|
|
|
if (url.endsWith(".pmtiles")) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
console.log("Downloading ", url)
|
2025-07-10 22:09:58 +02:00
|
|
|
urls.push(...packageInInfo(url))
|
2025-06-27 18:36:02 +02:00
|
|
|
const styleSpec = await Utils.downloadJsonCached(url, 1000 * 120, {
|
|
|
|
Origin: "https://mapcomplete.org",
|
|
|
|
})
|
|
|
|
for (const key of Object.keys(styleSpec?.["sources"] ?? {})) {
|
2025-07-10 22:09:58 +02:00
|
|
|
let url = styleSpec["sources"][key].url
|
2025-06-27 18:36:02 +02:00
|
|
|
if (!url) {
|
|
|
|
continue
|
|
|
|
}
|
2025-07-10 22:09:58 +02:00
|
|
|
console.log(">>> SPlitting", url)
|
|
|
|
const split = url.split(",https://")
|
|
|
|
console.log("Multisplit:", split)
|
|
|
|
url = split[0]
|
2025-06-27 18:36:02 +02:00
|
|
|
let urlClipped = url
|
|
|
|
if (url.indexOf("?") > 0) {
|
|
|
|
urlClipped = url?.substring(0, url.indexOf("?"))
|
|
|
|
}
|
2025-07-10 22:09:58 +02:00
|
|
|
urls.push(...packageInInfo(url))
|
2025-06-27 18:36:02 +02:00
|
|
|
if (urlClipped.endsWith(".json")) {
|
|
|
|
const tileInfo = await Utils.downloadJsonCached(url, 1000 * 120, {
|
2025-07-10 22:09:58 +02:00
|
|
|
Origins: "https://mapcomplete.org"
|
2025-06-27 18:36:02 +02:00
|
|
|
})
|
2025-07-10 22:09:58 +02:00
|
|
|
urls.push(...packageInInfo(tileInfo["tiles"] ?? []))
|
2025-06-27 18:36:02 +02:00
|
|
|
}
|
|
|
|
}
|
2025-07-10 18:26:31 +02:00
|
|
|
urls.push(
|
|
|
|
...(styleSpec["tiles"] ?? [])
|
|
|
|
.flatMap((ls) => ls)
|
2025-07-10 22:09:58 +02:00
|
|
|
.map((url) => packageInInfoD(url))
|
2025-07-10 18:26:31 +02:00
|
|
|
)
|
2025-07-10 22:09:58 +02:00
|
|
|
urls.push(...packageInInfoD(styleSpec["sprite"]))
|
|
|
|
urls.push(...packageInInfoD(styleSpec["glyphs"]))
|
2025-06-27 18:36:02 +02:00
|
|
|
} catch (e) {
|
2025-07-10 22:09:58 +02:00
|
|
|
console.error(e)
|
2025-06-27 18:36:02 +02:00
|
|
|
console.error(
|
2025-07-10 22:09:58 +02:00
|
|
|
"ERROR: could not download a resource: " + url + "\nSome sprites might not be whitelisted and thus not load")
|
2025-06-27 18:36:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-07-10 22:09:58 +02:00
|
|
|
urls = urls.filter((item) => !!item?.url)
|
2025-07-10 18:26:31 +02:00
|
|
|
urls.sort((a, b) => (a < b ? -1 : 1))
|
|
|
|
urls = Utils.DedupOnId(urls, (item) => item.url)
|
2025-06-27 18:36:02 +02:00
|
|
|
this.eliUrlsCached = urls
|
|
|
|
return urls
|
|
|
|
}
|
|
|
|
}
|