1
- import maplibregl from "maplibre-gl" ;
1
+ import maplibregl , { AJAXError } from "maplibre-gl" ;
2
2
import { Base64 } from "js-base64" ;
3
3
import type {
4
4
StyleSpecification ,
@@ -29,7 +29,7 @@ import { getBrowserLanguage, Language, type LanguageInfo } from "./language";
29
29
import { styleToStyle } from "./mapstyle" ;
30
30
import { MaptilerTerrainControl } from "./MaptilerTerrainControl" ;
31
31
import { MaptilerNavigationControl } from "./MaptilerNavigationControl" ;
32
- import { geolocation , getLanguageInfoFromFlag , toLanguageInfo } from "@maptiler/client" ;
32
+ import { MapStyle , geolocation , getLanguageInfoFromFlag , toLanguageInfo } from "@maptiler/client" ;
33
33
import { MaptilerGeolocateControl } from "./MaptilerGeolocateControl" ;
34
34
import { ScaleControl } from "./MLAdapters/ScaleControl" ;
35
35
import { FullscreenControl } from "./MLAdapters/FullscreenControl" ;
@@ -187,6 +187,8 @@ export class Map extends maplibregl.Map {
187
187
private languageAlwaysBeenStyle : boolean ;
188
188
private isReady = false ;
189
189
private terrainAnimationDuration = 1000 ;
190
+ private monitoredStyleUrls ! : Set < string > ;
191
+ private styleInProcess = false ;
190
192
191
193
constructor ( options : MapOptions ) {
192
194
displayNoWebGlWarning ( options . container ) ;
@@ -195,13 +197,19 @@ export class Map extends maplibregl.Map {
195
197
config . apiKey = options . apiKey ;
196
198
}
197
199
198
- const style = styleToStyle ( options . style ) ;
199
- const hashPreConstructor = location . hash ;
200
+ const { style, requiresUrlMonitoring, isFallback } = styleToStyle ( options . style ) ;
201
+ if ( isFallback ) {
202
+ console . warn (
203
+ "Invalid style. A style must be a valid URL to a style.json, a JSON string representing a valid StyleSpecification or a valid StyleSpecification object. Fallback to default MapTiler style." ,
204
+ ) ;
205
+ }
200
206
201
207
if ( ! config . apiKey ) {
202
208
console . warn ( "MapTiler Cloud API key is not set. Visit https://maptiler.com and try Cloud for free!" ) ;
203
209
}
204
210
211
+ const hashPreConstructor = location . hash ;
212
+
205
213
// default attribution control options
206
214
let attributionControlOptions = {
207
215
compact : false ,
@@ -215,13 +223,71 @@ export class Map extends maplibregl.Map {
215
223
} ;
216
224
}
217
225
218
- // calling the map constructor with full length style
219
- super ( {
226
+ const superOptions = {
220
227
...options ,
221
228
style,
222
229
maplibreLogo : false ,
223
230
transformRequest : combineTransformRequest ( options . transformRequest ) ,
224
231
attributionControl : options . forceNoAttributionControl === true ? false : attributionControlOptions ,
232
+ } as maplibregl . MapOptions ;
233
+
234
+ // Removing the style option from the super constructor so that we can initialize this.styleInProcess before
235
+ // calling .setStyle(). Otherwise, if a style is provided to the super constructor, the setStyle method is called as
236
+ // a child call of super, meaning instance attributes cannot be initialized yet.
237
+ // The styleInProcess instance attribute is necessary to track if a style has not fall into a CORS error, for which
238
+ // Maplibre DOES NOT throw an AJAXError (hence does not track the URL of the failed http request)
239
+ // biome-ignore lint/performance/noDelete: <explanation>
240
+ delete superOptions . style ;
241
+ super ( superOptions ) ;
242
+ this . setStyle ( style ) ;
243
+
244
+ if ( requiresUrlMonitoring ) {
245
+ this . monitorStyleUrl ( style as string ) ;
246
+ }
247
+
248
+ const applyFallbackStyle = ( ) => {
249
+ let warning = "The distant style could not be loaded." ;
250
+ // Loading a new style failed. If a style is not already in place,
251
+ // the default one is loaded instead + warning in console
252
+ if ( ! this . getStyle ( ) ) {
253
+ this . setStyle ( MapStyle . STREETS ) ;
254
+ warning += `Loading default MapTiler Cloud style "${ MapStyle . STREETS . getDefaultVariant ( ) . getId ( ) } " as a fallback.` ;
255
+ } else {
256
+ warning += "Leaving the style as is." ;
257
+ }
258
+ console . warn ( warning ) ;
259
+ } ;
260
+
261
+ this . on ( "style.load" , ( ) => {
262
+ this . styleInProcess = false ;
263
+ } ) ;
264
+
265
+ // Safeguard for distant styles at non-http 2xx status URLs
266
+ this . on ( "error" , ( event ) => {
267
+ if ( event . error instanceof AJAXError ) {
268
+ const err = event . error as AJAXError ;
269
+ const url = err . url ;
270
+ const cleanUrl = new URL ( url ) ;
271
+ cleanUrl . search = "" ;
272
+ const clearnUrlStr = cleanUrl . href ;
273
+
274
+ // If the URL is present in the list of monitored style URL,
275
+ // that means this AJAXError was about a style, and we want to fallback to
276
+ // the default style
277
+ if ( this . monitoredStyleUrls . has ( clearnUrlStr ) ) {
278
+ this . monitoredStyleUrls . delete ( clearnUrlStr ) ;
279
+ applyFallbackStyle ( ) ;
280
+ }
281
+ return ;
282
+ }
283
+
284
+ // CORS error when fetching distant URL are not detected as AJAXError by Maplibre, just as generic error with no url property
285
+ // so we have to find a way to detect them when it comes to failing to load a style.
286
+ if ( this . styleInProcess ) {
287
+ // If this.styleInProcess is true, it very likely means the style URL has not resolved due to a CORS issue.
288
+ // In such case, we load the default style
289
+ return applyFallbackStyle ( ) ;
290
+ }
225
291
} ) ;
226
292
227
293
if ( config . caching && ! CACHE_API_AVAILABLE ) {
@@ -632,6 +698,20 @@ export class Map extends maplibregl.Map {
632
698
} ) ;
633
699
}
634
700
701
+ private monitorStyleUrl ( url : string ) {
702
+ // In case this was called before the super constructor could be called.
703
+ if ( typeof this . monitoredStyleUrls === "undefined" ) {
704
+ this . monitoredStyleUrls = new Set < string > ( ) ;
705
+ }
706
+
707
+ // Note: Because of the usage of urlToAbsoluteUrl() the URL of a style is always supposed to be absolute
708
+
709
+ // Removing all the URL params to make it easier to later identify in the set
710
+ const cleanUrl = new URL ( url ) ;
711
+ cleanUrl . search = "" ;
712
+ this . monitoredStyleUrls . add ( cleanUrl . href ) ;
713
+ }
714
+
635
715
/**
636
716
* Update the style of the map.
637
717
* Can be:
@@ -650,7 +730,30 @@ export class Map extends maplibregl.Map {
650
730
this . forceLanguageUpdate = false ;
651
731
} ) ;
652
732
653
- return super . setStyle ( styleToStyle ( style ) , options ) ;
733
+ const styleInfo = styleToStyle ( style ) ;
734
+
735
+ if ( styleInfo . requiresUrlMonitoring ) {
736
+ this . monitorStyleUrl ( styleInfo . style as string ) ;
737
+ }
738
+
739
+ // If the style is invalid and what is returned is a fallback + the map already has a style,
740
+ // the style remains unchanged.
741
+ if ( styleInfo . isFallback ) {
742
+ if ( this . getStyle ( ) ) {
743
+ console . warn (
744
+ "Invalid style. A style must be a valid URL to a style.json, a JSON string representing a valid StyleSpecification or a valid StyleSpecification object. Keeping the curent style instead." ,
745
+ ) ;
746
+ return this ;
747
+ }
748
+
749
+ console . warn (
750
+ "Invalid style. A style must be a valid URL to a style.json, a JSON string representing a valid StyleSpecification or a valid StyleSpecification object. Fallback to default MapTiler style." ,
751
+ ) ;
752
+ }
753
+
754
+ this . styleInProcess = true ;
755
+ super . setStyle ( styleInfo . style , options ) ;
756
+ return this ;
654
757
}
655
758
656
759
/**
0 commit comments