1- const geoPortailBaseURL = "https://data.geopf.fr/navigation/isochrone"
2-
3- // Simple token-bucket rate limiter to enforce max 5 requests per second
4- // Allows bursts up to `bucketSize` but refills `maxTokens` every `refillIntervalMs`.
5- class RateLimiter {
6- private maxTokens : number
7- private tokens : number
8- private refillIntervalMs : number
9- private queue : Array < ( ) => void >
10- private refillTimer : any
11-
12- constructor ( maxTokens = 5 , refillIntervalMs = 1000 ) {
13- this . maxTokens = maxTokens
14- this . tokens = maxTokens
15- this . refillIntervalMs = refillIntervalMs
16- this . queue = [ ]
17- this . refillTimer = setInterval ( ( ) => this . refill ( ) , this . refillIntervalMs )
18- }
19-
20- private refill ( ) {
21- this . tokens = this . maxTokens
22- this . drainQueue ( )
23- }
24-
25- private drainQueue ( ) {
26- while ( this . tokens > 0 && this . queue . length > 0 ) {
27- const job = this . queue . shift ( )
28- if ( job ) {
29- this . tokens -= 1
30- job ( )
31- }
32- }
33- }
34-
35- // schedule returns a promise resolved by running `fn` when token available
36- public schedule < T > ( fn : ( ) => Promise < T > , signal ?: AbortSignal ) : Promise < T > {
37- return new Promise < T > ( ( resolve , reject ) => {
38- if ( signal ?. aborted ) {
39- return reject ( new DOMException ( 'Aborted' , 'AbortError' ) )
40- }
41-
42- const run = ( ) => {
43- if ( signal ?. aborted ) {
44- return reject ( new DOMException ( 'Aborted' , 'AbortError' ) )
45- }
46-
47- fn ( ) . then ( resolve ) . catch ( reject )
48- }
49-
50- // If token available, consume and run immediately
51- if ( this . tokens > 0 ) {
52- this . tokens -= 1
53- run ( )
54- } else {
55- // otherwise enqueue and attach abort listener to remove if aborted
56- const wrappedJob = ( ) => run ( )
57- this . queue . push ( wrappedJob )
58-
59- const onAbort = ( ) => {
60- // try to remove from queue
61- const idx = this . queue . indexOf ( wrappedJob )
62- if ( idx !== - 1 ) this . queue . splice ( idx , 1 )
63- signal ?. removeEventListener ( 'abort' , onAbort )
64- reject ( new DOMException ( 'Aborted' , 'AbortError' ) )
65- }
66-
67- signal ?. addEventListener ( 'abort' , onAbort )
68- }
69- } )
70- }
71-
72- public stop ( ) {
73- clearInterval ( this . refillTimer )
74- this . queue = [ ]
75- }
76- }
77-
78- const isochroneRateLimiter = new RateLimiter ( 5 , 1000 )
79-
80- // Helper: perform fetch through rate limiter and retry on failures
81- const fetchWithRetries = ( url : string , signal ?: AbortSignal , maxRetries = 3 ) : Promise < any > => {
82- let attempt = 0
83-
84- return new Promise ( ( resolve , reject ) => {
85- const tryOnce = ( ) => {
86- if ( signal ?. aborted ) {
87- return reject ( new DOMException ( 'Aborted' , 'AbortError' ) )
88- }
89-
90- attempt += 1
91-
92- isochroneRateLimiter . schedule ( async ( ) => {
93- const response = await fetch ( url , { signal } )
94- if ( ! response . ok ) {
95- throw new Error ( `HTTP ${ response . status } ` )
96- }
97- const data = await response . json ( )
98- return data
99- } , signal ) . then ( resolve ) . catch ( ( err ) => {
100- // If aborted, reject immediately
101- if ( signal ?. aborted ) {
102- return reject ( new DOMException ( 'Aborted' , 'AbortError' ) )
103- }
104-
105- if ( attempt <= maxRetries ) {
106- const delay = 500 * Math . pow ( 2 , attempt - 1 ) // 500ms, 1s, 2s, ...
107- setTimeout ( ( ) => {
108- tryOnce ( )
109- } , delay )
110- } else {
111- reject ( err )
112- }
113- } )
114- }
115-
116- tryOnce ( )
117- } )
118- }
119-
120- export const fetchIsochroneData = async ( lat : number , lon : number , time : number = 1800 , transportMode : string , signal ?: AbortSignal ) => {
121- if ( lat < - 63.28125 || lat > 55.8984375 || lon < - 63.28125 || lon > 55.8984375 ) {
122- return null ;
123- }
124-
125- const url = geoPortailBaseURL + `?gp-access-lib=3.4.2&resource=bdtopo-valhalla&point=${ lon } ,${ lat } &direction=departure&costType=time&costValue=${ time } &profile=${ transportMode } &timeUnit=second&distanceUnit=meter&crs=EPSG:4326&constraints=`
126-
127- // Use the fetch-with-retries helper so failures are retried (re-queued) a few times
128- const data = await fetchWithRetries ( url , signal , 3 )
129- return formatIsochrone ( data )
130- }
131-
1321const dataGouvBaseURL = "https://data.enseignementsup-recherche.gouv.fr"
2+
1333const etablissements = ( offset : number ) =>
1344 dataGouvBaseURL +
1355 `/api/explore/v2.1/catalog/datasets/fr-esr-principaux-etablissements-enseignement-superieur/records?order_by=uai&select=coordonnees%2Ctype_d_etablissement%2Cuo_lib&limit=100&offset=${ offset } &lang=fr&refine=pays_etranger_acheminement%3A%22France%22`
@@ -140,15 +10,6 @@ const fetchEtablissementsData = async (offset: number) => {
14010 return data
14111}
14212
143- const formatIsochrone = ( isochroneData : any ) => {
144- if ( ! isochroneData ) {
145- return [ ]
146- }
147-
148- return isochroneData ?. geometry ?. coordinates [ 0 ]
149- . map ( ( coord : any ) => [ coord [ 1 ] , coord [ 0 ] ] )
150- }
151-
15213export const fetchAllEtablissementsData = async ( ) => {
15314 let offset = 0
15415 let etablissements = [ ]
0 commit comments