450 lines
16 KiB
JavaScript
450 lines
16 KiB
JavaScript
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}/`);
|
|
});
|