@@ -9,6 +9,16 @@ function nextCyclOSMSubdomain(): string {
99 return sub ;
1010}
1111
12+ const THUNDERFOREST_SUBDOMAINS = [ "a" , "b" , "c" ] as const ;
13+ let thunderforestSubdomainIndex = 0 ;
14+
15+ function nextThunderforestSubdomain ( ) : string {
16+ const sub =
17+ THUNDERFOREST_SUBDOMAINS [ thunderforestSubdomainIndex % THUNDERFOREST_SUBDOMAINS . length ] ;
18+ thunderforestSubdomainIndex ++ ;
19+ return sub ;
20+ }
21+
1222const TILE_PARAMS_SCHEMA = {
1323 type : "object" as const ,
1424 required : [ "z" , "x" , "y" ] ,
@@ -56,11 +66,11 @@ export const tilesRoute: FastifyPluginAsync = async (fastify) => {
5666 headers : {
5767 "User-Agent" : "OpenMapX/1.0 (+https://openmapx.org)" ,
5868 } ,
59- signal : AbortSignal . timeout ( 15_000 ) ,
69+ signal : AbortSignal . timeout ( 10_000 ) ,
6070 } ) ;
6171
6272 if ( ! response . ok ) {
63- return reply . status ( response . status ) . send ( { message : "Upstream tile fetch failed" } ) ;
73+ throw new Error ( `CyclOSM returned ${ response . status } ` ) ;
6474 }
6575
6676 const buffer = Buffer . from ( await response . arrayBuffer ( ) ) ;
@@ -69,7 +79,35 @@ export const tilesRoute: FastifyPluginAsync = async (fastify) => {
6979 return reply . send ( buffer ) ;
7080 } catch ( error ) {
7181 req . log . warn ( { err : error , z, x, y } , "CyclOSM tile fetch failed" ) ;
72- return reply . status ( 502 ) . send ( { message : "CyclOSM tile provider unavailable" } ) ;
82+
83+ const thunderforestKey = process . env . THUNDERFOREST_API_KEY ;
84+ if ( ! thunderforestKey ) {
85+ return reply . status ( 502 ) . send ( { message : "CyclOSM tile provider unavailable" } ) ;
86+ }
87+
88+ const tfSub = nextThunderforestSubdomain ( ) ;
89+ const tfUrl = `https://${ tfSub } .tile.thunderforest.com/cycle/${ z } /${ x } /${ y } .png?apikey=${ thunderforestKey } ` ;
90+
91+ try {
92+ const tfResponse = await fetch ( tfUrl , {
93+ headers : {
94+ "User-Agent" : "OpenMapX/1.0 (+https://openmapx.org)" ,
95+ } ,
96+ signal : AbortSignal . timeout ( 10_000 ) ,
97+ } ) ;
98+
99+ if ( ! tfResponse . ok ) {
100+ return reply . status ( tfResponse . status ) . send ( { message : "Fallback tile fetch failed" } ) ;
101+ }
102+
103+ const tfBuffer = Buffer . from ( await tfResponse . arrayBuffer ( ) ) ;
104+ reply . header ( "Cache-Control" , "public, max-age=604800, s-maxage=604800" ) ;
105+ reply . type ( "image/png" ) ;
106+ return reply . send ( tfBuffer ) ;
107+ } catch ( tfError ) {
108+ req . log . warn ( { err : tfError , z, x, y } , "Thunderforest fallback tile fetch failed" ) ;
109+ return reply . status ( 502 ) . send ( { message : "All cycling tile providers unavailable" } ) ;
110+ }
73111 }
74112 } ,
75113 } ) ;
0 commit comments