diff --git a/src/Map.ts b/src/Map.ts index de99c3f..bc5b70a 100644 --- a/src/Map.ts +++ b/src/Map.ts @@ -1,4 +1,4 @@ -import maplibregl from "maplibre-gl"; +import maplibregl, { AJAXError } from "maplibre-gl"; import { Base64 } from "js-base64"; import type { StyleSpecification, @@ -29,7 +29,7 @@ import { getBrowserLanguage, Language, type LanguageInfo } from "./language"; import { styleToStyle } from "./mapstyle"; import { MaptilerTerrainControl } from "./MaptilerTerrainControl"; import { MaptilerNavigationControl } from "./MaptilerNavigationControl"; -import { geolocation, getLanguageInfoFromFlag, toLanguageInfo } from "@maptiler/client"; +import { MapStyle, geolocation, getLanguageInfoFromFlag, toLanguageInfo } from "@maptiler/client"; import { MaptilerGeolocateControl } from "./MaptilerGeolocateControl"; import { ScaleControl } from "./MLAdapters/ScaleControl"; import { FullscreenControl } from "./MLAdapters/FullscreenControl"; @@ -37,6 +37,7 @@ import { FullscreenControl } from "./MLAdapters/FullscreenControl"; import Minimap from "./Minimap"; import type { MinimapOptionsInput } from "./Minimap"; import { CACHE_API_AVAILABLE, registerLocalCacheProtocol } from "./caching"; +import { getRequestlogger } from "./RequestLogger"; export type LoadWithTerrainEvent = { type: "loadWithTerrain"; @@ -223,6 +224,33 @@ export class Map extends maplibregl.Map { attributionControl: options.forceNoAttributionControl === true ? false : attributionControlOptions, }); + // Safeguard for distant styles at non-http 2xx status URLs + this.on("error", (event) => { + if (event.error instanceof AJAXError) { + const err = event.error as AJAXError; + const url = err.url; + + // Looking into the request logger if a record with such url exists + // in order to get its resourceType + const requestLogger = getRequestlogger(); + const requestRecord = requestLogger.get(url); + + // If the AJAXError is for a "Style" we fall back to the default style, and add a console warning. + if (requestRecord && requestRecord.resourceType === "Style") { + let warning = `The style at URL ${url} could not be loaded. `; + // Loading a new style failed. If a style is not already in place, + // the default one is loaded instead + warning in console + if (!this.getStyle()) { + this.setStyle(MapStyle.STREETS); + warning += `Loading default style "${MapStyle.STREETS.getDefaultVariant().getId()}" as a fallback.`; + } else { + warning += "Leaving the style as is."; + } + console.warn(warning); + } + } + }); + if (config.caching && !CACHE_API_AVAILABLE) { console.warn( "The cache API is only available in secure contexts. More info at https://developer.mozilla.org/en-US/docs/Web/API/Cache", @@ -632,7 +660,19 @@ export class Map extends maplibregl.Map { this.forceLanguageUpdate = false; }); - return super.setStyle(styleToStyle(style), options); + const compatibleStyle = styleToStyle( + style, + // onStyleUrlNotFound callback + // This callback is async (performs a header fetch) + // and will most likely run after the `super.setStyle()` below + // (styleURL: string) => { + // console.warn(`The style URL ${styleURL} does not yield a valid style. Loading the default style instead.`); + // this.setStyle(MapStyle.STREETS); + // }, + ); + + super.setStyle(compatibleStyle, options); + return this; } /** diff --git a/src/RequestLogger.ts b/src/RequestLogger.ts new file mode 100644 index 0000000..0b8f412 --- /dev/null +++ b/src/RequestLogger.ts @@ -0,0 +1,68 @@ +import type { ResourceType } from "maplibre-gl"; + +/** + * Entry for a request + */ +export type RequestRecord = { + /** + * URL, absolute or relative + */ + url: string; + + /** + * Code for the type of resource + */ + resourceType?: ResourceType; + + /** + * Timestamps (ms) just before perfoaming the request + */ + timestamp: [number]; +}; + +/** + * Logs all the request (http and custom protocols) with resource types. + * Essentially a JS Map object. + */ +class RequestLogger { + private records = new Map(); + + /** + * Add a record. The unique key is the URL. If URL already exists, + * a new timestamp is added to the array for the already existing record. + */ + add(url: string, resourceType?: ResourceType) { + if (this.records.has(url)) { + const record = this.records.get(url); + record?.timestamp.push(+new Date()); + } else { + this.records.set(url, { + url, + resourceType, + timestamp: [+new Date()], + } as RequestRecord); + } + } + + /** + * Gets a record or `null`. + */ + get(url: string): RequestRecord | null { + if (this.records.has(url)) { + return this.records.get(url) as RequestRecord; + } + return null; + } +} + +let requestLogger: RequestLogger | null = null; + +/** + * Get a singleton RequetLogger instance + */ +export function getRequestlogger(): RequestLogger { + if (!requestLogger) { + requestLogger = new RequestLogger(); + } + return requestLogger; +} diff --git a/src/tools.ts b/src/tools.ts index c5a9d82..5f58110 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -4,6 +4,7 @@ import { defaults } from "./defaults"; import { config } from "./config"; import { MAPTILER_SESSION_ID } from "./config"; import { localCacheTransformRequest } from "./caching"; +import { getRequestlogger } from "./RequestLogger"; export function enableRTL() { // Prevent this from running server side @@ -79,8 +80,13 @@ export function maptilerCloudTransformRequest(url: string, resourceType?: Resour } } + const localCacheTransformedReq = localCacheTransformRequest(reqUrl, resourceType); + + const requestLogger = getRequestlogger(); + requestLogger.add(localCacheTransformedReq, resourceType); + return { - url: localCacheTransformRequest(reqUrl, resourceType), + url: localCacheTransformedReq, }; }