Compare commits

...

10 Commits

10 changed files with 415 additions and 32 deletions

View File

@@ -1,6 +1,8 @@
export function fetchApi(pathcomponents, query) {
console.log(pathcomponents);
query.pretty = true;
let url = '/api/' + pathcomponents.join("/") + "?" + new URLSearchParams(query).toString();
let url = '/api/' + pathcomponents.map(component => encodeURIComponent(component)).join("/") + "?" + new URLSearchParams(query).toString();
console.log(url);
return fetch(url).then(response => {
if (!response.ok) {
throw new Error("Fetching api failed");
@@ -26,3 +28,7 @@ export function fetchJourneys(from_, to) {
to: to,
});
}
export function fetchTrip(trip_id) {
return fetchApi(["trips", trip_id], {});
}

View File

@@ -1,38 +1,109 @@
export class LocationsDataStore {
static rememberAll(location_list) {
let locations = LocationsDataStore.get();
constructor() {
this.data = {};
this.read();
}
read() {
this.data = DataStore.get("locations") || {};
}
write() {
DataStore.set("locations", this.data);
}
rememberAll(location_list) {
for (let location of location_list) {
locations[location.id] = location;
this.data[location.id] = location;
}
LocationsDataStore.set(locations);
this.write();
}
static get() {
return DataStore.get("locations") || {};
}
rememberAllIfNotExist(location_list) {
for (let location of location_list) {
if (!(location.id in this.data)) {
this.data[location.id] = location;
}
}
static set(value) {
DataStore.set("locations", value);
this.write();
}
}
export class RecentLocationsDataStore {
static remember(location_id) {
let recent_locations = RecentLocationsDataStore.get();
recent_locations = recent_locations.filter((item) => {
constructor() {
this.data = [];
this.read();
}
read() {
this.data = DataStore.get("recent-locations") || [];
}
write() {
DataStore.set("recent-locations", this.data);
}
remember(location_id) {
this.data = this.data.filter((item) => {
return item != location_id;
});
recent_locations.push(location_id);
RecentLocationsDataStore.set(recent_locations);
this.data.push(location_id);
this.write();
}
}
export class LegsDataStore {
constructor() {
this.data = [];
this.read();
}
static get() {
return DataStore.get("recent-locations") || [];
read() {
this.data = DataStore.get("legs") || [];
}
static set(value) {
DataStore.set("recent-locations", value);
write() {
DataStore.set("legs", this.data);
}
remember(trip_id, origin_id, destination_id) {
this.data.push({
trip_id: trip_id,
origin_location_id: origin_id,
destination_location_id: destination_id,
});
this.write();
}
}
export class TripsDataStore {
constructor() {
this.data = {};
this.read();
}
read() {
this.data = DataStore.get("trips") || {};
}
write() {
DataStore.set("trips", this.data);
}
remember(trip) {
this.rememberAll([trip]);
}
rememberAll(trip_list) {
for (let trip of trip_list) {
this.data[trip.id] = trip;
}
this.write();
}
}
@@ -45,6 +116,10 @@ export class DataStore {
window.localStorage.setItem(key, JSON.stringify(value));
}
static locations = LocationsDataStore;
static recent_locations = RecentLocationsDataStore;
constructor() {
this.locations = new LocationsDataStore();
this.recent_locations = new RecentLocationsDataStore();
this.legs = new LegsDataStore();
this.trips = new TripsDataStore();
}
}

15
web/dom.js Normal file
View File

@@ -0,0 +1,15 @@
export function EL(type, properties) {
let el = document.createElement(type);
if ("class" in properties) {
for (let c of properties["class"]) {
el.classList.add(c);
}
}
if ("style" in properties) {
Object.assign(el.style, properties.style);
}
return el;
}

198
web/drafting-board.js Normal file
View File

@@ -0,0 +1,198 @@
import { EL } from "./dom.js";
import { displayTripDetails } from "./trip-details.js";
import { fetchTrip } from './api.js';
let element_board = document.querySelector("#drafting-board-content");
export function addJourneyToDraftingBoard(journey) {
for (let leg of journey.legs.filter(item => !("walking" in item))) {
window.dataStore.legs.remember(leg.tripId, leg.origin.id, leg.destination.id);
fetchTrip(leg.tripId).then(result => {
window.dataStore.trips.remember(result.trip);
});
}
drawDraftingBoard();
}
export function drawDraftingBoard() {
element_board.innerText = "";
let sorted_locations = trackedTripsLocationsSorted();
let display_locations = sorted_locations.map(item => window.dataStore.locations.data[item]);
let grid_location_indexes = {};
let location_offset = 0;
for (let sorted_location of sorted_locations) {
grid_location_indexes[sorted_location] = {
"name-start": (location_offset * 3) + 1,
"name-end": (location_offset * 3) + 4,
"column-start": (location_offset * 3) + 2,
"column-end": (location_offset * 3) + 3,
};
location_offset += 1;
}
function getStopFromTrip(trip_id, location_id) {
for (let stop of window.dataStore.trips.data[trip_id]?.stopovers || []) {
if (stop.stop.id == location_id) {
return stop;
}
}
return undefined;
}
let display_legs = window.dataStore.legs.data.map(leg => {
leg.trip = window.dataStore.trips.data[leg.trip_id];
leg.origin_location = getStopFromTrip(leg.trip_id, leg.origin_location_id);
leg.destination_location = getStopFromTrip(leg.trip_id, leg.destination_location_id);
return leg;
});
let rows = display_legs.length;
for (let display_location of display_locations) {
let el_location_name = EL("div", {
class: [ "station-name" ],
style: {
"grid-column-start": grid_location_indexes[display_location.id]["name-start"],
"grid-column-end": grid_location_indexes[display_location.id]["name-end"],
"grid-row-start": 1,
"grid-row-end": 1,
},
});
el_location_name.innerText = display_location?.name;
element_board.appendChild(el_location_name);
let el_location_column = EL("div", {
class: [ "station-column" ],
style: {
"grid-column-start": grid_location_indexes[display_location.id]["column-start"],
"grid-column-end": grid_location_indexes[display_location.id]["column-end"],
"grid-row-start": 2,
"grid-row-end": rows + 2,
},
});
element_board.appendChild(el_location_column);
}
let leg_offset = 0;
for (let display_leg of display_legs) {
console.log(display_leg);
let el_leg_left = EL("div", {
class: [ "leg-left" ],
style: {
"grid-column-start": grid_location_indexes[display_leg.origin_location_id]["name-start"],
"grid-column-end": grid_location_indexes[display_leg.origin_location_id]["column-start"],
"grid-row-start": leg_offset + 2,
"grid-row-end": leg_offset + 2,
},
});
el_leg_left.appendChild(document.createTextNode(new Date(display_leg.origin_location.departure).toLocaleTimeString()));
el_leg_left.appendChild(EL("br", {}));
el_leg_left.appendChild(document.createTextNode("Gleis " + display_leg.origin_location.departurePlatform));
element_board.appendChild(el_leg_left);
let el_leg = EL("div", {
class: [ "leg" ],
style: {
"grid-column-start": grid_location_indexes[display_leg.origin_location_id]["column-end"],
"grid-column-end": grid_location_indexes[display_leg.destination_location_id]["column-start"],
"grid-row-start": leg_offset + 2,
"grid-row-end": leg_offset + 2,
},
});
el_leg.innerText = display_leg.trip?.line?.name;
el_leg.addEventListener("click", event => {
console.log(display_leg.trip_id);
displayTripDetails(display_leg.trip_id);
});
element_board.appendChild(el_leg);
let el_leg_right = EL("div", {
class: [ "leg-right" ],
style: {
"grid-column-start": grid_location_indexes[display_leg.destination_location_id]["column-end"],
"grid-column-end": grid_location_indexes[display_leg.destination_location_id]["name-end"],
"grid-row-start": leg_offset + 2,
"grid-row-end": leg_offset + 2,
},
});
el_leg_right.appendChild(document.createTextNode(new Date(display_leg.destination_location.arrival).toLocaleTimeString()));
el_leg_right.appendChild(EL("br", {}));
el_leg_right.appendChild(document.createTextNode("Gleis " + display_leg.destination_location.arrivalPlatform));
element_board.appendChild(el_leg_right);
leg_offset += 1;
}
}
export function getLocationWithLeastInboundTrips(locations, legs) {
function numberOfInboundLegs(location) {
return legs.filter(leg => leg.destination_location_id == location).length;
}
let locations_sorted_by_number_of_inbound_legs = locations.toSorted((location_a, location_b) => {
let n_a = numberOfInboundLegs(location_a);
let n_b = numberOfInboundLegs(location_b);
if (n_a == n_b) {
return 0;
} else if (n_a > n_b) {
return 1;
} else {
return -1;
}
});
return locations_sorted_by_number_of_inbound_legs[0];
}
export function sortLocations(sorted_locations, unsorted_locations, legs) {
if (unsorted_locations.length == 0) {
return sorted_locations;
}
let selected_location_id = getLocationWithLeastInboundTrips(unsorted_locations, legs);
unsorted_locations = unsorted_locations.filter(location_id => location_id != selected_location_id);
legs = legs.filter(leg => leg.destination_id != selected_location_id && leg.origin_id != selected_location_id);
return sortLocations(sorted_locations.concat([ selected_location_id ]), unsorted_locations, legs);
}
export function trackedTripsLocationsSorted() {
let passed_locations = Array.from(new Set(
window.dataStore.legs.data.map(leg => leg.origin_location_id)
.concat(
window.dataStore.legs.data.map(leg => leg.destination_location_id)
)
));
let sorted_locations = sortLocations([], passed_locations, window.dataStore.legs.data);
console.log(sorted_locations);
console.log(sorted_locations.map(item => window.dataStore.locations.data[item]?.name));
return sorted_locations;
}

View File

@@ -37,5 +37,14 @@
</div>
</div>
</div>
<div id="trip-details" class="popup">
<div class="popup-close">&times;</div>
<div id="trip-details-content" class="popup-content">
<div class="container">
<div id="trip-details-response"></div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,5 +1,6 @@
import { fetchJourneys } from './api.js';
import { attachLocationsSearch } from './locations-search.js';
import { addJourneyToDraftingBoard } from './drafting-board.js';
let element_journeys_search = document.querySelector("#journeys-search");
let element_from = document.querySelector("#journey-search-from");
@@ -25,9 +26,14 @@ function createJourneyElement(journey) {
let el = document.createElement("div");
for (let leg of journey.legs) {
window.dataStore.locations.rememberAllIfNotExist([leg.origin, leg.destination]);
el.appendChild(createJourneyLegElement(leg));
}
el.addEventListener("click", event => {
addJourneyToDraftingBoard(journey);
});
return el;
}

View File

@@ -1,5 +1,4 @@
import { fetchLocations } from './api.js';
import { DataStore } from './datastore.js';
let element_locations_search = document.querySelector("#locations-search");
let element_query = document.querySelector("#locations-search-query");
@@ -9,7 +8,7 @@ export function setupLocationsSearch() {
element_query.addEventListener("change", (event) => {
element_response.innerText = "Loading…";
fetchLocations(event.target.value).then(result => {
DataStore.locations.rememberAll(result);
window.dataStore.locations.rememberAll(result);
element_response.innerText = "";
result.forEach(location => {
let location_element = createLocationElement(location);
@@ -25,7 +24,7 @@ function createLocationElement(location) {
location_element.dataset.locationId = location.id;
location_element.addEventListener("click", event => {
DataStore.recent_locations.remember(event.target.dataset.locationId);
window.dataStore.recent_locations.remember(event.target.dataset.locationId);
element_locations_search.locationSelectedCallback(event.target.innerText, event.target.dataset.locationId);
element_locations_search.style.display = "none";
});
@@ -41,8 +40,8 @@ export function attachLocationsSearch(search_element) {
};
element_query.value = "";
element_response.innerText = "";
let locations = DataStore.locations.get();
for (let location_id of DataStore.recent_locations.get()) {
let locations = window.dataStore.locations.data;
for (let location_id of window.dataStore.recent_locations.data) {
let location = locations[location_id];
let el = createLocationElement(location);
element_response.appendChild(el);

View File

@@ -5,6 +5,10 @@
box-sizing: border-box;
}
body {
background-color: #f5a4d1;
}
.container {
margin-right: auto;
margin-left: auto;
@@ -65,18 +69,64 @@ input.form-control {
}
#drafting-board {
background-color: #f5a4d1;
width: 100%;
min-height: 50px;
padding: 20px;
overflow: scroll;
}
#drafting-board-content {
border-color: red;
border-width: 1px;
border-style: solid;
display: grid;
min-width: 1000px;
min-height: 300px;
grid-template-columns: repeat(auto-fit, minmax(200px, auto) 2px minmax(200px, auto));
}
#drafting-board div {
min-height: 0px;
}
#drafting-board .station-name {
margin-left: 20px;
margin-right: 20px;
border-bottom-style: solid;
border-bottom-color: black;
border-bottom-width: 2px;
text-align: center;
}
#drafting-board .station-column {
width: 2px;
background-color: black;
}
#drafting-board .leg-left {
margin-top: auto;
margin-bottom: auto;
margin-right: 10px;
text-align: right;
}
#drafting-board .leg {
margin: 10px;
border-style: solid;
border-color: black;
border-width: 1px;
border-radius: 10px;
padding: 5px;
background-color: #ff4bb0;
}
#drafting-board .leg-right {
margin-top: auto;
margin-bottom: auto;
margin-left: 10px;
text-align: left;
}

View File

@@ -2,11 +2,16 @@ import * as Api from './api.js';
import { setupPopups } from "./popup.js";
import { setupLocationsSearch } from './locations-search.js';
import { setupJourneysSearch, attachJourneysSearch } from './journeys-search.js';
import { drawDraftingBoard } from './drafting-board.js';
import { DataStore } from './datastore.js';
window.Api = Api;
window.dataStore = new DataStore();
setupPopups();
setupLocationsSearch();
setupJourneysSearch();
attachJourneysSearch(document.querySelector("#journeys-search-button"));
drawDraftingBoard();

20
web/trip-details.js Normal file
View File

@@ -0,0 +1,20 @@
import { EL } from "./dom.js";
import { fetchTrip } from './api.js';
let element_trip_details = document.querySelector("#trip-details");
let element_response = document.querySelector("#trip-details-response");
export function displayTripDetails(trip_id) {
element_response.innerHTML = "Loading…";
element_trip_details.style.display = "block";
fetchTrip(trip_id).then(result => {
element_response.innerText = "";
for (let stopover of result.trip.stopovers) {
let el = EL("div", {});
el.innerText = stopover.stop.name;
element_response.appendChild(el);
}
});
}