const axios = require('axios'); const fs = require('fs'); const csv = require('csv-parser'); const polylineLib = require('@mapbox/polyline'); require('dotenv').config({ path: '.env' }); function loadTollStations() { return new Promise((resolve) => { const csvPath = './travel/gares_peage_export.csv'; const results = []; fs.createReadStream(csvPath) .pipe(csv()) .on('data', (row) => { if (row.id_gare && row.lat && row.lon) { results.push({ id: row.id_gare, operatorId: row.id_gare.substring(0, 2), tollId: row.id_gare.substring(2, 5), name: row.nom || '', lat: parseFloat(row.lat), lon: parseFloat(row.lon), }); } }) .on('end', () => resolve(results)); }); } function _distKm(lat1, lng1, lat2, lng2) { const dLat = (lat2 - lat1) * Math.PI / 180; const dLng = (lng2 - lng1) * Math.PI / 180; const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLng/2)**2; return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); } async function getUlysRate(vehicleCategory, passages) { try { const payload = { vehicleCategory: String(vehicleCategory), paymentOption: 2, tollPassages: passages.map((p) => ({ toll: { operatorId: p.operatorId, tollId: p.tollId }, passageDate: new Date().toISOString(), })), }; const res = await axios.post('https://api-ulys.azure-api.net/tollstation/v1/rate', payload); const data = res.data; if (Array.isArray(data) && data.length > 0) { if (passages.length === 2) { if (data.length !== 1 || !data[0].exitToll) return null; return data[0].price > 0 ? data[0].price : null; } else { if (data.length === 1 && data[0].price > 0) return data[0].price; const total = data.reduce((sum, d) => sum + (d.price || 0), 0); return total > 0 ? total : null; } } return null; } catch (e) { return null; } } async function test() { const origin = "25 Impasse du Puits du Suc, Saint-Martin-en-Haut, France"; const destination = "Nice, France"; const apiKey = process.env.API_MAPS; const routesUrl = 'https://routes.googleapis.com/directions/v2:computeRoutes'; const res = await axios.post(routesUrl, { travelMode: 'DRIVE', routingPreference: 'TRAFFIC_UNAWARE', origin: { address: origin }, destination: { address: destination }, }, { headers: { 'Content-Type': 'application/json', 'X-Goog-Api-Key': apiKey, 'X-Goog-FieldMask': 'routes.polyline.encodedPolyline' }}); const poly = res.data.routes[0].polyline.encodedPolyline; const coords = polylineLib.decode(poly, 5); const safePolyline = poly; const url = 'https://api-ulys.azure-api.net/placemark/v2/legs?precision=5&includeLayersIds=GaresPeage'; const ulysRes = await axios.post(url, JSON.stringify(safePolyline), { headers: { 'Content-Type': 'application/json' } }); const items = Array.isArray(ulysRes.data) ? ulysRes.data : (ulysRes.data.features || []); const stations = await loadTollStations(); const gates = []; for (const item of items) { const pm = item.Placemark || item.placemark || {}; const tags = pm.Tags || pm.tags || {}; let idStr = tags.ID_PEAGE; if (!idStr && pm.Code) idStr = pm.Code.split('_')[0]; const s = stations.find(s => s.id === idStr); if (s && !gates.find(g => g.id === idStr)) gates.push(s); } // Fallback for missing first system let missingSystemPrice = 0; if (gates.length > 0) { const originLat = coords[0][0]; const originLng = coords[0][1]; const firstGate = gates[0]; const distToFirstGate = _distKm(originLat, originLng, firstGate.lat, firstGate.lon); if (distToFirstGate > 50) { console.log(`First gate ${firstGate.name} is ${Math.round(distToFirstGate)}km from origin. Checking for missing system...`); // Find all geometric gates within 2km of the route, UP TO the firstGate let firstGateIndex = 0; for (let i = 0; i < coords.length; i++) { if (_distKm(coords[i][0], coords[i][1], firstGate.lat, firstGate.lon) < 1) { firstGateIndex = i; break; } } const candidates = []; stations.forEach(s => { let minDist = Infinity; let minIndex = -1; for (let i = 0; i < firstGateIndex; i++) { const d = _distKm(s.lat, s.lon, coords[i][0], coords[i][1]); if (d < minDist) { minDist = d; minIndex = i; } } if (minDist < 2) { candidates.push({ ...s, polyIndex: minIndex }); } }); candidates.sort((a, b) => a.polyIndex - b.polyIndex); if (candidates.length >= 2) { // Try combinations from furthest to closest to find the longest closed system let found = false; for (let i = 0; i < Math.min(10, candidates.length); i++) { for (let j = candidates.length - 1; j > i && j > candidates.length - 10; j--) { if (candidates[i].operatorId !== candidates[j].operatorId) continue; const price = await getUlysRate(2, [candidates[i], candidates[j]]); if (price) { console.log(`Found missing system: ${candidates[i].name} -> ${candidates[j].name} = ${price}€`); missingSystemPrice += price; found = true; break; } } if (found) break; } } } } console.log(`Missing system price: ${missingSystemPrice}€`); } test();