This commit is contained in:
2025-07-16 07:50:54 +09:00
parent fb145c44f2
commit 8f4d89325b
3 changed files with 199 additions and 5201 deletions

View File

@@ -11,165 +11,170 @@
crossorigin=""></script> crossorigin=""></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style> <style>
#map { html, body {
height: calc(100vh - 60px); height: 100%;
background-color: #e5e5e5; margin: 0;
} padding: 0;
}
.station-label {
text-align: center; #map {
font-weight: bold; height: 100%;
font-size: 15px; }
color: black;
} .station-label {
text-align: center;
font-weight: bold;
font-size: 15px;
color: black;
}
.train-marker { .train-marker {
text-align: center; text-align: center;
} }
.bottombar { #reload {
height: 40px; padding: 5px 10px;
display: flex; background-color: #007BFF;
justify-content: space-between; color: white;
align-items: center; border: none;
padding: 0 20px; cursor: pointer;
} border-radius: 4px;
}
.bottombar .left { #reload:hover {
display: flex; background-color: #0056b3;
align-items: center; }
}
.bottombar .left input[type="checkbox"] { .hidden {
margin-right: 10px; display: none;
} }
.bottombar .right { #info {
display: flex; position: fixed;
align-items: center; bottom: 10px;
} left: 10px;
z-index: 1000;
.bottombar .loading { }
margin-right: 15px;
}
#reloadTrains {
padding: 5px 10px;
background-color: #007BFF;
color: white;
border: none;
cursor: pointer;
border-radius: 4px;
}
#reloadTrains:hover {
background-color: #0056b3;
}
.hidden {
display: none;
}
#loading {
margin-left: 10px;
}
</style> </style>
</head> </head>
<body> <body>
<div id="map"></div> <div id="map"></div>
<div class="bottombar"> <div id="info">
<div class="left"> <button id="reload">Reload</button>
<input type="checkbox" id="layer0"> Express <span id="loading">Loading...</span>
<input type="checkbox" id="layer1"> Semi
<input type="checkbox" id="layer2"> Normal
<input type="checkbox" id="layer3"> Passenger
<input type="checkbox" id="layer4"> Subway
<input type="checkbox" id="layer5"> Logis
</div>
<div class="right">
<span class="loading" id="loading">Loading...</span>
<button id="reloadTrains">Reload</button>
<input type="checkbox" id="autoReload"> Auto
</div>
</div> </div>
<script> <script>
const trainAPI = 'https://gis.korail.com/api/train?bbox=120.6263671875,28.07910949377748,134.0736328125,45.094739803960664'
const server = ''
let openPopupTrainId = null;
let followingTrainId = null;
let isFollowing = false;
async function myFetch(url, options = {}) { async function myFetch(url, options = {}) {
let headers = options.headers let headers = options.headers
let f = fetch('https://proxy.devpg.net', { return fetch('https://proxy/', {
headers: { headers: {
'X-Proxy-URL': url, 'X-Proxy-URL': url,
'X-Proxy-Header': JSON.stringify(headers), 'X-Proxy-Header': JSON.stringify(headers),
'X-Proxy-Cache': '0' 'X-Proxy-Cache': '0'
} }
}); });
console.log(`fetch(${url}, headers=${JSON.stringify(headers)}`, f);
return f
} }
const trainAPI = 'https://gis.korail.com/api/train?bbox=120.6263671875,28.07910949377748,134.0736328125,45.094739803960664'
const server = ''
const map = L.map('map').setView([36.5, 128], 7.5); const map = L.map('map').setView([36.5, 128], 7.5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map); }).addTo(map);
const railLayers = { L.tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', {
express: L.layerGroup().addTo(map), attribution: '&copy; <a href="https://www.openrailwaymap.org/">OpenRailwayMap</a>',
normal: L.layerGroup().addTo(map), maxZoom: 19
semi: L.layerGroup().addTo(map), }).addTo(map);
logis: L.layerGroup().addTo(map)
};
const stationLayers = []; const stationLayers = [];
for (let i = 0; i < 64; i++) { for (let i = 0; i < 64; i++) {
const layerGroup = L.layerGroup().addTo(map); stationLayers.push(L.layerGroup());
stationLayers.push(layerGroup);
} }
for (let layer of stationLayers) {
map.removeLayer(layer);
}
const trainLayer = L.layerGroup().addTo(map); const trainLayer = L.layerGroup().addTo(map);
const trainPositions = new Map();
function startSpinner() { function startSpinner() {
document.getElementById("loading").classList.remove("hidden"); document.getElementById("loading").classList.remove("hidden");
} }
function clearSpinner() { function clearSpinner() {
document.getElementById("loading").classList.add("hidden"); document.getElementById("loading").classList.add("hidden");
} }
function loadRail(type) { function calculateDistance(lat1, lon1, lat2, lon2) {
fetch(`${server}/api/rail/${type}`) const R = 6371;
.then(response => { const dLat = (lat2 - lat1) * Math.PI / 180;
if (!response.ok) { const dLon = (lon2 - lon1) * Math.PI / 180;
throw new Error(`Network response was not ok ${response.statusText}`); const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
function calculateAverageSpeed(trainId) {
const positions = trainPositions.get(trainId);
if (!positions || positions.length < 2) return null;
const firstChange = positions[0];
const lastChange = positions[positions.length - 1];
const totalDistance = calculateDistance(firstChange.lat, firstChange.lon, lastChange.lat, lastChange.lon);
const totalTime = (lastChange.timestamp - firstChange.timestamp) / 1000;
if (totalTime === 0) return null;
return (totalDistance / totalTime) * 3600;
}
function updateTrainPosition(trainId, lat, lon, timestamp) {
if (!trainPositions.has(trainId)) {
trainPositions.set(trainId, []);
}
const positions = trainPositions.get(trainId);
if (positions.length > 0) {
const lastPos = positions[positions.length - 1];
if (Math.abs(lastPos.lat - lat) < 0.0001 && Math.abs(lastPos.lon - lon) < 0.0001) {
return;
}
}
const lookbackCount = Math.min(12, positions.length);
for (let i = Math.max(0, positions.length - lookbackCount); i < positions.length; i++) {
const historicalPos = positions[i];
if (Math.abs(historicalPos.lat - lat) < 0.0001 && Math.abs(historicalPos.lon - lon) < 0.0001) {
const positionsBack = positions.length - 1 - i;
if (positionsBack > 0 && positionsBack <= 10) {
return;
} }
return response.json(); }
}) }
.then(data => {
L.geoJSON(data, { positions.push({ lat, lon, timestamp });
style: { if (positions.length > 30) {
color: type === 'express' ? 'blue' : type === 'normal' ? 'green' : type === 'semi' ? 'orange' : 'red' positions.shift();
} }
}).addTo(railLayers[type]);
})
.catch(error => console.error(`Error fetching ${type} rail data: `, error));
} }
function loadStations() { function loadStations() {
fetch(`${server}/api/station`) fetch(`${server}/api/station`)
.then(response => { .then(response => response.json())
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => { .then(data => {
L.geoJSON(data, { L.geoJSON(data, {
pointToLayer: function (feature, latlng) { pointToLayer: function (feature, latlng) {
const shownLayer = parseInt(feature.properties.shown_layer.slice(2), 2); const shownLayer = parseInt(feature.properties.shown_layer.slice(2), 2);
const marker = L.marker(latlng, { const marker = L.marker(latlng, {
icon: L.divIcon({ icon: L.divIcon({
className: 'station-label', className: 'station-label',
@@ -180,7 +185,6 @@
if (stationLayers[shownLayer]) { if (stationLayers[shownLayer]) {
stationLayers[shownLayer].addLayer(marker); stationLayers[shownLayer].addLayer(marker);
} }
return marker; return marker;
} }
}); });
@@ -190,35 +194,47 @@
function loadTrains() { function loadTrains() {
startSpinner(); startSpinner();
const timestamp = Date.now();
let wasPopupOpen = false;
if (map._popup && map._popup._source) {
const popup = map._popup;
if (popup._source.options && popup._source.options.trainId) {
openPopupTrainId = popup._source.options.trainId;
wasPopupOpen = true;
}
}
myFetch(trainAPI, { myFetch(trainAPI, {
headers: { headers: {
"x-requested-with": "com.korail.talk", "x-requested-with": "com.korail.talk",
"referer": "https://gis.korail.com/korailTalk/entrance", "referer": "https://gis.korail.com/korailTalk/entrance",
"user-agent": "korailtalk AppVersion/6.3.3" "user-agent": "korailtalk AppVersion/6.3.3"
} }
}) })
.then(response => { .then(response => response.json())
if (!response.ok) { .then(data => {
throw new Error('Network response was not ok'); trainLayer.clearLayers();
} let markerToReopen = null;
return response.json(); let markerToFollow = null;
})
.then(data => {
trainLayer.clearLayers();
L.geoJSON(data, { L.geoJSON(data, {
pointToLayer: function (feature, latlng) { pointToLayer: function (feature, latlng) {
const trainId = `${feature.properties.trn_case}_${feature.properties.trn_no}`;
updateTrainPosition(trainId, latlng.lat, latlng.lng, timestamp);
const avgSpeed = calculateAverageSpeed(trainId);
const speedText = avgSpeed ? `${avgSpeed.toFixed(1)} km/h` : 'N/A';
const followButtonText = followingTrainId === trainId ? 'Unfollow' : 'Follow';
const followButtonColor = followingTrainId === trainId ? '#dc3545' : '#28a745';
const trainIconHtml = ` const trainIconHtml = `
<div style="position: relative; text-align: center;"> <div style="position: relative; text-align: center;">
<div style="background-color: #007bff;
border-radius: 50%;
width: 16px;
height: 16px;
display: inline-block;
border: 2px solid white;"></div>
<div style="font-size: 12px; <div style="font-size: 12px;
color: black; color: ${feature.properties.trn_case === "SRT" ? 'white' : 'black'};
background-color: white; background-color: ${feature.properties.trn_case === "SRT" ? '#572b4c' : '#007bff'};
border: 1px solid black; border: 1px solid black;
margin-top: 2px; margin-top: 2px;
padding: 1px 3px; padding: 1px 3px;
@@ -229,61 +245,72 @@
transform: translateX(-50%); transform: translateX(-50%);
white-space: nowrap;">${feature.properties.trn_case} ${feature.properties.trn_no}</div> white-space: nowrap;">${feature.properties.trn_case} ${feature.properties.trn_no}</div>
</div>`; </div>`;
return L.marker(latlng, {
const marker = L.marker(latlng, {
icon: L.divIcon({ icon: L.divIcon({
className: 'train-marker', className: 'train-marker',
html: trainIconHtml, html: trainIconHtml,
iconSize: [30, 42], iconSize: [30, 42],
iconAnchor: [15, 21] iconAnchor: [15, 21]
}) }),
trainId: trainId
}).bindPopup(`${feature.properties.trn_case} ${feature.properties.trn_no}<br> }).bindPopup(`${feature.properties.trn_case} ${feature.properties.trn_no}<br>
(${feature.properties.dpt_stn_nm} -> ${feature.properties.arv_stn_nm})<br> (${feature.properties.dpt_stn_nm} -> ${feature.properties.arv_stn_nm})<br>
${feature.properties.now_stn} ${feature.properties.next_stn}`); ${feature.properties.now_stn} ${feature.properties.next_stn}<br>
<strong>Avg Speed: ${speedText}</strong><br>
<button onclick="toggleFollow('${trainId}')"
style="background-color: ${followButtonColor};
color: white;
border: none;
padding: 3px 8px;
border-radius: 3px;
cursor: pointer;
margin-top: 5px;">${followButtonText}</button>`);
if (wasPopupOpen && trainId === openPopupTrainId) {
markerToReopen = marker;
} }
}).addTo(trainLayer);
clearSpinner(); if (isFollowing && trainId === followingTrainId) {
}) markerToFollow = marker;
.catch(error => console.error('Error fetching train data: ', error)); }
return marker;
}
}).addTo(trainLayer);
if (markerToReopen) {
setTimeout(() => markerToReopen.openPopup(), 100);
}
if (markerToFollow) {
setTimeout(() => map.setView(markerToFollow.getLatLng(), map.getZoom()), 100);
}
clearSpinner();
})
.catch(error => {
console.error('Error fetching train data: ', error);
clearSpinner();
});
} }
loadRail('express'); function toggleFollow(trainId) {
loadRail('normal'); if (followingTrainId === trainId) {
loadRail('semi'); followingTrainId = null;
loadRail('logis'); isFollowing = false;
} else {
followingTrainId = trainId;
for (let f = 0; f < 6; f++) { isFollowing = true;
document.getElementById(`layer${f}`).addEventListener('change', function () { // lable0: 0b 1XXXXX, label1: 0bX1XXXX, ... }
for (let i = 0; i < 64; i++) { loadTrains();
if ( i & (1 << (5 - 0)) ) {
if (this.checked) {
map.addLayer(stationLayers[i]);
} else {
map.removeLayer(stationLayers[i]);
}
}
}
})
} }
document.getElementById('reloadTrains').addEventListener('click', loadTrains); document.getElementById('reload').addEventListener('click', loadTrains);
document.getElementById('autoReload').addEventListener('click', autoReload);
let reloadInterval = null
function autoReload() {
if (this.checked) {
reloadInterval = setInterval(loadTrains, 2000);
}
else {
if (reloadInterval) {
clearInterval(reloadInterval);
reloadInterval = null;
}
}
}
loadStations(); loadStations();
loadTrains(); loadTrains();
setInterval(loadTrains, 2000);
</script> </script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -40,11 +40,7 @@ async def getApiTrain():
}) })
return tapi.json() return tapi.json()
with open("json/trains.json", "r") as f:
return json.load(f)
if __name__=="__main__": if __name__=="__main__":
import uvicorn import uvicorn
uvicorn.run(app) uvicorn.run(app)