492 lines
17 KiB
Svelte
492 lines
17 KiB
Svelte
<script>
|
|
export let station = "";
|
|
export let title = "Loading...";
|
|
import { onMount } from "svelte";
|
|
import Loading from "$lib/navigation/loading.svelte";
|
|
import OverlayIsland from "$lib/islands/overlay-island.svelte";
|
|
import AlertBar from "$lib/ldb/nrcc/alert-bar.svelte";
|
|
import Island from "$lib/islands/island.svelte";
|
|
import { getApiUrl } from "$lib/scripts/upstream";
|
|
|
|
let requestedStation;
|
|
$: requestedStation = station;
|
|
|
|
let jsonData = null;
|
|
let services = [];
|
|
let busServices = [];
|
|
let ferryServices = [];
|
|
let dataAge = null;
|
|
let isLoading = true;
|
|
let dataExists = false;
|
|
let alerts = [];
|
|
let serviceDetail;
|
|
|
|
$: {
|
|
if (jsonData === null && requestedStation) {
|
|
fetchData();
|
|
}
|
|
|
|
if (jsonData?.GetStationBoardResult?.generatedAt) {
|
|
dataAge = new Date(jsonData.GetStationBoardResult.generatedAt);
|
|
}
|
|
|
|
if (jsonData?.GetStationBoardResult?.trainServices?.service) {
|
|
services = jsonData.GetStationBoardResult.trainServices.service;
|
|
} else {
|
|
services = [];
|
|
}
|
|
|
|
if (jsonData?.GetStationBoardResult?.busServices?.service) {
|
|
busServices = jsonData.GetStationBoardResult.busServices.service;
|
|
}
|
|
|
|
if (jsonData?.GetStationBoardResult?.ferryServices?.service) {
|
|
ferryServices = jsonData.GetStationBoardResult.ferryServices.service;
|
|
}
|
|
|
|
if (jsonData?.GetStationBoardResult?.locationName) {
|
|
title = jsonData.GetStationBoardResult.locationName;
|
|
} else {
|
|
title = requestedStation.toUpperCase();
|
|
}
|
|
}
|
|
|
|
async function fetchData() {
|
|
dataExists = true;
|
|
isLoading = true; // Set loading state
|
|
try {
|
|
console.log(`Requested Station: ${requestedStation}`);
|
|
const data = await fetch(`${getApiUrl()}/api/v2/live/station/${requestedStation}/public`);
|
|
jsonData = await data.json();
|
|
} catch (error) {
|
|
console.error("Error fetching data:", error);
|
|
dataExists = false;
|
|
title = "Not Found";
|
|
} finally {
|
|
isLoading = false; // Clear loading state
|
|
}
|
|
prepareNrcc();
|
|
}
|
|
|
|
function parseTime(string) {
|
|
let output;
|
|
let change;
|
|
switch (string) {
|
|
case "Delayed":
|
|
output = "LATE";
|
|
change = "changed";
|
|
break;
|
|
case "Cancelled":
|
|
output = "CANC";
|
|
change = "cancelled";
|
|
break;
|
|
case "On Time":
|
|
case "On time":
|
|
output = "RT";
|
|
change = "";
|
|
break;
|
|
case "":
|
|
output = "-";
|
|
change = "";
|
|
break;
|
|
case undefined:
|
|
output = "-";
|
|
change = "";
|
|
break;
|
|
case "No report":
|
|
output = "-";
|
|
change = "";
|
|
break;
|
|
case "undefined":
|
|
output = false;
|
|
change = "";
|
|
break;
|
|
default:
|
|
output = string;
|
|
change = "changed";
|
|
}
|
|
return { data: output, changed: change };
|
|
}
|
|
|
|
async function loadService(sid) {
|
|
for (const service of services) {
|
|
if (service.serviceID == sid) {
|
|
serviceDetail = service;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadBusService(sid) {
|
|
for (const service of busServices) {
|
|
if (service.serviceID == sid) {
|
|
serviceDetail = service;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function closeService() {
|
|
serviceDetail = null;
|
|
}
|
|
|
|
async function prepareNrcc() {
|
|
if (jsonData?.GetStationBoardResult?.nrccMessages?.message) {
|
|
const nrcc = jsonData.GetStationBoardResult.nrccMessages.message;
|
|
if (Array.isArray(nrcc)) {
|
|
alerts = nrcc;
|
|
return;
|
|
}
|
|
alerts.push(nrcc);
|
|
return;
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
if (requestedStation && jsonData === null) {
|
|
fetchData();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{#if alerts.length}
|
|
<AlertBar {alerts} />
|
|
{/if}
|
|
|
|
{#if isLoading}
|
|
<Loading />
|
|
{:else if dataAge}
|
|
<p id="timestamp">Updated: {dataAge.toLocaleTimeString()}</p>
|
|
{#if services.length}
|
|
<table class="ldbTable">
|
|
<tr>
|
|
<th class="from">From</th>
|
|
<th class="to">To</th>
|
|
<th class="plat">Plat.</th>
|
|
<th class="time">Sch Arr.</th>
|
|
<th class="time">Exp Arr.</th>
|
|
<th class="time">Sch Dep.</th>
|
|
<th class="time">Exp Dep.</th>
|
|
</tr>
|
|
{#each services as service}
|
|
<tr>
|
|
<td class="origdest from" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}>
|
|
{#if Array.isArray(service.origin?.location)}
|
|
{service.origin.location[0]["locationName"] + " & " + service.origin.location[1]["locationName"]}
|
|
{:else}
|
|
{service.origin?.location?.locationName || ""}
|
|
{/if}
|
|
</td>
|
|
<td class="origdest to" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}>
|
|
{#if Array.isArray(service.destination?.location)}
|
|
{service.destination.location[0]["locationName"] + " & " + service.destination.location[0]["locationName"]}
|
|
{:else}
|
|
{service.destination?.location?.locationName || ""}
|
|
{/if}
|
|
</td>
|
|
<td class="plat">{service.platform || "-"}</td>
|
|
<td class="time">{parseTime(service.sta).data}</td>
|
|
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
|
|
<td class="time">{parseTime(service.std).data}</td>
|
|
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
|
|
</tr>
|
|
|
|
<tr
|
|
><td colspan="7">
|
|
<p class="service-detail">
|
|
A {service.operator || "Unknown"} service
|
|
{#if service["length"]}
|
|
with {service["length"] || "some"} coaches
|
|
{/if}
|
|
</p>
|
|
{#if service.delayReason}
|
|
<p class="service-detail">{service.delayReason}</p>
|
|
{/if}
|
|
{#if service.cancelReason}
|
|
<p class="service-detail">{service.cancelReason}</p>
|
|
{/if}
|
|
</td></tr
|
|
>
|
|
{/each}
|
|
</table>
|
|
{:else}
|
|
<p class="table-head-text">No Scheduled Train Services</p>
|
|
{/if}
|
|
{#if busServices.length}
|
|
<br />
|
|
<img class="transport-mode" src="/images/transport-modes/bus.svg" alt="Bus services" /><br />
|
|
<span class="table-head-text">Bus Services</span>
|
|
<table class="ldbTable">
|
|
<tr>
|
|
<th class="from">From</th>
|
|
<th class="to">To</th>
|
|
<th class="time">Sch Arr.</th>
|
|
<th class="time">Exp Arr.</th>
|
|
<th class="time">Sch Dep.</th>
|
|
<th class="time">Exp Dep.</th>
|
|
</tr>
|
|
{#each busServices as service}
|
|
<tr>
|
|
<td class="origdest from" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}
|
|
>{service.origin?.location?.locationName || ""}</td
|
|
>
|
|
<td class="origdest to" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}
|
|
>{service.destination?.location?.locationName || ""}</td
|
|
>
|
|
<td class="time">{parseTime(service.sta).data}</td>
|
|
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
|
|
<td class="time">{parseTime(service.std).data}</td>
|
|
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
|
|
</tr>
|
|
|
|
<tr
|
|
><td colspan="7">
|
|
<p class="service-detail">
|
|
A {service.operator || "Unknown"} service
|
|
</p>
|
|
{#if service.delayReason}
|
|
<p class="service-detail">{service.delayReason}</p>
|
|
{/if}
|
|
{#if service.cancelReason}
|
|
<p class="service-detail">{service.cancelReason}</p>
|
|
{/if}
|
|
</td></tr
|
|
>
|
|
{/each}
|
|
</table>
|
|
{/if}
|
|
{#if ferryServices.length}
|
|
<br />
|
|
<img class="transport-mode" src="/images/transport-modes/ferry.svg" alt="Bus services" /><br />
|
|
<span class="table-head-text">Ferry Services</span>
|
|
<table class="ldbTable">
|
|
<tr>
|
|
<th class="from">From</th>
|
|
<th class="to">To</th>
|
|
<th class="time">Sch Arr.</th>
|
|
<th class="time">Exp Arr.</th>
|
|
<th class="time">Sch Dep.</th>
|
|
<th class="time">Exp Dep.</th>
|
|
</tr>
|
|
{#each ferryServices as service}
|
|
<tr>
|
|
<td class="origdest from">{service.origin?.location?.locationName || ""}</td>
|
|
<td class="origdest to">{service.destination?.location?.locationName || ""}</td>
|
|
<td class="time">{parseTime(service.sta).data}</td>
|
|
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
|
|
<td class="time">{parseTime(service.std).data}</td>
|
|
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
|
|
</tr>
|
|
|
|
<tr
|
|
><td colspan="7">
|
|
{#if service.delayReason}
|
|
<p class="service-detail">{service.delayReason}</p>
|
|
{/if}
|
|
{#if service.cancelReason}
|
|
<p class="service-detail">{service.cancelReason}</p>
|
|
{/if}
|
|
</td></tr
|
|
>
|
|
{/each}
|
|
</table>
|
|
{/if}
|
|
{:else}
|
|
<Island>
|
|
<p style="font-weight:600">Unable to load data</p>
|
|
</Island>
|
|
{/if}
|
|
|
|
{#if serviceDetail}
|
|
<OverlayIsland>
|
|
<div id="detailBox">
|
|
<h6>Service Detail</h6>
|
|
<button type="button" id="closeService" on:click={closeService}>X</button>
|
|
<table id="detailTable">
|
|
<tr>
|
|
<th>Location</th>
|
|
<th>Sch</th>
|
|
<th>Exp</th>
|
|
</tr>
|
|
{#if serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint}
|
|
{#if Array.isArray(serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint)}
|
|
{#each serviceDetail.previousCallingPoints.callingPointList.callingPoint as prevPoint}
|
|
<tr>
|
|
<td>{prevPoint.locationName}</td>
|
|
<td>{prevPoint.st}</td>
|
|
<td class="time {parseTime(prevPoint.at || prevPoint.et).changed}">{parseTime(prevPoint.at || prevPoint.et).data}</td>
|
|
</tr>
|
|
{/each}
|
|
{:else}
|
|
<tr>
|
|
<td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.locationName}</td>
|
|
<td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.st}</td>
|
|
<td
|
|
class="time {parseTime(
|
|
serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et
|
|
).changed}"
|
|
>{parseTime(
|
|
serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et
|
|
).data}</td
|
|
>
|
|
</tr>
|
|
{/if}
|
|
{/if}
|
|
<tr class="thisStop">
|
|
<td>{title}</td>
|
|
<td>{serviceDetail.std || serviceDetail.sta}</td>
|
|
<td class="time {parseTime(serviceDetail.etd || serviceDetail.eta).changed}">{parseTime(serviceDetail.etd || serviceDetail.eta).data}</td>
|
|
</tr>
|
|
{#if serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint}
|
|
{#if Array.isArray(serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint)}
|
|
{#each serviceDetail.subsequentCallingPoints.callingPointList.callingPoint as nextPoint}
|
|
<tr>
|
|
<td>{nextPoint.locationName}</td>
|
|
<td>{nextPoint.st}</td>
|
|
<td class="time {parseTime(nextPoint.et).changed}">{parseTime(nextPoint.et).data}</td>
|
|
</tr>
|
|
{/each}
|
|
{:else}
|
|
<tr class="detailRow">
|
|
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.locationName}</td>
|
|
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.st}</td>
|
|
<td class="time {parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).changed}"
|
|
>{parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).data}</td
|
|
>
|
|
</tr>
|
|
{/if}
|
|
{/if}
|
|
</table>
|
|
</div>
|
|
</OverlayIsland>
|
|
{/if}
|
|
|
|
<style>
|
|
#timestamp {
|
|
margin: auto;
|
|
text-align: left;
|
|
font-size: 14px;
|
|
}
|
|
.ldbTable {
|
|
width: 100%;
|
|
min-width: 300px;
|
|
margin: auto;
|
|
padding-right: 2px;
|
|
padding-left: 0px;
|
|
color: white;
|
|
font-size: 13px;
|
|
}
|
|
.service-detail {
|
|
color: cyan;
|
|
text-align: left;
|
|
font-size: 12px;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
.transport-mode {
|
|
width: 30px;
|
|
}
|
|
.table-head-text {
|
|
color: white;
|
|
font-size: 14px;
|
|
}
|
|
@media (min-width: 800px) {
|
|
table {
|
|
font-size: 15px;
|
|
max-width: 850px;
|
|
}
|
|
.service-detail {
|
|
font-size: 14px;
|
|
}
|
|
.transport-mode {
|
|
width: 50px;
|
|
}
|
|
#timestamp {
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
@media (min-width: 1000px) {
|
|
table {
|
|
font-size: 17px;
|
|
}
|
|
.service-detail {
|
|
font-size: 16px;
|
|
}
|
|
.table-head-text {
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
@media (min-width: 1600px) {
|
|
table {
|
|
font-size: 19px;
|
|
}
|
|
.service-detail {
|
|
font-size: 18px;
|
|
}
|
|
}
|
|
.origdest {
|
|
color: yellow;
|
|
}
|
|
.from {
|
|
width: 25%;
|
|
text-align: left;
|
|
}
|
|
.to {
|
|
width: 25%;
|
|
text-align: left;
|
|
}
|
|
.plat {
|
|
width: 10%;
|
|
}
|
|
.time {
|
|
width: 10%;
|
|
}
|
|
.changed {
|
|
animation: pulse-change 1.5s linear infinite;
|
|
}
|
|
|
|
.cancelled {
|
|
animation: pulse-cancel 1.5s linear infinite;
|
|
}
|
|
|
|
@keyframes pulse-change {
|
|
50% {
|
|
color: var(--main-warning-color);
|
|
}
|
|
}
|
|
|
|
@keyframes pulse-cancel {
|
|
50% {
|
|
color: var(--main-alert-color);
|
|
}
|
|
}
|
|
#detailBox {
|
|
width: 100%;
|
|
}
|
|
h6 {
|
|
position: absolute;
|
|
top: -25px;
|
|
left: 20px;
|
|
font-size: 18px;
|
|
}
|
|
#closeService {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
border: none;
|
|
border-radius: 60px;
|
|
width: 35px;
|
|
height: 35px;
|
|
background-color: var(--main-bg-color);
|
|
color: white;
|
|
font-weight: 700;
|
|
font-size: 16px;
|
|
}
|
|
#detailTable {
|
|
margin-top: 40px;
|
|
color: white;
|
|
font-size: 15px;
|
|
}
|
|
.thisStop {
|
|
color: yellow;
|
|
}
|
|
</style>
|