Initial upload
This commit is contained in:
72
main.js
Normal file
72
main.js
Normal file
@ -0,0 +1,72 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const NodeCache = require('node-cache');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 80;
|
||||
|
||||
const stopCache = new NodeCache({ stdTTL: 86400 }); // 1 day
|
||||
|
||||
// Create HTTP server and attach Socket.IO
|
||||
const server = http.createServer(app);
|
||||
const io = socketIo(server);
|
||||
|
||||
async function fetchStops() {
|
||||
try {
|
||||
const { data } = await axios.get('https://elron.ee/stops_data.json');
|
||||
stopCache.set('stops', data.data || []);
|
||||
} catch (err) {
|
||||
console.error('Error fetching stops:', err);
|
||||
}
|
||||
}
|
||||
|
||||
fetchStops();
|
||||
setInterval(fetchStops, 86400 * 1000);
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static('public'));
|
||||
|
||||
app.get('/api/trip/:id', async (req, res) => {
|
||||
const tripId = req.params.id;
|
||||
const response = await fetch(`https://elron.ee/live-map/trip/${tripId}`, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const data = await response.text(); // if response is not pure JSON
|
||||
res.set('Access-Control-Allow-Origin', '*');
|
||||
res.send(data);
|
||||
});
|
||||
|
||||
// Socket.IO connection
|
||||
io.on('connection', (socket) => {
|
||||
console.log('New client connected');
|
||||
|
||||
// Emit stops data to the client
|
||||
socket.emit('stops', stopCache.get('stops') || []);
|
||||
|
||||
// Emit vehicles data to the client every 2 seconds
|
||||
const vehicleInterval = setInterval(async () => {
|
||||
const vehicles = await fetchVehicles();
|
||||
socket.emit('vehicles', vehicles);
|
||||
}, 2000);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Client disconnected');
|
||||
clearInterval(vehicleInterval);
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch vehicles function
|
||||
async function fetchVehicles() {
|
||||
try {
|
||||
const { data } = await axios.get('https://elron.ee/map_data.json');
|
||||
return data.data || [];
|
||||
} catch (err) {
|
||||
console.error('Error fetching vehicle data:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server
|
||||
server.listen(PORT, () => console.log(`Server running on ${PORT}`));
|
1306
package-lock.json
generated
Normal file
1306
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "elron-gps-tracker",
|
||||
"version": "1.0.0",
|
||||
"author": "VELENDEU, eetnaviation",
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.velend.eu/VELENDEU/elron-gps-tracker",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "node main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"express": "^5.1.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
}
|
BIN
public/elron_normal.png
Normal file
BIN
public/elron_normal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
BIN
public/elron_special.png
Normal file
BIN
public/elron_special.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
176
public/index.html
Normal file
176
public/index.html
Normal file
@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Elron GPS Tracker</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
||||
<style>
|
||||
html, body, #map { height: 100%; margin: 0; padding: 0; }
|
||||
#header { text-align: center; background: #f5f5f5; padding: 10px 0; }
|
||||
#search-container { position: absolute; top: 10px; left: 10px; z-index: 1000; padding: 10px; border-radius: 5px; background: white; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1>Elron GPS Tracker</h1>
|
||||
</div>
|
||||
|
||||
<div id="search-container">
|
||||
<div class="search-field">
|
||||
<input type="text" id="line-search" placeholder="Search for line..." />
|
||||
<button id="line-search-button" class="search-button">Search Line</button>
|
||||
</div>
|
||||
<div class="search-field">
|
||||
<input type="text" id="stop-search" placeholder="Search for stop..." />
|
||||
<button id="stop-search-button" class="search-button">Search Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-rotatedmarker/leaflet.rotatedMarker.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const map = L.map('map').setView([58.3776, 26.729], 7);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
|
||||
|
||||
const stopsLayer = L.layerGroup().addTo(map);
|
||||
const trainsLayer = L.layerGroup().addTo(map);
|
||||
|
||||
const normalIcon = L.icon({ iconUrl: 'elron_normal.png', iconSize: [32, 32] });
|
||||
const specialIcon = L.icon({ iconUrl: 'elron_special.png', iconSize: [32, 32] });
|
||||
const stopIcon = L.icon({ iconUrl: 'stop_marker.png', iconSize: [20, 20] });
|
||||
const stopIconSpecial = L.icon({ iconUrl: 'stop_marker_special.png', iconSize: [20, 20] });
|
||||
|
||||
const trainMarkers = {};
|
||||
const stopMarkers = {};
|
||||
const socket = io();
|
||||
|
||||
socket.on('stops', stops => {
|
||||
const seenStops = new Set();
|
||||
stops.forEach(stop => {
|
||||
seenStops.add(stop.peatus);
|
||||
const icon = stop.teade ? stopIconSpecial : stopIcon;
|
||||
|
||||
if (stopMarkers[stop.peatus]) {
|
||||
// Update existing marker
|
||||
const marker = stopMarkers[stop.peatus];
|
||||
marker.setLatLng([+stop.latitude, +stop.longitude]);
|
||||
marker.setIcon(icon);
|
||||
marker.setPopupContent(generateStopPopup(stop));
|
||||
} else {
|
||||
// Create new marker
|
||||
const marker = L.marker([+stop.latitude, +stop.longitude], { icon })
|
||||
.bindPopup(generateStopPopup(stop))
|
||||
.addTo(stopsLayer);
|
||||
stopMarkers[stop.peatus] = marker;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove stops not present anymore
|
||||
Object.keys(stopMarkers).forEach(id => {
|
||||
if (!seenStops.has(id)) {
|
||||
stopsLayer.removeLayer(stopMarkers[id]);
|
||||
delete stopMarkers[id];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('vehicles', vehicles => {
|
||||
const seenTrains = new Set();
|
||||
|
||||
vehicles.forEach(train => {
|
||||
if (!train.latitude || !train.longitude || !train.reis) return;
|
||||
seenTrains.add(train.reis);
|
||||
|
||||
const icon = (train.erinevus_plaanist || train.lisateade || train.pohjus_teade || train.reisi_staatus !== 'plaaniline') ? specialIcon : normalIcon;
|
||||
const position = [+train.latitude, +train.longitude];
|
||||
const rotation = +train.rongi_suund || 0;
|
||||
const popupContent = generateTrainPopup(train);
|
||||
|
||||
if (trainMarkers[train.reis]) {
|
||||
const marker = trainMarkers[train.reis];
|
||||
marker.setLatLng(position);
|
||||
marker.setRotationAngle(rotation);
|
||||
marker.setIcon(icon);
|
||||
marker.setPopupContent(popupContent);
|
||||
} else {
|
||||
const marker = L.marker(position, {
|
||||
icon,
|
||||
rotationAngle: rotation,
|
||||
rotationOrigin: 'center center'
|
||||
}).bindPopup(popupContent).addTo(trainsLayer);
|
||||
trainMarkers[train.reis] = marker;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove trains no longer present
|
||||
Object.keys(trainMarkers).forEach(id => {
|
||||
if (!seenTrains.has(id)) {
|
||||
trainsLayer.removeLayer(trainMarkers[id]);
|
||||
delete trainMarkers[id];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function generateTrainPopup(train) {
|
||||
return `
|
||||
<strong>${train.reis || ''}</strong><br>
|
||||
Liin: ${train.liin || ''}<br>
|
||||
Algus: ${train.reisi_algus_aeg || ''}<br>
|
||||
Lõpp: ${train.reisi_lopp_aeg || ''}<br>
|
||||
Kiirus: ${train.kiirus || ''} km/h<br>
|
||||
Viimane peatus: ${train.viimane_peatus || ''}<br>
|
||||
Uuendus: ${train.asukoha_uuendus || ''}<br>
|
||||
${train.erinevus_plaanist ? `Erinevus plaanist: ${train.erinevus_plaanist}<br>` : ''}
|
||||
${train.lisateade ? `Lisateade: ${train.lisateade}<br>` : ''}
|
||||
${train.pohjus_teade ? `Põhjus: ${train.pohjus_teade}<br>` : ''}
|
||||
${train.reisi_staatus !== 'plaaniline' ? `Staatus: ${train.reisi_staatus}<br>` : ''}
|
||||
<button onclick="loadTripDetails('${train.reis}')">View Trip Details</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function generateStopPopup(stop) {
|
||||
return `
|
||||
<strong>${stop.peatus || ''}</strong><br>
|
||||
${stop.teade ? `Teade: ${stop.teade}<br>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadTripDetails(tripId) {
|
||||
try {
|
||||
const res = await fetch(`/api/trip/${tripId}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 1 && data.data) {
|
||||
let tripDetails = '<strong>Trip Details:</strong><br><table><tr><th>Stop</th><th>Planned</th><th>Actual</th><th>Delay</th></tr>';
|
||||
data.data.forEach(stop => {
|
||||
const plannedTime = stop.plaaniline_aeg;
|
||||
const actualTime = stop.tegelik_aeg || 'N/A';
|
||||
const delay = actualTime !== 'N/A' ? calculateDelay(plannedTime, actualTime) : 'N/A';
|
||||
const delayStyle = (delay !== 'N/A' && delay > 5) ? ' style="color:red;"' : '';
|
||||
tripDetails += `<tr${delayStyle}><td>${stop.peatus}</td><td>${plannedTime}</td><td>${actualTime}</td><td>${delay !== 'N/A' ? delay + ' min' : delay}</td></tr>`;
|
||||
});
|
||||
tripDetails += '</table>';
|
||||
const detailWindow = window.open('', 'Trip Details', 'width=600,height=400');
|
||||
detailWindow.document.write(`<html><head><title>Trip Details</title></head><body>${tripDetails}</body></html>`);
|
||||
} else {
|
||||
alert('No trip details available.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching trip details:', err);
|
||||
alert('Error loading trip details.');
|
||||
}
|
||||
}
|
||||
|
||||
function calculateDelay(planned, actual) {
|
||||
const plannedTime = new Date(`1970-01-01T${planned}:00Z`).getTime();
|
||||
const actualTime = new Date(`1970-01-01T${actual}:00Z`).getTime();
|
||||
return Math.round((actualTime - plannedTime) / 60000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
174
public/indexorig.html
Normal file
174
public/indexorig.html
Normal file
@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Elron Gps Tracker</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css">
|
||||
<style>
|
||||
html, body, #map { height: 100%; margin: 0; padding: 0; }
|
||||
/*#header { text-align: center; background: #f5f5f5; padding: 10px 0; }
|
||||
#search-container { position: absolute; top: 10px; left: 10px; z-index: 1000; padding: 10px; border-radius: 5px; }*/
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Elron Gps Tracker</h1>
|
||||
<div id="search-container">
|
||||
<div class="search-field">
|
||||
<input type="text" id="line-search" placeholder="Search for line...">
|
||||
<button id="line-search-button" class="search-button">Search Line</button>
|
||||
</div>
|
||||
<div class="search-field">
|
||||
<input type="text" id="stop-search" placeholder="Search for stop...">
|
||||
<button id="stop-search-button" class="search-button">Search Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-rotatedmarker/leaflet.rotatedMarker.js"></script>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const map = L.map('map').setView([58.3776, 26.729], 7);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
|
||||
|
||||
const stopsLayer = L.layerGroup().addTo(map);
|
||||
const trainsLayer = L.layerGroup().addTo(map);
|
||||
|
||||
const normalIcon = L.icon({ iconUrl: 'elron_normal.png', iconSize: [32, 32] });
|
||||
const specialIcon = L.icon({ iconUrl: 'elron_special.png', iconSize: [32, 32] });
|
||||
const stopIcon = L.icon({ iconUrl: 'stop_marker.png', iconSize: [20, 20] });
|
||||
const stopIconSpecial = L.icon({ iconUrl: 'stop_marker_special.png', iconSize: [20, 20] });
|
||||
|
||||
const trainMarkers = {};
|
||||
const stopMarkers = {};
|
||||
const socket = io();
|
||||
|
||||
socket.on('stops', stops => {
|
||||
stopsLayer.clearLayers();
|
||||
stops.forEach(stop => {
|
||||
const icon = stop.teade ? stopIconSpecial : stopIcon;
|
||||
const marker = L.marker([+stop.latitude, +stop.longitude], { icon })
|
||||
.bindPopup(generateStopPopup(stop))
|
||||
.addTo(stopsLayer);
|
||||
stopMarkers[stop.peatus] = marker;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('vehicles', vehicles => {
|
||||
const seen = new Set();
|
||||
|
||||
vehicles.forEach(train => {
|
||||
if (!train.latitude || !train.longitude || !train.reis) return;
|
||||
seen.add(train.reis);
|
||||
|
||||
const icon = (train.erinevus_plaanist || train.lisateade || train.pohjus_teade || train.reisi_staatus !== 'plaaniline') ? specialIcon : normalIcon;
|
||||
const position = [+train.latitude, +train.longitude];
|
||||
const rotation = +train.rongi_suund || 0;
|
||||
const popupContent = generateTrainPopup(train);
|
||||
|
||||
if (trainMarkers[train.reis]) {
|
||||
const marker = trainMarkers[train.reis];
|
||||
marker.setLatLng(position);
|
||||
marker.setRotationAngle(rotation);
|
||||
marker.setIcon(icon);
|
||||
marker.bindPopup(popupContent);
|
||||
} else {
|
||||
const marker = L.marker(position, {
|
||||
icon,
|
||||
rotationAngle: rotation,
|
||||
rotationOrigin: 'center center'
|
||||
}).bindPopup(popupContent).addTo(trainsLayer);
|
||||
trainMarkers[train.reis] = marker;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(trainMarkers).forEach(id => {
|
||||
if (!seen.has(id)) {
|
||||
trainsLayer.removeLayer(trainMarkers[id]);
|
||||
delete trainMarkers[id];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function generateTrainPopup(train) {
|
||||
return `
|
||||
<strong>${train.reis || ''}</strong><br>
|
||||
Liin: ${train.liin || ''}<br>
|
||||
Algus: ${train.reisi_algus_aeg || ''}<br>
|
||||
Lõpp: ${train.reisi_lopp_aeg || ''}<br>
|
||||
Kiirus: ${train.kiirus || ''} km/h<br>
|
||||
Viimane peatus: ${train.viimane_peatus || ''}<br>
|
||||
Uuendus: ${train.asukoha_uuendus || ''}<br>
|
||||
${train.erinevus_plaanist ? `Erinevus plaanist: ${train.erinevus_plaanist}<br>` : ''}
|
||||
${train.lisateade ? `Lisateade: ${train.lisateade}<br>` : ''}
|
||||
${train.pohjus_teade ? `Põhjus: ${train.pohjus_teade}<br>` : ''}
|
||||
${train.reisi_staatus !== 'plaaniline' ? `Staatus: ${train.reisi_staatus}<br>` : ''}
|
||||
<button onclick="loadTripDetails('${train.reis}')">View Trip Details</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function generateStopPopup(stop) {
|
||||
return `
|
||||
<strong>${stop.peatus || ''}</strong><br>
|
||||
${stop.teade ? `Teade: ${stop.teade}<br>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadTripDetails(tripId) {
|
||||
try {
|
||||
const res = await fetch(`/api/trip/${tripId}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 1 && data.data) {
|
||||
let tripDetails = '<strong>Trip Details:</strong><br><table><tr><th>Stop</th><th>Planned</th><th>Actual</th><th>Delay</th></tr>';
|
||||
data.data.forEach(stop => {
|
||||
const plannedTime = stop.plaaniline_aeg;
|
||||
const actualTime = stop.tegelik_aeg || 'N/A';
|
||||
const delay = actualTime !== 'N/A' ? calculateDelay(plannedTime, actualTime) : 'N/A';
|
||||
const delayStyle = (delay !== 'N/A' && delay > 5) ? ' style="color:red;"' : '';
|
||||
tripDetails += `<tr${delayStyle}><td>${stop.peatus}</td><td>${plannedTime}</td><td>${actualTime}</td><td>${delay !== 'N/A' ? delay + ' min' : delay}</td></tr>`;
|
||||
});
|
||||
tripDetails += '</table>';
|
||||
const detailWindow = window.open('', 'Trip Details', 'width=600,height=400');
|
||||
detailWindow.document.write(`<html><head><title>Trip Details</title></head><body>${tripDetails}</body></html>`);
|
||||
} else {
|
||||
alert('No trip details available.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching trip details:', err);
|
||||
alert('Error loading trip details.');
|
||||
}
|
||||
}
|
||||
|
||||
function calculateDelay(planned, actual) {
|
||||
const plannedDate = new Date(`1970-01-01T${planned}:00Z`);
|
||||
const actualDate = new Date(`1970-01-01T${actual}:00Z`);
|
||||
return Math.round((actualDate - plannedDate) / 60000);
|
||||
}
|
||||
|
||||
document.getElementById('line-search-button').addEventListener('click', () => {
|
||||
const search = document.getElementById('line-search').value.toLowerCase();
|
||||
Object.values(trainMarkers).forEach(marker => {
|
||||
const popup = marker.getPopup().getContent();
|
||||
if (popup.toLowerCase().includes(search)) {
|
||||
marker.openPopup();
|
||||
map.panTo(marker.getLatLng());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('stop-search-button').addEventListener('click', () => {
|
||||
const search = document.getElementById('stop-search').value.toLowerCase();
|
||||
Object.entries(stopMarkers).forEach(([name, marker]) => {
|
||||
if (name.toLowerCase().includes(search)) {
|
||||
marker.openPopup();
|
||||
map.panTo(marker.getLatLng());
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
BIN
public/stop_marker.png
Normal file
BIN
public/stop_marker.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.8 KiB |
BIN
public/stop_marker_special.png
Normal file
BIN
public/stop_marker_special.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.0 KiB |
Reference in New Issue
Block a user