/** * Sites route handler * Handles site search and nearby sites queries */ const https = require('https'); /** * Normalize site data from API response to consistent format * @param {Object} site - Raw site data from API * @returns {Object} - Normalized site object */ function normalizeSite(site) { return { 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 }; } /** * Parse sites from API response (handles multiple response formats) * @param {Object|Array} parsedData - Parsed JSON data from API * @returns {Array} - Array of normalized sites */ function parseSitesFromResponse(parsedData) { let sites = []; if (Array.isArray(parsedData)) { sites = parsedData.map(normalizeSite); } else if (parsedData.sites && Array.isArray(parsedData.sites)) { sites = parsedData.sites.map(normalizeSite); } else if (parsedData.ResponseData && parsedData.ResponseData.Result) { sites = parsedData.ResponseData.Result.map(normalizeSite); } return sites; } /** * Handle site search endpoint * @param {http.IncomingMessage} req - HTTP request object * @param {http.ServerResponse} res - HTTP response object * @param {url.UrlWithParsedQuery} parsedUrl - Parsed URL object */ function handleSiteSearch(req, res, parsedUrl) { 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 = ''; 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)); const sites = parseSitesFromResponse(parsedData); 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: [] })); }); } /** * Calculate distance between two coordinates (simple approximation) * @param {number} lat1 - Latitude of point 1 * @param {number} lon1 - Longitude of point 1 * @param {number} lat2 - Latitude of point 2 * @param {number} lon2 - Longitude of point 2 * @returns {number} - Distance in meters */ function calculateDistance(lat1, lon1, lat2, lon2) { return Math.sqrt( Math.pow((lat1 - lat2) * 111000, 2) + Math.pow((lon1 - lon2) * 111000 * Math.cos(lat1 * Math.PI / 180), 2) ); } /** * Handle nearby sites endpoint * @param {http.IncomingMessage} req - HTTP request object * @param {http.ServerResponse} res - HTTP response object * @param {url.UrlWithParsedQuery} parsedUrl - Parsed URL object */ function handleNearbySites(req, res, parsedUrl) { 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 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); const sites = parseSitesFromResponse(parsedData); sites.forEach(site => { if (site.lat && site.lon) { const distance = calculateDistance(lat, lon, site.lat, site.lon); if (distance <= radius) { allSites.push(site); } } }); 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 })); } }); }); } module.exports = { handleSiteSearch, handleNearbySites, normalizeSite, parseSitesFromResponse };