Refactor: Complete codebase reorganization and modernization
- Split server.js routes into modular files (server/routes/) - departures.js: Departure data endpoints - sites.js: Site search and nearby sites - config.js: Configuration endpoints - Reorganized file structure following Node.js best practices: - Moved sites-config.json to config/sites.json - Moved API_RESPONSE_DOCUMENTATION.md to docs/ - Moved raspberry-pi-setup.sh to scripts/ - Archived legacy files to archive/ directory - Updated all code references to new file locations - Added archive/ to .gitignore to exclude legacy files from repo - Updated README.md with new structure and organization - All functionality tested and working correctly Version: 1.2.0
This commit is contained in:
78
server/routes/config.js
Normal file
78
server/routes/config.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Config route handler
|
||||
* Handles configuration get/update and client-side config
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Handle GET /api/config endpoint
|
||||
* @param {http.IncomingMessage} req - HTTP request object
|
||||
* @param {http.ServerResponse} res - HTTP response object
|
||||
* @param {Object} config - Application configuration
|
||||
*/
|
||||
function handleGetConfig(req, res, config) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /api/config/update endpoint
|
||||
* @param {http.IncomingMessage} req - HTTP request object
|
||||
* @param {http.ServerResponse} res - HTTP response object
|
||||
* @param {Object} config - Application configuration (will be modified)
|
||||
*/
|
||||
function handleUpdateConfig(req, res, config) {
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const newConfig = JSON.parse(body);
|
||||
if (newConfig.sites) {
|
||||
// Update config object (passed by reference)
|
||||
Object.assign(config, newConfig);
|
||||
|
||||
// Save to file
|
||||
const configPath = path.join('config', 'sites.json');
|
||||
fs.writeFileSync(configPath, 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 }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /api/config/client endpoint
|
||||
* Returns client-side configuration (API keys, default location)
|
||||
* @param {http.IncomingMessage} req - HTTP request object
|
||||
* @param {http.ServerResponse} res - HTTP response object
|
||||
*/
|
||||
function handleClientConfig(req, res) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
openweathermapApiKey: process.env.OPENWEATHERMAP_API_KEY || '',
|
||||
defaultLocation: {
|
||||
latitude: parseFloat(process.env.DEFAULT_LATITUDE) || 59.3293,
|
||||
longitude: parseFloat(process.env.DEFAULT_LONGITUDE) || 18.0686,
|
||||
name: process.env.DEFAULT_LOCATION_NAME || 'Stockholm'
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleGetConfig,
|
||||
handleUpdateConfig,
|
||||
handleClientConfig
|
||||
};
|
||||
152
server/routes/departures.js
Normal file
152
server/routes/departures.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Departures route handler
|
||||
* Handles fetching and returning transit departure data
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
/**
|
||||
* Fetch departures for a specific site from SL Transport API
|
||||
* @param {string} siteId - The site ID to fetch departures for
|
||||
* @returns {Promise<Object>} - Departure data
|
||||
*/
|
||||
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) {
|
||||
console.log('Sample departure structure:', JSON.stringify(parsedData.departures[0], null, 2));
|
||||
|
||||
const sample = parsedData.departures[0];
|
||||
console.log('Direction fields:', {
|
||||
direction: sample.direction,
|
||||
directionText: sample.directionText,
|
||||
directionCode: sample.directionCode,
|
||||
destination: sample.destination
|
||||
});
|
||||
}
|
||||
|
||||
resolve(parsedData);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse even after fixing:', parseError);
|
||||
// Return empty departures array instead of rejecting to be more resilient
|
||||
resolve({
|
||||
departures: [],
|
||||
error: 'Failed to parse API response: ' + parseError.message
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing API response:', error);
|
||||
// Return empty departures array instead of rejecting to be more resilient
|
||||
resolve({
|
||||
departures: [],
|
||||
error: 'Error processing API response: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}).on('error', (error) => {
|
||||
console.error('Error fetching departures:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch departures for all enabled sites
|
||||
* @param {Array} enabledSites - Array of enabled site configurations
|
||||
* @returns {Promise<Object>} - Object with sites array containing departure data
|
||||
*/
|
||||
async function fetchAllDepartures(enabledSites) {
|
||||
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 departures API endpoint
|
||||
* @param {http.IncomingMessage} req - HTTP request object
|
||||
* @param {http.ServerResponse} res - HTTP response object
|
||||
* @param {Object} config - Application configuration
|
||||
*/
|
||||
async function handleDepartures(req, res, config) {
|
||||
try {
|
||||
const enabledSites = config.sites.filter(site => site.enabled);
|
||||
const data = await fetchAllDepartures(enabledSites);
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleDepartures,
|
||||
fetchDeparturesForSite,
|
||||
fetchAllDepartures
|
||||
};
|
||||
198
server/routes/sites.js
Normal file
198
server/routes/sites.js
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 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<Object>} - 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
|
||||
};
|
||||
Reference in New Issue
Block a user