const DEFAULT_MAPBOX_STYLE_ID = "mapbox/light-v11"; const DEFAULT_CITY_TILE_STYLES = [ { id: "light", label: "浅色", styleId: "mapbox/light-v11" }, { id: "streets", label: "街道", styleId: "mapbox/streets-v12" }, { id: "outdoors", label: "地形", styleId: "mapbox/outdoors-v12" }, { id: "satellite", label: "卫星", styleId: "mapbox/satellite-streets-v12" }, { id: "custom", label: "自定义", styleId: "" }, ]; const OSM_PROVIDER = { name: "osm", attribution: "© OpenStreetMap", tileSize: 256, }; const CARTO_LIGHT_PROVIDER = { name: "cartoLight", attribution: "© OpenStreetMap © CARTO", tileSize: 256, }; const LOCAL_CITY_TILES_ATTRIBUTION = "© Mapbox © OpenStreetMap"; function normalizeProviderName(name) { return String(name || "osm").replace(/[-_\s]/g, "").toLowerCase(); } function normalizeMapboxStyleId(styleId = DEFAULT_MAPBOX_STYLE_ID) { const cleaned = String(styleId || DEFAULT_MAPBOX_STYLE_ID) .replace(/^styles\/v1\//, "") .replace(/^\/+|\/+$/g, ""); const stylePath = cleaned.includes("/") ? cleaned : `mapbox/${cleaned}`; return stylePath.split("/").map((part) => encodeURIComponent(part)).join("/"); } function normalizeStyleOption(style) { const id = String(style?.id || "").trim(); const label = String(style?.label || id).trim(); const styleId = String(style?.styleId || style?.mapboxStyleId || "").trim(); if (!id || !label || (id !== "custom" && !styleId)) return null; return { id, label, styleId }; } function normalizeTileBounds(bounds) { if (!bounds || typeof bounds !== "object") return null; const normalized = { z: Number(bounds.z), minX: Number(bounds.minX), maxX: Number(bounds.maxX), minY: Number(bounds.minY), maxY: Number(bounds.maxY), }; if (Object.values(normalized).some((value) => !Number.isInteger(value))) return null; if (normalized.minX > normalized.maxX || normalized.minY > normalized.maxY) return null; return normalized; } function tileIsInsideBounds(bounds, { z, x, y }) { if (!bounds) return true; return z === bounds.z && x >= bounds.minX && x <= bounds.maxX && y >= bounds.minY && y <= bounds.maxY; } export function normalizeCityTileStyles(runtimeConfig = {}) { const configured = Array.isArray(runtimeConfig.mapboxStyles) ? runtimeConfig.mapboxStyles.map(normalizeStyleOption).filter(Boolean) : []; const stylesById = new Map(); for (const style of configured) stylesById.set(style.id, style); for (const style of DEFAULT_CITY_TILE_STYLES) { if (!stylesById.has(style.id)) stylesById.set(style.id, style); } return Array.from(stylesById.values()); } function resolveMapboxStyleId(special, runtimeConfig) { if (special.mapboxStyleId) return special.mapboxStyleId; const styles = normalizeCityTileStyles(runtimeConfig); const activeStyleId = special.activeMapStyleId || runtimeConfig.activeMapStyleId || runtimeConfig.defaultMapStyleId || runtimeConfig.defaultMapboxStyleKey || runtimeConfig.defaultStyleId || ""; if (!activeStyleId) { return runtimeConfig.mapboxStyleId || runtimeConfig.styleId || runtimeConfig.mapbox?.styleId || styles[0]?.styleId || DEFAULT_MAPBOX_STYLE_ID; } if (activeStyleId === "custom") { return runtimeConfig.customMapboxStyleId || runtimeConfig.mapboxStyleId || runtimeConfig.styleId || runtimeConfig.mapbox?.styleId || DEFAULT_MAPBOX_STYLE_ID; } return styles.find((style) => style.id === activeStyleId)?.styleId || runtimeConfig.mapboxStyleId || runtimeConfig.styleId || runtimeConfig.mapbox?.styleId || DEFAULT_MAPBOX_STYLE_ID; } export function resolveCityTileProvider(special = {}, runtimeConfig = {}) { const providerName = normalizeProviderName( special.tileProvider || runtimeConfig.cityTileProvider || runtimeConfig.tileProvider || runtimeConfig.provider, ); if (providerName === "mapbox" || providerName === "mapboxstatic" || providerName === "mapboxtiles") { const token = special.mapboxAccessToken || special.mapboxToken || runtimeConfig.mapboxAccessToken || runtimeConfig.mapboxToken || runtimeConfig.mapbox?.accessToken || runtimeConfig.mapbox?.token || ""; if (!token) return { ...CARTO_LIGHT_PROVIDER, fallbackReason: "missing-mapbox-token" }; return { name: "mapboxStatic", attribution: "© Mapbox © OpenStreetMap", tileSize: Number(special.tileSize || runtimeConfig.tileSize || 256) || 256, mapboxStyleId: normalizeMapboxStyleId(resolveMapboxStyleId(special, runtimeConfig)), mapboxAccessToken: token, retina: special.retinaTiles ?? runtimeConfig.retinaTiles ?? true, }; } if (providerName === "carto" || providerName === "cartolight" || providerName === "cartopositron") { return { ...CARTO_LIGHT_PROVIDER }; } if (providerName === "local" || providerName === "localcitytiles" || providerName === "localtiles") { const baseUrl = String( special.localTileBaseUrl || special.tileBaseUrl || runtimeConfig.localTileBaseUrl || runtimeConfig.localCityTileBaseUrl || "", ).replace(/\/+$/g, ""); if (!baseUrl) return { ...CARTO_LIGHT_PROVIDER, fallbackReason: "missing-local-tile-base-url" }; return { name: "localCityTiles", attribution: special.localTileAttribution || runtimeConfig.localTileAttribution || LOCAL_CITY_TILES_ATTRIBUTION, tileSize: Number(special.tileSize || runtimeConfig.tileSize || 256) || 256, baseUrl, tileBounds: normalizeTileBounds(special.localTileBounds || runtimeConfig.localTileBounds), version: String(special.localTileVersion || runtimeConfig.localTileVersion || "").trim(), }; } return { ...OSM_PROVIDER }; } export function buildCityTileUrl(provider, { z, x, y }) { if (provider?.name === "localCityTiles") { if (!tileIsInsideBounds(provider.tileBounds, { z, x, y })) return ""; const versionSuffix = provider.version ? `?v=${encodeURIComponent(provider.version)}` : ""; return `${provider.baseUrl}/${z}/${x}/${y}.png${versionSuffix}`; } if (provider?.name === "mapboxStatic") { const scaleSuffix = provider.retina === false ? "" : "@2x"; const params = new URLSearchParams({ access_token: provider.mapboxAccessToken }); return `https://api.mapbox.com/styles/v1/${provider.mapboxStyleId}/tiles/${provider.tileSize}/${z}/${x}/${y}${scaleSuffix}?${params.toString()}`; } if (provider?.name === "cartoLight") { const scaleSuffix = provider.retina === false ? "" : "@2x"; return `https://a.basemaps.cartocdn.com/light_all/${z}/${x}/${y}${scaleSuffix}.png`; } return `https://tile.openstreetmap.org/${z}/${x}/${y}.png`; } export function runtimeCityTileConfig() { return globalThis.CITYBOOK_MAP_CONFIG || {}; }