This commit is contained in:
2022-10-16 20:01:44 +02:00
parent ab933e4ad1
commit 83aa054a70
11 changed files with 766 additions and 706 deletions
+13 -87
View File
@@ -1,38 +1,26 @@
<template>
<div class="app_content">
<nav class="navbar">
<div v-if="!selectedStation">
<router-link to="/">
Pragotron TD2 <span class="text--accent">v{{ VERSION }}</span> <sup>by Spythere</sup>
</div>
<button v-else class="back-btn btn--text" @click="selectedStation = null">&lt; powrót</button>
</router-link>
<!-- <button v-else class="back-btn btn--text" @click="selectedStation = null">&lt; powrót</button> -->
</nav>
<main>
<div class="scenery-selector" v-if="!selectedStation">
<h1>Scenerie <span class="text--accent">online</span></h1>
<ul class="scenery-list">
<li v-for="(station, i) in onlineStations">
<span v-if="i > 0">&bull;</span>
<button class="btn--text" @click="handleClick(station)">
{{ station.stationName }}
</button>
</li>
</ul>
</div>
<PragotronVue v-else />
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="$route.path"></component>
</keep-alive>
</router-view>
</main>
</div>
</template>
<script lang="ts">
import { provide, Ref, ref } from 'vue';
import { defineComponent } from '@vue/runtime-core';
import PragotronVue from './components/Pragotron.vue';
import IStationData from './types/IStationData';
import { ISceneryResponse } from './types/ISceneryReponse';
import { IOnlineStationsResponse } from './types/IOnlineStationsResponse';
import PragotronVue from './views/PragotronView.vue';
import IStationData from './types/ISceneryData';
import packageInfo from '../package.json';
@@ -41,22 +29,6 @@ export default defineComponent({
PragotronVue,
},
setup() {
const mockStation: IStationData = {
stationName: 'Czermin',
nameAbbreviation: '',
stationCheckpoints: [],
};
const selectedStation = ref(null) as Ref<null | IStationData>;
provide('selectedStation', selectedStation);
return {
selectedStation,
};
},
data: () => ({
onlineStations: [] as IStationData[],
dataLoaded: false,
@@ -65,47 +37,8 @@ export default defineComponent({
}),
async mounted() {
const stationDataArray: ISceneryResponse[] = await (
await fetch(`${import.meta.env.VITE_STACJOWNIK_API_URL}/api/getSceneries`)
).json();
const stationDataJSON = stationDataArray.map((stationData) => ({
stationName: stationData.name,
stationCheckpoints: stationData.checkpoints?.length > 0 ? stationData.checkpoints.split(';') : [stationData.name],
nameAbbreviation: '',
}));
const stationsAPIResponse: IOnlineStationsResponse = await (
await fetch('https://api.td2.info.pl:9640/?method=getStationsOnline')
).json();
this.onlineStations = stationsAPIResponse.message
.reduce((acc, station) => {
if (station.region != 'eu') return acc;
if (!station.isOnline) return acc;
const savedStationData = stationDataJSON.find((data) => data.stationName == station.stationName);
if (savedStationData) acc.push(savedStationData);
else
acc.push({
stationName: station.stationName,
nameAbbreviation: '',
stationCheckpoints: [],
});
return acc;
}, [] as IStationData[])
.sort((s1, s2) => (s1.stationName > s2.stationName ? 1 : -1));
this.dataLoaded = true;
},
methods: {
handleClick(station: IStationData) {
this.selectedStation = station;
},
},
});
</script>
@@ -150,16 +83,9 @@ main {
align-items: center;
}
ul.scenery-list {
display: flex;
justify-content: center;
flex-wrap: wrap;
padding: 1em;
margin: 0 auto;
font-size: 1.3em;
max-width: 1000px;
a {
text-decoration: none;
color: white;
}
</style>
-541
View File
@@ -1,541 +0,0 @@
<template>
<div class="pragotron">
<div class="filters">
<div>
<label>
<input type="checkbox" v-model="includeNonPassenger" />
Relacje niepasażerskie
</label>
<label>
<input type="checkbox" v-model="includeArrivals" />
Relacje kończące bieg
</label>
</div>
<div>
<label for="checkpoint">
Posterunek:
<select id="checkpoint" v-model="selectedCheckpointName">
<option v-for="cp in selectedStation.stationCheckpoints" :value="cp">{{ cp }}</option>
</select>
</label>
</div>
</div>
<div class="wrapper">
<div class="top-pane">
<span class="title">
<div>{{ selectedCheckpointName.toUpperCase() }}</div>
</span>
<div class="headers">
<span>GODZ.</span>
<span>POCIĄG</span>
<span>PRZEZ</span>
<span>DO STACJI</span>
<span>OPÓŹNIONY</span>
</div>
</div>
<div class="table">
<div class="row" v-for="(departure, i) in filledTable" :key="i">
<div class="row-content">
<span class="departure-date">
<transition name="slot-anim" mode="out-in">
<span class="slider-slot-digit" :key="departure.tableValues.dateDigits[0]">{{
departure.tableValues.dateDigits[0]
}}</span>
</transition>
<transition name="slot-anim" mode="out-in">
<span class="slider-slot-digit" :key="departure.tableValues.dateDigits[1]">
{{ departure.tableValues.dateDigits[1] }}</span
>
</transition>
<span :key="departure.tableValues.dateDigits[1]"> : </span>
<transition name="slot-anim" mode="out-in">
<span class="slider-slot-digit" :key="departure.tableValues.dateDigits[2]">
{{ departure.tableValues.dateDigits[2] }}
</span>
</transition>
<transition name="slot-anim" mode="out-in">
<span class="slider-slot-digit" :key="departure.tableValues.dateDigits[3]">
{{ departure.tableValues.dateDigits[3] }}
</span>
</transition>
</span>
<span class="train-class">
<transition name="slot-anim" mode="out-in">
<div class="slider-slot" :key="departure.trainNumber">{{ departure.trainNumber }}</div>
</transition>
</span>
<span class="route-via">
<transition name="slot-anim" mode="out-in">
<div class="slider-slot" :key="departure.tableValues.routeVia">
{{ abbrevStationName(departure.tableValues.routeVia) }}
</div>
</transition>
</span>
<span class="route-to">
<transition name="slot-anim" mode="out-in">
<div class="slider-slot" :key="departure.tableValues.routeTo">
{{ abbrevStationName(departure.tableValues.routeTo) }}
</div>
</transition>
</span>
<span class="delay">
<transition name="slot-anim" mode="out-in">
<div class="slider-slot" :key="departure.delayMinutes">
{{ departure.delayMinutes > 0 ? departure.delayMinutes + ' min' : '' }}
</div>
</transition>
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
/* eslint-disable */
import { defineComponent, inject, reactive, Ref } from 'vue';
import stationAbbrevsJSON from '../data/stationAbbrevs.json';
import routeValues from '../data/routeValues.json';
import StationData from '../types/IStationData';
import { ITimetableStop, ITrainResponse } from '../types/ITrainResponse';
import { ITableRow } from '../types/ITableRow';
const stationAbbrevs: { [key: string]: string } = stationAbbrevsJSON;
const departureInfoEmptyObj: ITableRow = {
timetableId: -1,
routeTo: '',
routeVia: '',
trainNumber: '',
date: new Date(0),
dateDigits: [' ', ' ', ' ', ' '],
arrivalTimestamp: 0,
departureTimestamp: 0,
checkpointName: '',
delayMinutes: 0,
tableValues: {
routeTo: '',
routeVia: '',
currentRouteToIndex: 0,
currentRouteViaIndex: 0,
dateDigits: [' ', ' ', ' ', ' '],
},
};
export default defineComponent({
data: () => ({
currentStationName: '',
includeNonPassenger: true,
includeArrivals: true,
apiTrainData: [] as ITrainResponse[],
lastRefreshTime: 0,
updatedDepartureList: [] as ITableRow[],
departureList: new Array(7).fill(0).map(() => ({ ...departureInfoEmptyObj })) as ITableRow[],
departureRoutes: [''],
dateDigits: [' ', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
currentRouteIndex: 0,
currentDateDigitIndex: 0,
stationAbbrevs: stationAbbrevs as { [key: string]: string },
selectedCheckpointName: '',
}),
setup() {
const selectedStation = inject('selectedStation') as Ref<StationData>;
return {
selectedStation,
};
},
async mounted() {
this.selectDefaultCheckpoint();
this.shuffleRoutes();
await this.fetchDepartureList();
setInterval(() => {
this.fetchDepartureList();
}, 10000);
requestAnimationFrame(this.update);
},
computed: {
filledTable() {
const filteredData = this.apiTrainData
.reduce((list, train) => {
if (!train.timetable) return list;
const timetable = train.timetable;
const stopInfo: ITimetableStop | undefined = timetable.stopList.find(
(sp) => sp.stopNameRAW.toLowerCase() == this.selectedCheckpointName.toLowerCase()
);
if (!stopInfo || stopInfo.confirmed) return list;
const stopInfoIndex = timetable.stopList.indexOf(stopInfo);
const { departureTimestamp, departureDelay, arrivalTimestamp, departureLine } = stopInfo;
const routeVia =
timetable.stopList.find((stop, i) => {
return (
i > stopInfoIndex &&
// i < timetable.stopList.length - 1,
stop.stopName.includes('strong') &&
stop.stopTime &&
stop.stopTime > 0
);
})?.stopNameRAW || '';
const date = departureLine ? new Date(departureTimestamp) : new Date(arrivalTimestamp);
// [HH, MM, SS] - nienawidzę dat w JavaScripcie
const dateArray = date.toLocaleString('pl-PL').split(', ')[1].split(':') || ['', '', '', ''];
// [H,H,M,M] - ZABIJCIE MNIE BŁAGAM
const dateDigits = [...dateArray[0].split(''), ...dateArray[1].split('')];
const routeTo = timetable.route.split('|')[1];
list.push({
trainNumber: `${timetable.category} ${train.trainNo}`,
timetableId: timetable.timetableId,
routeTo: routeTo,
routeVia: routeVia,
date,
dateDigits,
delayMinutes: departureDelay,
checkpointName: this.selectedCheckpointName.toLowerCase(),
arrivalTimestamp,
departureTimestamp,
tableValues: {
routeTo: '',
routeVia: '',
dateDigits: [' ', ' ', ' ', ' '],
currentRouteToIndex: 0,
currentRouteViaIndex: 0
},
});
if (!this.departureRoutes.includes(routeVia)) this.departureRoutes.push(routeVia);
if (!this.departureRoutes.includes(routeTo)) this.departureRoutes.push(routeTo);
return list;
}, [] as ITableRow[])
.filter(
(dep) =>
(this.includeNonPassenger || !/^[T|L|Z|P]/g.test(dep.trainNumber)) &&
(this.includeArrivals || dep.departureTimestamp)
)
.sort((dep1, dep2) => (dep1.date?.getTime() || 0) - (dep2.date?.getTime() || 0));
for (let i = 0; i < this.departureList.length; i++) {
if (i <= filteredData.length - 1) {
const updateInfo = filteredData[i];
const existingInfo = this.departureList[i];
this.departureList[i] = { ...updateInfo };
this.departureList[i].tableValues.routeTo = existingInfo.routeTo;
this.departureList[i].tableValues.routeVia = existingInfo.routeVia;
this.departureList[i].tableValues.dateDigits = [...existingInfo.tableValues.dateDigits];
} else {
this.departureList[i] = departureInfoEmptyObj;
}
}
return this.departureList;
},
},
methods: {
selectDefaultCheckpoint() {
this.selectedCheckpointName = this.selectedStation.stationCheckpoints[0] || this.selectedStation.stationName;
},
abbrevStationName(name: string) {
return (name in stationAbbrevs ? stationAbbrevs[name] : name).toUpperCase();
},
update(time: number) {
if (time >= this.lastRefreshTime + 125) {
this.updateTableRows();
this.currentRouteIndex = (this.currentRouteIndex + 1) % this.departureRoutes.length;
// this.currentDateDigitIndex = (this.currentDateDigitIndex + 1) % this.dateDigits.length;
this.lastRefreshTime = time;
}
requestAnimationFrame(this.update);
},
// d = 0 -> time = time
// d = time -> time2 = time2-time
updateTableRows() {
this.departureList.forEach((dep, i) => {
if (dep.tableValues.routeTo.toLowerCase() != dep.routeTo.toLowerCase()) {
dep.tableValues.routeTo = this.departureRoutes[(dep.tableValues.currentRouteToIndex) % this.departureRoutes.length];
}
if (dep.tableValues.routeVia.toLowerCase() != dep.routeVia.toLowerCase()) {
dep.tableValues.routeVia =
this.departureRoutes[(dep.tableValues.currentRouteViaIndex) % this.departureRoutes.length];
}
dep.tableValues.currentRouteToIndex = (dep.tableValues.currentRouteToIndex + 1) % this.departureRoutes.length;
dep.tableValues.currentRouteViaIndex = (dep.tableValues.currentRouteViaIndex + 1) % this.departureRoutes.length;
dep.tableValues.dateDigits.forEach((digit, j) => {
if (dep.dateDigits[j] != digit)
dep.tableValues.dateDigits[j] =
this.dateDigits[(Number(digit) + 1) % this.dateDigits.length];
});
});
},
shuffleRoutes() {
for (let i = 0; i < 25; i++) {
const randIndex = Math.floor(Math.random() * routeValues.length);
const randRoute = routeValues[randIndex];
// this.departureRoutes.push(randRoute);
}
this.departureRoutes.sort(() => Math.random() - 0.5);
},
// refreshTable(apiData: ITrainResponse[]) {
// const updatedDepartureList = apiData.reduce((list, train) => {
// if (!train.timetable) return list;
// const timetable = train.timetable;
// const stopInfo: ITimetableStop | undefined = timetable.stopList.find(
// (sp) => sp.stopNameRAW.toLowerCase() == this.selectedCheckpointName.toLowerCase()
// );
// if (!stopInfo || stopInfo.confirmed) return list;
// const stopInfoIndex = timetable.stopList.indexOf(stopInfo);
// const { departureTimestamp, departureDelay, arrivalTimestamp, departureLine } = stopInfo;
// const routeVia =
// timetable.stopList.find((stop, i) => {
// return (
// i > stopInfoIndex &&
// // i < timetable.stopList.length - 1,
// stop.stopName.includes('strong') &&
// stop.stopTime &&
// stop.stopTime > 0
// );
// })?.stopNameRAW || '';
// const date = departureLine ? new Date(departureTimestamp) : new Date(arrivalTimestamp);
// // [HH, MM, SS] - nienawidzę dat w JavaScripcie
// const dateArray = date.toLocaleString('pl-PL').split(', ')[1].split(':') || [' ', ' ', ' ', ' '];
// // [H,H,M,M] - ZABIJCIE MNIE BŁAGAM
// const dateDigits = [...dateArray[0].split(''), ...dateArray[1].split('')];
// const routeTo = timetable.route.split('|')[1];
// list.push({
// trainNumber: `${timetable.category} ${train.trainNo}`,
// timetableId: timetable.timetableId,
// routeTo: routeTo,
// routeVia: routeVia,
// date,
// dateDigits,
// delayMinutes: departureDelay,
// checkpointName: this.selectedCheckpointName.toLowerCase(),
// arrivalTimestamp,
// departureTimestamp,
// tableValues: {
// routeTo: '',
// routeVia: '',
// },
// });
// if (!this.departureRoutes.includes(routeVia)) this.departureRoutes.push(routeVia);
// if (!this.departureRoutes.includes(routeTo)) this.departureRoutes.push(routeTo);
// return list;
// }, [] as ITableRow[]);
// this.updatedDepartureList = updatedDepartureList;
// },
async fetchDepartureList() {
const trainsAPIResponse: ITrainResponse[] = await (
await fetch(`${import.meta.env.VITE_STACJOWNIK_API_URL}/api/getActiveTrainList`)
).json();
if (!trainsAPIResponse) return;
this.apiTrainData = trainsAPIResponse;
// this.refreshTable(trainsAPIResponse);
},
},
});
</script>
<style lang="scss" scoped>
/* ****** ANIMATION ****** */
.slot-anim {
&-enter-active,
&-leave-active {
transition: all 50ms ease-in-out;
}
&-enter-from,
&-leave-to {
transform: rotateX(90deg) perspective(200px);
}
}
.digit-slot-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
}
}
/* ************** */
.filters {
display: flex;
justify-content: space-between;
padding: 0.25em 0;
gap: 0.5em;
}
.wrapper {
width: 1200px;
height: 650px;
display: flex;
flex-direction: column;
}
.top-pane {
background-color: white;
color: black;
height: 180px;
display: flex;
flex-direction: column;
justify-content: space-between;
.title {
padding: 0;
font-size: 3.5em;
}
.headers {
display: grid;
grid-template-columns: 1fr 1fr 2fr 2fr 1fr;
gap: 0 10px;
padding: 0 10px;
text-align: center;
font-size: 1.35em;
}
}
.table {
background: white;
flex-grow: 1;
display: grid;
grid-template-rows: repeat(7, 1fr);
gap: 5px 0;
}
.row {
&-content {
display: grid;
grid-template-columns: 1fr 1fr 2fr 2fr 1fr;
gap: 0 10px;
padding: 0 10px;
height: 100%;
align-items: center;
color: white;
font-size: 1.2em;
background: #1a1a1a;
span {
display: flex;
justify-content: center;
}
}
}
.departure-date {
background: black;
span {
background: black;
height: 2em;
line-height: 2em;
flex-grow: 2;
width: 100%;
}
}
.slider-slot {
background: #010101;
width: 100%;
height: 2em;
line-height: 2em;
}
</style>
+5 -3
View File
@@ -1,4 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App).use(router).mount('#app');
createApp(App).mount('#app')
+23
View File
@@ -0,0 +1,23 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import HomeView from './views/HomeView.vue';
import PragotronView from './views/PragotronView.vue';
const routes: RouteRecordRaw[] = [
{
path: '/',
component: HomeView,
},
{
path: '/board',
component: PragotronView,
props: (route) => ({ stationName: route.query.name }),
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
@@ -1,4 +1,4 @@
export default interface IStationData {
export default interface ISceneryData {
stationName: string;
nameAbbreviation: string;
stationCheckpoints: string[];
+1
View File
@@ -4,6 +4,7 @@ interface ITableRowValues {
currentRouteToIndex: number;
currentRouteViaIndex: number;
currentDateDigitIndex: number;
dateDigits: string[],
}
+72
View File
@@ -0,0 +1,72 @@
<template>
<div class="home-view">
<div class="scenery-selector">
<h1>Scenerie <span class="text--accent">online</span></h1>
<ul class="scenery-list" v-if="dataLoaded">
<li v-for="(stationName, i) in onlineStations">
<span v-if="i > 0">&bull;</span>
<button class="btn--text" @click="handleClick(stationName)">
{{ stationName }}
</button>
</li>
</ul>
<div v-else>Ładowanie listy aktywnych scenerii...</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { IOnlineStationsResponse } from '../types/IOnlineStationsResponse';
export default defineComponent({
data() {
return {
onlineStations: [] as string[],
dataLoaded: false,
};
},
async mounted() {
const stationsAPIResponse: IOnlineStationsResponse = await (
await fetch('https://api.td2.info.pl:9640/?method=getStationsOnline')
).json();
this.dataLoaded = true;
this.onlineStations = stationsAPIResponse.message
.reduce((acc, station) => {
if (station.region != 'eu') return acc;
if (!station.isOnline) return acc;
acc.push(station.stationName);
return acc;
}, [] as string[])
.sort((s1, s2) => (s1 > s2 ? 1 : -1));
},
methods: {
handleClick(stationName: string) {
this.$router.push(`/board?name=${stationName.replace(/ /g, '_')}`);
// this.selectedStation = station;
},
},
});
</script>
<style ;ang="scss" scoped>
ul.scenery-list {
display: flex;
justify-content: center;
flex-wrap: wrap;
padding: 1em;
margin: 0 auto;
font-size: 1.3em;
max-width: 1000px;
}
</style>
+511
View File
@@ -0,0 +1,511 @@
<template>
<div class="pragotron">
<div class="pragotron_content">
<div class="filters">
<div>
<label>
<input type="checkbox" v-model="includeNonPassenger" />
Relacje niepasażerskie
</label>
<label>
<input type="checkbox" v-model="includeArrivals" />
Relacje kończące bieg
</label>
</div>
<div>
<label for="checkpoint">
Posterunek:
<select id="checkpoint" v-model="selectedCheckpointName">
<option v-for="cp in selectedStation?.stationCheckpoints" :value="cp">{{ cp }}</option>
</select>
</label>
</div>
</div>
<div class="wrapper">
<div class="top-pane">
<span class="title">
<div>{{ selectedCheckpointName.toUpperCase() }}</div>
</span>
<div class="headers">
<span>GODZ.</span>
<span>POCIĄG</span>
<span>PRZEZ</span>
<span>DO STACJI</span>
<span>OPÓŹNIONY</span>
</div>
</div>
<div class="table">
<div class="row" v-for="(departure, i) in filledTable" :key="i">
<div class="row-content">
<span class="departure-date">
<transition name="slot-anim" mode="out-in">
<span class="slider-slot-digit" :key="departure.tableValues.dateDigits[0]">{{
departure.tableValues.dateDigits[0]
}}</span>
</transition>
<transition name="slot-anim" mode="out-in">
<span class="slider-slot-digit" :key="departure.tableValues.dateDigits[1]">
{{ departure.tableValues.dateDigits[1] }}</span
>
</transition>
<span :key="departure.tableValues.dateDigits[1]"> : </span>
<transition name="slot-anim" mode="out-in">
<span class="slider-slot-digit" :key="departure.tableValues.dateDigits[2]">
{{ departure.tableValues.dateDigits[2] }}
</span>
</transition>
<transition name="slot-anim" mode="out-in">
<span class="slider-slot-digit" :key="departure.tableValues.dateDigits[3]">
{{ departure.tableValues.dateDigits[3] }}
</span>
</transition>
</span>
<span class="train-class">
<transition name="slot-anim" mode="out-in">
<div class="slider-slot" :key="departure.trainNumber">{{ departure.trainNumber }}</div>
</transition>
</span>
<span class="route-via">
<transition name="slot-anim" mode="out-in">
<div class="slider-slot" :key="departure.tableValues.routeVia">
{{ abbrevStationName(departure.tableValues.routeVia) }}
</div>
</transition>
</span>
<span class="route-to">
<transition name="slot-anim" mode="out-in">
<div class="slider-slot" :key="departure.tableValues.routeTo">
{{ abbrevStationName(departure.tableValues.routeTo) }}
</div>
</transition>
</span>
<span class="delay">
<transition name="slot-anim" mode="out-in">
<div class="slider-slot" :key="departure.delayMinutes">
{{ departure.delayMinutes > 0 ? departure.delayMinutes + ' min' : '' }}
</div>
</transition>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import stationAbbrevsJSON from '../data/stationAbbrevs.json';
import routeValues from '../data/routeValues.json';
import { ITimetableStop, ITrainResponse } from '../types/ITrainResponse';
import { ITableRow } from '../types/ITableRow';
import { ISceneryResponse } from '../types/ISceneryReponse';
import ISceneryData from '../types/ISceneryData';
const stationAbbrevs: { [key: string]: string } = stationAbbrevsJSON;
const departureInfoEmptyObj: ITableRow = {
timetableId: -1,
routeTo: '',
routeVia: '',
trainNumber: '',
date: new Date(0),
dateDigits: [' ', ' ', ' ', ' '],
arrivalTimestamp: 0,
departureTimestamp: 0,
checkpointName: '',
delayMinutes: 0,
tableValues: {
routeTo: ' ',
routeVia: ' ',
currentRouteToIndex: 0,
currentRouteViaIndex: 0,
currentDateDigitIndex: 0,
dateDigits: [' ', ' ', ' ', ' '],
},
};
export default defineComponent({
props: {
stationName: {
type: String,
required: true,
},
},
data: () => ({
currentStationName: '',
sceneriesInfo: [] as ISceneryData[],
includeNonPassenger: true,
includeArrivals: true,
apiTrainData: [] as ITrainResponse[],
animationFrameIndex: 0,
intervalIndex: 0,
lastRefreshTime: 0,
departureTable: new Array(7).fill(0).map(() => ({ ...departureInfoEmptyObj })) as ITableRow[],
departureRoutes: [''],
dateDigits: [' ', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
currentRouteIndex: 0,
currentDateDigitIndex: 0,
stationAbbrevs: stationAbbrevs as { [key: string]: string },
selectedCheckpointName: '',
}),
async created() {
await this.fetchSceneryInfo();
this.selectDefaultCheckpoint();
},
activated() {
this.selectDefaultCheckpoint();
this.shuffleRoutes();
this.fetchDepartureList();
this.intervalIndex = setInterval(() => {
this.fetchDepartureList();
}, 30000);
this.animationFrameIndex = requestAnimationFrame(this.update);
},
deactivated() {
cancelAnimationFrame(this.animationFrameIndex);
clearInterval(this.intervalIndex);
},
computed: {
selectedStation() {
return this.sceneriesInfo.find(({ stationName }) => stationName == this.stationName.replace(/_/g, ' '));
},
filledTable() {
const filteredData = this.apiTrainData
.reduce((list, train) => {
if (!train.timetable) return list;
const timetable = train.timetable;
const stopInfo: ITimetableStop | undefined = timetable.stopList.find(
(sp) => sp.stopNameRAW.toLowerCase() == this.selectedCheckpointName.toLowerCase()
);
if (!stopInfo || stopInfo.confirmed) return list;
const stopInfoIndex = timetable.stopList.indexOf(stopInfo);
const { departureTimestamp, departureDelay, arrivalTimestamp, departureLine } = stopInfo;
const routeVia =
timetable.stopList.find((stop, i) => {
return (
i > stopInfoIndex &&
// i < timetable.stopList.length - 1,
stop.stopName.includes('strong') &&
stop.stopTime &&
stop.stopTime > 0
);
})?.stopNameRAW || '';
const date = departureLine ? new Date(departureTimestamp) : new Date(arrivalTimestamp);
// [HH, MM, SS] - nienawidzę dat w JavaScripcie
const dateArray = date.toLocaleString('pl-PL').split(', ')[1].split(':') || [' ', ' ', ' ', ' '];
// [H,H,M,M] - ZABIJCIE MNIE BŁAGAM
const dateDigits = [...dateArray[0].split(''), ...dateArray[1].split('')];
const routeTo = timetable.route.split('|')[1];
list.push({
trainNumber: `${timetable.category} ${train.trainNo}`,
timetableId: timetable.timetableId,
routeTo: routeTo,
routeVia: routeVia,
date,
dateDigits,
delayMinutes: departureDelay,
checkpointName: this.selectedCheckpointName.toLowerCase(),
arrivalTimestamp,
departureTimestamp,
tableValues: {
routeTo: '',
routeVia: '',
dateDigits: [' ', ' ', ' ', ' '],
currentRouteToIndex: 0,
currentRouteViaIndex: 0,
currentDateDigitIndex: 0,
},
});
if (!this.departureRoutes.includes(routeVia)) this.departureRoutes.push(routeVia);
if (!this.departureRoutes.includes(routeTo)) this.departureRoutes.push(routeTo);
return list;
}, [] as ITableRow[])
.filter(
(dep) =>
(this.includeNonPassenger || !/^[T|L|Z|P]/g.test(dep.trainNumber)) &&
(this.includeArrivals || dep.departureTimestamp)
)
.sort((dep1, dep2) => (dep1.date?.getTime() || 0) - (dep2.date?.getTime() || 0));
for (let i = 0; i < this.departureTable.length; i++) {
if (i <= filteredData.length - 1) {
const updateInfo = filteredData[i];
const existingInfo = this.departureTable[i];
this.departureTable[i] = { ...updateInfo };
this.departureTable[i].tableValues.routeTo = existingInfo.routeTo;
this.departureTable[i].tableValues.routeVia = existingInfo.routeVia;
this.departureTable[i].tableValues.dateDigits = [...existingInfo.tableValues.dateDigits];
} else {
this.departureTable[i] = {
...this.departureTable[i],
timetableId: -1,
routeTo: '',
routeVia: '',
trainNumber: '',
date: new Date(0),
dateDigits: [' ', ' ', ' ', ' '],
arrivalTimestamp: 0,
departureTimestamp: 0,
checkpointName: '',
delayMinutes: 0,
};
}
}
return this.departureTable;
},
},
methods: {
async fetchSceneryInfo() {
const sceneryInfoRes: ISceneryResponse[] = await (
await fetch(`${import.meta.env.VITE_STACJOWNIK_API_URL}/api/getSceneries`)
).json();
this.sceneriesInfo = sceneryInfoRes.map((stationData) => ({
stationName: stationData.name,
stationCheckpoints:
stationData.checkpoints?.length > 0 ? stationData.checkpoints.split(';') : [stationData.name],
nameAbbreviation: '',
}));
},
selectDefaultCheckpoint() {
this.selectedCheckpointName = this.selectedStation?.stationCheckpoints[0] || this.stationName;
},
abbrevStationName(name: string) {
return (name in stationAbbrevs ? stationAbbrevs[name] : name).toUpperCase();
},
update(time: number) {
if (time >= this.lastRefreshTime + 125) {
this.updateTableRows();
this.currentRouteIndex = (this.currentRouteIndex + 1) % this.departureRoutes.length;
// this.currentDateDigitIndex = (this.currentDateDigitIndex + 1) % this.dateDigits.length;
this.lastRefreshTime = time;
}
requestAnimationFrame(this.update);
},
// d = 0 -> time = time
// d = time -> time2 = time2-time
updateTableRows() {
this.departureTable.forEach((dep, i) => {
if (dep.tableValues.routeTo.toLowerCase() != dep.routeTo.toLowerCase()) {
dep.tableValues.routeTo = this.departureRoutes[dep.tableValues.currentRouteToIndex];
dep.tableValues.currentRouteToIndex = (dep.tableValues.currentRouteToIndex + 1) % this.departureRoutes.length;
}
if (dep.tableValues.routeVia.toLowerCase() != dep.routeVia.toLowerCase()) {
dep.tableValues.routeVia = this.departureRoutes[dep.tableValues.currentRouteViaIndex];
dep.tableValues.currentRouteViaIndex =
(dep.tableValues.currentRouteViaIndex + 1) % this.departureRoutes.length;
}
dep.tableValues.dateDigits.forEach((digit, j) => {
if (dep.dateDigits[j] != digit)
dep.tableValues.dateDigits[j] = this.dateDigits[dep.tableValues.currentDateDigitIndex];
});
// Poprawić do wszystkich
dep.tableValues.currentDateDigitIndex = (dep.tableValues.currentDateDigitIndex + 1) % this.dateDigits.length;
});
},
shuffleRoutes() {
for (let i = 0; i < 25; i++) {
const randIndex = Math.floor(Math.random() * routeValues.length);
const randRoute = routeValues[randIndex];
// this.departureRoutes.push(randRoute);
}
this.departureRoutes.sort(() => Math.random() - 0.5);
},
async fetchDepartureList() {
const trainsAPIResponse: ITrainResponse[] = await (
await fetch(`${import.meta.env.VITE_STACJOWNIK_API_URL}/api/getActiveTrainList`)
).json();
if (!trainsAPIResponse) return;
this.apiTrainData = trainsAPIResponse;
},
},
});
</script>
<style lang="scss" scoped>
/* ****** ANIMATION ****** */
.slot-anim {
&-enter-active,
&-leave-active {
transition: all 50ms ease-in-out;
}
&-enter-from,
&-leave-to {
transform: rotateX(90deg) perspective(200px);
}
}
.digit-slot-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
}
}
/* ************** */
.filters {
display: flex;
justify-content: space-between;
padding: 0.25em 0;
gap: 0.5em;
}
.wrapper {
width: 1200px;
height: 650px;
display: flex;
flex-direction: column;
}
.top-pane {
background-color: white;
color: black;
height: 180px;
display: flex;
flex-direction: column;
justify-content: space-between;
.title {
padding: 0;
font-size: 3.5em;
}
.headers {
display: grid;
grid-template-columns: 1fr 1fr 2fr 2fr 1fr;
gap: 0 10px;
padding: 0 10px;
text-align: center;
font-size: 1.35em;
}
}
.table {
background: white;
flex-grow: 1;
display: grid;
grid-template-rows: repeat(7, 1fr);
gap: 5px 0;
}
.row {
&-content {
display: grid;
grid-template-columns: 1fr 1fr 2fr 2fr 1fr;
gap: 0 10px;
padding: 0 10px;
height: 100%;
align-items: center;
color: white;
font-size: 1.2em;
background: #1a1a1a;
span {
display: flex;
justify-content: center;
}
}
}
.departure-date {
background: black;
span {
background: black;
height: 2em;
line-height: 2em;
flex-grow: 2;
width: 100%;
}
}
.slider-slot {
background: #010101;
width: 100%;
height: 2em;
line-height: 2em;
}
</style>