const http = require('http'); const https = require('https'); const url = require('url'); const PORT = 3002; // Default configuration let config = { sites: [ { id: '1411', name: 'Ambassaderna', enabled: true } ] }; // Function to load configuration from file function loadSitesConfig() { try { const fs = require('fs'); if (fs.existsSync('sites-config.json')) { const configData = fs.readFileSync('sites-config.json', 'utf8'); const loadedConfig = JSON.parse(configData); // Handle old format (array of sites) if (Array.isArray(loadedConfig)) { config.sites = loadedConfig; } else { config = loadedConfig; } console.log('Loaded configuration:', config); } } catch (error) { console.error('Error loading configuration:', error); } } // Load configuration on startup loadSitesConfig(); // Function to fetch data from the SL Transport API for a specific site function fetchDeparturesForSite(siteId) { return new Promise((resolve, reject) => { const apiUrl = `https://transport.integration.sl.se/v1/sites/${siteId}/departures`; console.log(`Fetching data from: ${apiUrl}`); https.get(apiUrl, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { console.log('Raw API response:', data.substring(0, 200) + '...'); try { try { const parsedData = JSON.parse(data); console.log('Successfully parsed as regular JSON'); resolve(parsedData); return; } catch (jsonError) { console.log('Not valid JSON, trying to fix format...'); } if (data.startsWith('departures":')) { data = '{' + data; } else if (data.includes('departures":')) { const startIndex = data.indexOf('departures":'); if (startIndex > 0) { data = '{' + data.substring(startIndex); } } data = data.replace(/}{\s*"/g, '},{"'); data = data.replace(/"([^"]+)":\s*([^,{}\[\]]+)(?=")/g, '"$1": $2,'); data = data.replace(/,\s*}/g, '}').replace(/,\s*\]/g, ']'); try { const parsedData = JSON.parse(data); console.log('Successfully parsed fixed JSON'); if (parsedData && parsedData.departures && parsedData.departures.length > 0) { // Log first departure to see structure console.log('Sample departure structure:', JSON.stringify(parsedData.departures[0], null, 2)); // Check for direction-related fields const sample = parsedData.departures[0]; console.log('Direction fields:', { direction: sample.direction, directionText: sample.directionText, directionCode: sample.directionCode, journeyDirection: sample.journey?.direction, stopPoint: sample.stopPoint }); } resolve(parsedData); } catch (parseError) { console.error('Error parsing fixed JSON:', parseError); resolve({ departures: [], error: 'Failed to parse API response: ' + parseError.message, rawResponse: data.substring(0, 500) + '...' }); } } catch (error) { console.error('Error processing API response:', error); resolve({ departures: [], error: 'Error processing API response: ' + error.message, rawResponse: data.substring(0, 500) + '...' }); } }); }).on('error', (error) => { console.error('Error fetching data from API:', error); resolve({ departures: [], error: 'Error fetching data from API: ' + error.message }); }); }); } // Create HTTP server const server = http.createServer(async (req, res) => { const parsedUrl = url.parse(req.url, true); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // Function to fetch data from all enabled sites async function fetchAllDepartures() { const enabledSites = config.sites.filter(site => site.enabled); if (enabledSites.length === 0) { return { sites: [], error: 'No enabled sites configured' }; } try { const sitesPromises = enabledSites.map(async (site) => { try { const departureData = await fetchDeparturesForSite(site.id); return { siteId: site.id, siteName: site.name, data: departureData }; } catch (error) { console.error(`Error fetching departures for site ${site.id}:`, error); return { siteId: site.id, siteName: site.name, error: error.message }; } }); const results = await Promise.all(sitesPromises); return { sites: results }; } catch (error) { console.error('Error fetching all departures:', error); return { sites: [], error: error.message }; } } // Handle API endpoints if (parsedUrl.pathname === '/api/departures') { try { const data = await fetchAllDepartures(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); } catch (error) { console.error('Error handling departures request:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } } else if (parsedUrl.pathname === '/api/sites/search') { // Search for transit sites const query = parsedUrl.query.q; if (!query || query.length < 2) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Query must be at least 2 characters' })); return; } const searchUrl = `https://transport.integration.sl.se/v1/sites?q=${encodeURIComponent(query)}`; console.log(`Searching sites: ${searchUrl}`); https.get(searchUrl, (apiRes) => { let data = ''; // Check for HTTP errors if (apiRes.statusCode < 200 || apiRes.statusCode >= 300) { console.error(`API returned status code: ${apiRes.statusCode}`); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `API returned status ${apiRes.statusCode}`, sites: [] })); return; } apiRes.on('data', (chunk) => { data += chunk; }); apiRes.on('end', () => { try { console.log('Raw API response:', data.substring(0, 500)); const parsedData = JSON.parse(data); console.log('Parsed data:', JSON.stringify(parsedData).substring(0, 500)); // Handle different possible response formats let sites = []; if (Array.isArray(parsedData)) { // Response is directly an array sites = parsedData.map(site => ({ id: String(site.id || site.siteId || site.SiteId || ''), name: site.name || site.siteName || site.Name || site.StopPointName || 'Unknown', lat: site.lat || site.latitude || site.Lat || site.Latitude || null, lon: site.lon || site.longitude || site.Lon || site.Longitude || null })); } else if (parsedData.sites && Array.isArray(parsedData.sites)) { // Response has a sites property sites = parsedData.sites.map(site => ({ id: String(site.id || site.siteId || site.SiteId || ''), name: site.name || site.siteName || site.Name || site.StopPointName || 'Unknown', lat: site.lat || site.latitude || site.Lat || site.Latitude || null, lon: site.lon || site.longitude || site.Lon || site.Longitude || null })); } else if (parsedData.ResponseData && parsedData.ResponseData.Result) { // SL API format with ResponseData sites = parsedData.ResponseData.Result.map(site => ({ id: String(site.SiteId || site.id || ''), name: site.Name || site.name || site.StopPointName || 'Unknown', lat: site.Lat || site.lat || site.Latitude || site.latitude || null, lon: site.Lon || site.lon || site.Longitude || site.longitude || null })); } else { console.log('Unexpected response format:', parsedData); sites = []; } // Log sample site to see structure if (sites.length > 0) { console.log('Sample site structure:', JSON.stringify(sites[0], null, 2)); const sitesWithCoords = sites.filter(s => s.lat && s.lon); console.log(`Found ${sites.length} sites, ${sitesWithCoords.length} with coordinates`); } else { console.log('No sites found'); } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ sites })); } catch (error) { console.error('Error parsing site search response:', error); console.error('Response data:', data.substring(0, 500)); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Error parsing search results', details: error.message, sites: [] })); } }); }).on('error', (error) => { console.error('Error searching sites:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Error searching sites', details: error.message, sites: [] })); }); } else if (parsedUrl.pathname === '/api/sites/nearby') { // Get nearby transit sites based on coordinates const lat = parseFloat(parsedUrl.query.lat); const lon = parseFloat(parsedUrl.query.lon); const radius = parseInt(parsedUrl.query.radius) || 5000; // Default 5km radius if (isNaN(lat) || isNaN(lon)) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid latitude or longitude', sites: [] })); return; } // Use a broader search to get sites, then filter by distance // For now, we'll search for common Stockholm area terms and filter const searchTerms = ['Stockholm', 'T-Centralen', 'Gamla Stan', 'Södermalm']; const allSites = []; let completedSearches = 0; searchTerms.forEach(term => { const searchUrl = `https://transport.integration.sl.se/v1/sites?q=${encodeURIComponent(term)}`; https.get(searchUrl, (apiRes) => { let data = ''; apiRes.on('data', (chunk) => { data += chunk; }); apiRes.on('end', () => { try { const parsedData = JSON.parse(data); let sites = []; if (Array.isArray(parsedData)) { sites = parsedData; } else if (parsedData.sites) { sites = parsedData.sites; } else if (parsedData.ResponseData && parsedData.ResponseData.Result) { sites = parsedData.ResponseData.Result; } sites.forEach(site => { const siteLat = site.lat || site.latitude || site.Lat || site.Latitude; const siteLon = site.lon || site.longitude || site.Lon || site.Longitude; if (siteLat && siteLon) { // Calculate distance (simple haversine approximation) const distance = Math.sqrt( Math.pow((lat - siteLat) * 111000, 2) + Math.pow((lon - siteLon) * 111000 * Math.cos(lat * Math.PI / 180), 2) ); if (distance <= radius) { allSites.push({ id: String(site.id || site.siteId || site.SiteId || ''), name: site.name || site.siteName || site.Name || site.StopPointName || 'Unknown', lat: siteLat, lon: siteLon }); } } }); completedSearches++; if (completedSearches === searchTerms.length) { // Remove duplicates const uniqueSites = Array.from(new Map(allSites.map(s => [s.id, s])).values()); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ sites: uniqueSites })); } } catch (error) { completedSearches++; if (completedSearches === searchTerms.length) { const uniqueSites = Array.from(new Map(allSites.map(s => [s.id, s])).values()); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ sites: uniqueSites })); } } }); }).on('error', () => { completedSearches++; if (completedSearches === searchTerms.length) { const uniqueSites = Array.from(new Map(allSites.map(s => [s.id, s])).values()); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ sites: uniqueSites })); } }); }); } else if (parsedUrl.pathname === '/api/config') { // Return the current configuration res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(config)); } else if (parsedUrl.pathname === '/api/config/update' && req.method === 'POST') { // Update configuration let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { try { const newConfig = JSON.parse(body); if (newConfig.sites) { config = newConfig; // Save to file const fs = require('fs'); fs.writeFileSync('sites-config.json', JSON.stringify(config, null, 2)); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Configuration updated' })); } else { throw new Error('Invalid configuration format'); } } catch (error) { console.error('Error updating configuration:', error); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: error.message })); } }); } // Serve static files else if (parsedUrl.pathname === '/' || parsedUrl.pathname === '/index.html') { const fs = require('fs'); fs.readFile('index.html', (err, data) => { if (err) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error loading index.html'); return; } res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(data); }); } else if (/\.(js|css|png|jpg|jpeg|gif|ico)$/.test(parsedUrl.pathname)) { const fs = require('fs'); const filePath = parsedUrl.pathname.substring(1); fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('File not found'); return; } const ext = parsedUrl.pathname.split('.').pop(); const contentType = { 'js': 'text/javascript', 'css': 'text/css', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'ico': 'image/x-icon' }[ext] || 'text/plain'; res.writeHead(200, { 'Content-Type': contentType }); res.end(data); }); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); // Start the server server.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}/`); });