587 lines
22 KiB
Vue
587 lines
22 KiB
Vue
<template>
|
|
<Navbar />
|
|
|
|
<div id="preferenceDropdown" class="dropdown">
|
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
|
|
Map Filters
|
|
</button>
|
|
<div style="padding-bottom: 7px;" id="dropMenu" class="dropdown-menu" aria-labelledby="dropdownMenuButton1" v-on:click.stop="handleClick">
|
|
<div id="prefHeader">STATIONS</div>
|
|
<div class="container-fluid" @change="decideShowStations();">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="showMainlandStations" v-model="showMainlandStations"/>
|
|
<label class="form-check-label" for="showMainlandStations">Mainline Stations</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="showDARTStations" v-model="showDARTStations"/>
|
|
<label class="form-check-label" for="showDARTStations">DART Stations</label>
|
|
</div>
|
|
</div>
|
|
<div id="prefHeader">TRAINS</div>
|
|
<div class="container-fluid" @change="decideShowTrains();">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="showMainland" v-model="showMainland"/>
|
|
<label class="form-check-label" for="showMainland">Mainline Trains</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="showLate" v-model="showLate"/>
|
|
<label class="form-check-label" for="showLate">Late Trains</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="showOnTime" v-model="showOnTime"/>
|
|
<label class="form-check-label" for="showOnTime">On-time Trains</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="showDART" v-model="showDART"/>
|
|
<label class="form-check-label" for="showDART">DARTs</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="showRunning" v-model="showRunning"/>
|
|
<label class="form-check-label" for="showRunning">Running Trains</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="showTerminated" v-model="showTerminated"/>
|
|
<label class="form-check-label" for="showTerminated">Terminated Trains</label>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="showNotYetRunning" v-model="showNotYetRunning"/>
|
|
<label class="form-check-label" for="showNotYetRunning">Not-yet Running Trains</label>
|
|
</div>
|
|
</div>
|
|
<button id="savePref" class="btn btn-outline-info" v-if="store.loggedIn" @click="postPreferences()">Save Preferences</button>
|
|
</div>
|
|
</div>
|
|
|
|
<transition id="sidebar" name="slideLeft">
|
|
<div v-if="store.displaySelectedTrain && store.selectedTrain">
|
|
<TrainSidebar />
|
|
</div>
|
|
<div v-else-if="store.displaySelectedStation && store.selectedStation">
|
|
<StationSidebar />
|
|
</div>
|
|
</transition>
|
|
|
|
<ol-map :loadTilesWhileAnimating="true" :loadTilesWhileInteracting="true" style="position:absolute; height:90.6vh; width:100%;">
|
|
<ol-view ref="view" :center="center" :rotation="rotation" :zoom="zoom" :projection="projection" />
|
|
<ol-tile-layer>
|
|
<ol-source-osm />
|
|
</ol-tile-layer>
|
|
|
|
<div v-if="(!store.isWaitingForLoginStatus && !store.loggedIn) || (store.loggedIn && readyToDisplayMap)">
|
|
<!-- train overlay -->
|
|
<template v-for="coordinate, i in trainCoordinates" :position="inline-block">
|
|
<ol-overlay v-if="showTrains[i]" :position="coordinate" :offset="[-14,-16]">
|
|
<div class="overlay-content" @click="getSelectedTrain(i)">
|
|
<div v-if="getTrainType(i) === 'DART'">
|
|
<img v-if="isTrainRunning(i) && isTrainLate(i)" src="../assets/red-train-tram-solid.png" class="trainMapIcon" alt="Late DART Icon">
|
|
<img v-else-if="isTrainRunning(i) && !isTrainLate(i)" src="../assets/green-train-tram-solid.png" class="trainMapIcon" alt="On-Time DART Icon">
|
|
<img v-else src="../assets/train-tram-solid.svg" class="trainMapIcon" alt="Not Running DART Icon">
|
|
</div>
|
|
<div v-else>
|
|
<img v-if="isTrainRunning(i) && isTrainLate(i)" src="../assets/red-train-solid.png" class="trainMapIcon" alt="Late Train Icon">
|
|
<img v-else-if="isTrainRunning(i) && !isTrainLate(i)" src="../assets/green-train-solid.png" class="trainMapIcon" alt="On-Time Train Icon">
|
|
<img v-else src="../assets/train-solid.svg" class="trainMapIcon" alt="Not Running Train Icon">
|
|
</div>
|
|
</div>
|
|
</ol-overlay>
|
|
</template>
|
|
|
|
<!-- station overlay -->
|
|
<template v-for="coordinate, i in stationCoordinates" :position="inline-block">
|
|
<ol-overlay v-if="showStations[i]" :position="coordinate" :offset="[-14,-16]">
|
|
<div class="overlay-content" @click="getSelectedStation(i)">
|
|
<img src="../assets/station.png" class="stationMapIcon" alt="Station Icon">
|
|
</div>
|
|
</ol-overlay>
|
|
</template>
|
|
</div>
|
|
</ol-map>
|
|
|
|
<div>
|
|
<MarqueeText v-if="publicMessages.length>0" id="publicMessageTicker" :paused="isPaused" :duration="800" :repeat="1"
|
|
@mouseenter="isPaused = !isPaused" @mouseleave="isPaused = false">
|
|
<span v-for="message in publicMessages"> {{ message + " • " }} </span>
|
|
</MarqueeText>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { store } from '../store/store';
|
|
import { fromLonLat } from 'ol/proj.js';
|
|
import { getFunctions, httpsCallable, connectFunctionsEmulator } from "firebase/functions";
|
|
import { createToast } from 'mosha-vue-toastify';
|
|
import 'mosha-vue-toastify/dist/style.css'
|
|
import app from '../api/firebase';
|
|
import Navbar from '../components/Navbar.vue';
|
|
import MarqueeText from 'vue-marquee-text-component';
|
|
import TrainSidebar from '../components/TrainSidebar.vue';
|
|
import StationSidebar from '../components/StationSidebar.vue';
|
|
|
|
export default {
|
|
name: "MapPage",
|
|
|
|
data() {
|
|
const toast = () => {
|
|
createToast(this.toastMessage, {
|
|
hideProgressBar: true,
|
|
timeout: 4000,
|
|
toastBackgroundColor: this.toastBackground
|
|
})
|
|
}
|
|
|
|
return {
|
|
center: fromLonLat([-7.5029786, 53.4494762]),
|
|
projection: 'EPSG:3857',
|
|
zoom: 7,
|
|
rotation: 0,
|
|
|
|
showTrains: [],
|
|
showStations: [],
|
|
trainCoordinates: [],
|
|
stationCoordinates: [],
|
|
allTrains: {},
|
|
allStations: {},
|
|
publicMessages: [],
|
|
isPaused: false,
|
|
readyToDisplayMap: false,
|
|
store,
|
|
|
|
toastMessage: "",
|
|
toastBackground: "",
|
|
toast,
|
|
|
|
showMainlandStations: true,
|
|
showDARTStations: true,
|
|
showLate: true,
|
|
showOnTime: true,
|
|
showMainland: true,
|
|
showDART: true,
|
|
showRunning: true,
|
|
showTerminated: true,
|
|
showNotYetRunning: true,
|
|
}
|
|
},
|
|
|
|
components: {
|
|
Navbar,
|
|
MarqueeText,
|
|
TrainSidebar,
|
|
StationSidebar
|
|
},
|
|
|
|
created() {
|
|
this.readyToDisplayMap = false
|
|
let host = window.location.hostname
|
|
if (host === '127.0.0.1' || host === 'localhost') {
|
|
this.postTrainAndStationData();
|
|
}
|
|
else {
|
|
this.getTrainAndStationData();
|
|
}
|
|
// request new data every 60 seconds
|
|
// window.setInterval(this.getTrainAndStationData, 60000);
|
|
},
|
|
|
|
methods: {
|
|
showToast(message, backgroundColour) {
|
|
this.toastMessage = message
|
|
this.toastBackground = backgroundColour
|
|
this.toast()
|
|
},
|
|
|
|
handleClick(e){
|
|
e.stopPropagation();
|
|
},
|
|
|
|
getPreferences() {
|
|
if (!store.loggedIn) return
|
|
const functions = getFunctions(app);
|
|
let host = window.location.hostname
|
|
if (host === '127.0.0.1' || host == 'localhost') {
|
|
connectFunctionsEmulator(functions, host, 5001);
|
|
}
|
|
|
|
const getPreferencesData = httpsCallable(functions, 'getPreferences')
|
|
getPreferencesData().then((response) => {
|
|
if (response.data.data) {
|
|
this.hasPreferences = true
|
|
this.showMainlandStations = response.data.data["showMainlandStations"]
|
|
this.showDARTStations = response.data.data["showDARTStations"]
|
|
this.showLate = response.data.data["showLate"]
|
|
this.showOnTime = response.data.data["showOnTime"]
|
|
this.showMainland = response.data.data["showMainland"]
|
|
this.showDART = response.data.data["showDART"]
|
|
this.showRunning = response.data.data["showRunning"]
|
|
this.showTerminated = response.data.data["showTerminated"]
|
|
this.showNotYetRunning = response.data.data["showNotYetRunning"]
|
|
|
|
// update the map with the user's preferences
|
|
this.decideShowStations()
|
|
this.decideShowTrains()
|
|
this.readyToDisplayMap = true
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
this.readyToDisplayMap = true
|
|
console.log(error.message)
|
|
})
|
|
},
|
|
|
|
postPreferences() {
|
|
if (!store.loggedIn) return
|
|
let preferences = {
|
|
"showMainlandStations": this.showMainlandStations,
|
|
"showDARTStations": this.showDARTStations,
|
|
"showLate": this.showLate,
|
|
"showOnTime": this.showOnTime,
|
|
"showMainland": this.showMainland,
|
|
"showDART": this.showDART,
|
|
"showRunning": this.showRunning,
|
|
"showTerminated": this.showTerminated,
|
|
"showNotYetRunning": this.showNotYetRunning
|
|
}
|
|
|
|
const functions = getFunctions(app);
|
|
let host = window.location.hostname
|
|
if (host === '127.0.0.1' || host == 'localhost') {
|
|
connectFunctionsEmulator(functions, host, 5001);
|
|
}
|
|
|
|
this.showToast("Saving preferences", "green")
|
|
const postPreferencesData = httpsCallable(functions, 'postPreferences')
|
|
postPreferencesData(preferences).then(() => {
|
|
this.readyToDisplayMap = true
|
|
})
|
|
.catch((error) => {
|
|
this.showToast(error.message, "red")
|
|
})
|
|
},
|
|
|
|
// method to determine whether or not to show each train
|
|
decideShowTrains() {
|
|
for (var i=0; i<this.showTrains.length; i++) {
|
|
let isDART = this.getTrainType(i) == "DART";
|
|
if ((this.showRunning && this.allTrains[i]["TrainStatus"][0] == "R") || (this.showTerminated && this.allTrains[i]["TrainStatus"][0] == "T") || this.showNotYetRunning && this.allTrains[i]["TrainStatus"][0] == "N") {
|
|
if ((this.showDART && isDART) || (this.showMainland && !isDART)) {
|
|
this.showTrains[i] = (this.showLate && this.isTrainLate(i)) || (this.showOnTime && !this.isTrainLate(i));
|
|
}
|
|
else {
|
|
this.showTrains[i] = false;
|
|
}
|
|
}
|
|
else {
|
|
this.showTrains[i] = false;
|
|
}
|
|
}
|
|
},
|
|
|
|
// method to determine whether or not to show each station
|
|
decideShowStations() {
|
|
for (var i=0; i<this.showStations.length; i++) {
|
|
let isDARTStation = this.getStationType(i) == "DART";
|
|
this.showStations[i] = (this.showDARTStations && isDARTStation) || (this.showMainlandStations && !isDARTStation);
|
|
}
|
|
},
|
|
|
|
// method to display a selected train
|
|
getSelectedTrain(i) {
|
|
store.setSelectedTrain(this.allTrains[i]);
|
|
if (store.displaySelectedStation) store.setDisplaySelectedStation(false);
|
|
store.setDisplaySelectedTrain(true);
|
|
},
|
|
|
|
// method to display a selected station
|
|
getSelectedStation(i) {
|
|
store.setSelectedStation(this.allStations[i]);
|
|
if (store.displaySelectedTrain) store.setDisplaySelectedTrain(false);
|
|
store.setDisplaySelectedStation(true);
|
|
},
|
|
|
|
// method to determine whether or not a selected train is late
|
|
isTrainLate(i) {
|
|
// check if the train is running
|
|
if (this.allTrains[i]["TrainStatus"][0] == "R") {
|
|
let publicMessage = this.allTrains[i]["PublicMessage"][0];
|
|
let startTimeStr = publicMessage.indexOf("(");
|
|
|
|
// check if the train is late
|
|
if (publicMessage[startTimeStr+1] != "-" && publicMessage[startTimeStr+1] != "0") {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// method to determine whether or not a selected train is running
|
|
isTrainRunning(i) {
|
|
if (this.allTrains[i]["TrainStatus"][0] == "R") return true;
|
|
else return false;
|
|
},
|
|
|
|
// method that returns the type of train (either "Train" or "DART")
|
|
getTrainType(i) {
|
|
return this.allTrains[i]["TrainType"][0];
|
|
},
|
|
|
|
// method that returns the type of station (either "Train" or "DART")
|
|
getStationType(i) {
|
|
return this.allStations[i]["StationType"][0]
|
|
},
|
|
|
|
// method to fetch live train and station data from the database
|
|
getTrainAndStationData() {
|
|
const functions = getFunctions(app);
|
|
let host = window.location.hostname
|
|
if (host === '127.0.0.1' || host == 'localhost') {
|
|
connectFunctionsEmulator(functions, host, 5001);
|
|
}
|
|
const getTrainData = httpsCallable(functions, 'getLiveTrainData');
|
|
let loader = this.$loading.show({
|
|
loader: 'dots',
|
|
container: this.$refs.container,
|
|
canCancel: false
|
|
});
|
|
|
|
getTrainData().then((response) => {
|
|
try {
|
|
if (!response.data) throw new Error("Error fetching live train data from the database")
|
|
var insights = {
|
|
"totalNumTrains": 0,
|
|
"numRunningTrains": 0,
|
|
"numLateRunningTrains": 0,
|
|
"numTrains": 0,
|
|
"numDarts": 0,
|
|
"totalNumStations": 0,
|
|
"numTrainStations": 0,
|
|
"numDartStations": 0
|
|
};
|
|
|
|
var unorderedTrains = [];
|
|
var currentMessages = [];
|
|
var latest = null;
|
|
var earliest = null;
|
|
var currLatestTime = null;
|
|
var currEarliestTime = null;
|
|
|
|
// create an array of coordinates and hashmap with the key-values {index: JSON obj}
|
|
for (var i=0; i<response.data.length; i++) {
|
|
let train = response.data[i];
|
|
this.allTrains[i] = train;
|
|
this.trainCoordinates[i] = fromLonLat([train["TrainLongitude"][0], train["TrainLatitude"][0]])
|
|
insights["totalNumTrains"] += 1
|
|
|
|
// filling showTrains with the default value - true
|
|
this.showTrains[i] = true;
|
|
if (train["TrainType"][0] == "Train") insights["numTrains"] += 1;
|
|
else if (train["TrainType"][0] == "DART") insights["numDarts"] += 1;
|
|
|
|
// filter out \n in public messages
|
|
train["PublicMessage"][0] = train["PublicMessage"][0].replace(/\\n/g, ". ");
|
|
let publicMessage = train["PublicMessage"][0];
|
|
currentMessages.push(publicMessage);
|
|
|
|
// check if the train is running
|
|
if (train["TrainStatus"][0] == "R") {
|
|
insights["numRunningTrains"] += 1;
|
|
let startTimeStr = publicMessage.indexOf("(");
|
|
let timeEnd = publicMessage.indexOf(" ", startTimeStr+1);
|
|
let num = parseInt(publicMessage.substring(startTimeStr+1, timeEnd))
|
|
unorderedTrains.push({"time": num, "jsonIndex": i})
|
|
|
|
// check if the train is late
|
|
if (publicMessage[startTimeStr+1] != "-" && publicMessage[startTimeStr+1] != "0") {
|
|
insights["numLateRunningTrains"] += 1;
|
|
if (!latest) latest = train;
|
|
|
|
// check for a new latest train
|
|
if (num > currLatestTime) {
|
|
latest = train;
|
|
currLatestTime = num;
|
|
}
|
|
}
|
|
// train is early or ontime
|
|
else {
|
|
if (!earliest) earliest = train;
|
|
// check for a new earliest train (early trains are -x mins late)
|
|
if (num < currEarliestTime) {
|
|
earliest = train;
|
|
currEarliestTime = num;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
insights["percentageLate"] = ((insights["numLateRunningTrains"] / insights["numRunningTrains"]) * 100).toFixed(2);
|
|
insights["percentageNotLate"] = (100 - insights["percentageLate"]).toFixed(2);
|
|
insights["latestTime"] = currLatestTime;
|
|
insights["earliestTime"] = currEarliestTime;
|
|
this.publicMessages = currentMessages;
|
|
|
|
// assign the results to the Vue Store
|
|
store.setEarliestTrain(earliest);
|
|
store.setLatestTrain(latest);
|
|
store.setRawData(response.data);
|
|
store.setOrderedTrains(unorderedTrains);
|
|
|
|
const getStationData = httpsCallable(functions, 'getStationData');
|
|
getStationData().then((response) => {
|
|
if (!response.data) throw new Error("Error fetching station from the database");
|
|
|
|
for (var i=0; i<response.data.length; i++) {
|
|
let station = response.data[i];
|
|
this.allStations[i] = station;
|
|
this.stationCoordinates[i] = fromLonLat([station["StationLongitude"][0], station["StationLatitude"][0]])
|
|
insights["totalNumStations"] += 1
|
|
|
|
// setting the station to show on the map by default - true
|
|
this.showStations[i] = true;
|
|
if (station["StationType"][0] == "DART") insights["numDartStations"] += 1;
|
|
else if (station["StationType"][0] == "Train") insights["numTrainStations"] += 1;
|
|
}
|
|
|
|
store.setInsights(insights);
|
|
loader.hide();
|
|
// request the user's preferences
|
|
this.getPreferences()
|
|
})
|
|
}
|
|
catch (error) {
|
|
loader.hide()
|
|
this.showToast(error.message, "red")
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
loader.hide()
|
|
this.showToast("Error fetching live data", "red")
|
|
})
|
|
},
|
|
|
|
// method to populate the database for local testing
|
|
postTrainAndStationData() {
|
|
const functions = getFunctions(app);
|
|
let host = window.location.hostname
|
|
if (host === '127.0.0.1' || host === 'localhost') {
|
|
connectFunctionsEmulator(functions, host, 5001);
|
|
}
|
|
|
|
const postTrainData = httpsCallable(functions, 'postLiveTrainData');
|
|
postTrainData().then(() => {
|
|
const postStationData = httpsCallable(functions, 'postStationData');
|
|
postStationData().then(() => {
|
|
this.getTrainAndStationData()
|
|
})
|
|
})
|
|
.catch((error) => {
|
|
this.showToast(error.message, "red")
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.overlay-content {
|
|
width: 1%;
|
|
}
|
|
|
|
.trainMapIcon {
|
|
width: 28px;
|
|
height: 32px;
|
|
}
|
|
|
|
.trainMapIcon:hover {
|
|
width:30px;
|
|
height:34px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.stationMapIcon {
|
|
width: 14px;
|
|
height: 17px;
|
|
}
|
|
|
|
.stationMapIcon:hover {
|
|
width: 16px;
|
|
height: 19px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#dropdownMenuButton1 {
|
|
box-shadow: 0 0 5px 2px #6e757dbe;
|
|
}
|
|
|
|
#dropMenu {
|
|
/*In case we want to edit dropdown menu*/
|
|
font-size: 14.6px;
|
|
}
|
|
|
|
#preferenceDropdown {
|
|
position: absolute;
|
|
z-index: 3;
|
|
right: 1%;
|
|
top: 11%;
|
|
}
|
|
|
|
#prefHeader {
|
|
font-size: 18px;
|
|
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
|
|
text-align: center;
|
|
position: relative;
|
|
}
|
|
|
|
#sidebar {
|
|
position: absolute;
|
|
height: 80%;
|
|
width: 20%;
|
|
left: 2%;
|
|
top: 14%;
|
|
z-index: 2;
|
|
text-align: center;
|
|
animation: gradient 15s ease infinite;
|
|
background: linear-gradient(45deg, #ffffff, #fef3f3, #ffffff, #f2f2f2);
|
|
background-size: 100%, 100%;
|
|
box-shadow: 0 0 4px 2px #cccccc;
|
|
overflow: hidden;
|
|
font-family:'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif
|
|
}
|
|
|
|
#savePref {
|
|
left:2%;
|
|
top: 2px;
|
|
width: 95%;
|
|
position: relative;
|
|
}
|
|
.slideLeft-enter-active, .slideLeft-leave-active {
|
|
transition: opacity .5s;
|
|
transition: all 0.8s;
|
|
|
|
}
|
|
.slideLeft-enter-from, .slideLeft-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(-100px);
|
|
}
|
|
|
|
@keyframes gradient {
|
|
0% {
|
|
background-position: 0% 50%;
|
|
}
|
|
50% {
|
|
background-position: 100% 50%;
|
|
}
|
|
100% {
|
|
background-position: 0% 50%;
|
|
}
|
|
}
|
|
|
|
#publicMessageTicker {
|
|
z-index: 3;
|
|
position: absolute;
|
|
bottom:0px;
|
|
width:100%;
|
|
background-color: rgb(255, 255, 125);
|
|
color: black;
|
|
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
|
|
text-align: bottom;
|
|
font-size: 16px;
|
|
}
|
|
</style> |