486 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Svelte
		
	
	
	
	
	
			
		
		
	
	
			486 lines
		
	
	
		
			14 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';
 | 
						|
 | 
						|
  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(`https://owlboard.info/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>
 |