Compare commits

...

16 Commits

13 changed files with 223 additions and 31 deletions

25
README.md Normal file
View File

@@ -0,0 +1,25 @@
# Traveldrafter
Keep track over multiple different connections while traveling trains.
## Run
```
nix run git+https://git.clerie.de/clerie/traveldrafter.git
```
The web frontend is served under <http://localhost:3000/>.
In production you probably would want to serve dir `node_modules/traveldrafter/web` directory as the service root from a webserver like nginx directly and only proxy the `/api` route to the application.
## Development
Pass the path to the directory of the frontend scripts as `WEBDIR` environemnt var.
```
git clone https://git.clerie.de/clerie/traveldrafter.git
cd traveldrafter
WEBDIR=web nix run
```

12
app.js
View File

@@ -8,9 +8,13 @@ import {profile as dbnavProfile} from 'db-vendo-client/p/dbnav/index.js';
import {mapRouteParsers} from 'db-vendo-client/lib/api-parsers.js';
import {createHafasRestApi as createApi} from 'hafas-rest-api';
const webdir_from_module = path.join(path.dirname(fileURLToPath(import.meta.url)), 'web');
const config = {
hostname: process.env.HOSTNAME || 'localhost',
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
port: process.env.HTTP_PORT ? parseInt(process.env.HTTP_PORT) : 3000,
address: process.env.HTTP_ADDRESS || '::1',
webdir: process.env.WEBDIR || webdir_from_module,
name: 'db-vendo-client',
description: 'db-vendo-client',
homepage: 'https://github.com/public-transport/db-vendo-client',
@@ -26,8 +30,6 @@ const config = {
};
const start = async () => {
const webdir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'web');
const app = express();
const vendo = createClient(
@@ -38,9 +40,9 @@ const start = async () => {
const api = await createApi(vendo, config);
app.use("/api", api);
app.use('/', express.static(webdir));
app.use('/', express.static(config.webdir));
app.listen(config.port, (err) => {
app.listen(config.port, config.address, (err) => {
if (err) {
console.error(err);
}

View File

@@ -21,6 +21,8 @@
npmConfigHook = pkgs.importNpmLock.npmConfigHook;
dontNpmBuild = true;
meta.mainProgram = "traveldrafter";
};
default = self.packages.${system}.traveldrafter;
});

View File

@@ -69,11 +69,22 @@ export class LegsDataStore {
DataStore.set("legs", this.data);
}
remember(trip_id, origin_id, destination_id) {
remember(trip_id, origin_location_id, destination_location_id) {
this.data = this.data.filter((leg) => {
return !(leg.trip_id == trip_id && leg.origin_location_id == origin_location_id && leg.destination_location_id == destination_location_id);
});
this.data.push({
trip_id: trip_id,
origin_location_id: origin_id,
destination_location_id: destination_id,
origin_location_id: origin_location_id,
destination_location_id: destination_location_id,
});
this.write();
}
forget(trip_id, origin_location_id, destination_location_id) {
this.data = this.data.filter((leg) => {
return !(leg.trip_id == trip_id && leg.origin_location_id == origin_location_id && leg.destination_location_id == destination_location_id);
});
this.write();

View File

@@ -11,5 +11,7 @@ export function EL(type, properties) {
Object.assign(el.style, properties.style);
}
el.on = (event_name, callback) => el.addEventListener(event_name, callback);
return el;
}

View File

@@ -1,18 +1,22 @@
import { EL } from "./dom.js";
import { displayTripDetails } from "./trip-details.js";
import { displayLegDetails } from "./leg-details.js";
import { fetchTrip } from './api.js';
let element_board = document.querySelector("#drafting-board-content");
export function addJourneyToDraftingBoard(journey) {
let awaiting_promises = [];
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 => {
awaiting_promises.push(fetchTrip(leg.tripId).then(result => {
window.dataStore.trips.remember(result.trip);
});
}));
}
drawDraftingBoard();
Promise.all(awaiting_promises)
.then(results => {
drawDraftingBoard();
});
}
export function drawDraftingBoard() {
@@ -59,8 +63,45 @@ export function drawDraftingBoard() {
return leg;
});
display_legs = display_legs.toSorted((leg_a, leg_b) => {
if (leg_a.origin_location_id == leg_b.origin_location_id) {
if (leg_a.origin_location.departure > leg_b.origin_location.departure) {
return 1;
} else {
return -1;
}
} else if (leg_a.destination_location_id == leg_b.destination_location_id) {
if (leg_a.destination_location.arrival > leg_b.destination_location.arrival) {
return 1;
} else {
return -1;
}
} else if (leg_a.destination_location_id == leg_b.origin_location_id) {
if (leg_a.destination_location.arrival > leg_b.origin_location.departure) {
return 1;
} else {
return -1;
}
} else if (leg_a.origin_location_id == leg_b.destination_location_id) {
if (leg_a.origin_location.departure > leg_b.destination_location.arrival) {
return 1;
} else {
return -1;
}
} else {
if (leg_a.origin_location.departure > leg_b.origin_location.departure) {
return 1;
} else {
return -1;
}
}
});
let rows = display_legs.length;
element_board.style.setProperty("--drafting-board-number-locations", display_locations.length);
element_board.style.setProperty("--drafting-board-number-legs", rows);
for (let display_location of display_locations) {
let el_location_name = EL("div", {
class: [ "station-name" ],
@@ -81,7 +122,7 @@ export function drawDraftingBoard() {
"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,
"grid-row-end": rows + 3,
},
});
@@ -120,8 +161,7 @@ export function drawDraftingBoard() {
el_leg.innerText = display_leg.trip?.line?.name;
el_leg.addEventListener("click", event => {
console.log(display_leg.trip_id);
displayTripDetails(display_leg.trip_id);
displayLegDetails(display_leg.trip_id, display_leg.origin_location_id, display_leg.destination_location_id);
});
element_board.appendChild(el_leg);
@@ -155,6 +195,9 @@ export function getLocationWithLeastInboundTrips(locations, legs) {
let n_a = numberOfInboundLegs(location_a);
let n_b = numberOfInboundLegs(location_b);
// 1: b vor a
// -1: a vor b
if (n_a == n_b) {
return 0;
} else if (n_a > n_b) {
@@ -175,7 +218,7 @@ export function sortLocations(sorted_locations, unsorted_locations, legs) {
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);
legs = legs.filter(leg => leg.destination_location_id != selected_location_id && leg.origin_location_id != selected_location_id);
return sortLocations(sorted_locations.concat([ selected_location_id ]), unsorted_locations, legs);
}

View File

@@ -9,14 +9,19 @@
</head>
<body>
<div id="menu">
<div id="journeys-search-button">Search journeys</div>
<div class="menu-item">
<h1>Traveldrafter</h1>
</div>
<div id="journeys-search-button" class="menu-item menu-button">
<div>Search journeys</div>
</div>
</div>
<div id="drafting-board">
<div id="drafting-board-content"></div>
</div>
<div id="journeys-search" class="popup">
<div id="journeys-search" class="popup popup-hidden">
<div class="popup-close">&times;</div>
<div id="journeys-search-content" class="popup-content">
<div class="container">
@@ -28,7 +33,7 @@
</div>
</div>
<div id="locations-search" class="popup">
<div id="locations-search" class="popup popup-hidden">
<div class="popup-close">&times;</div>
<div id="locations-search-content" class="popup-content">
<div class="container">
@@ -38,7 +43,16 @@
</div>
</div>
<div id="trip-details" class="popup">
<div id="leg-details" class="popup popup-hidden">
<div class="popup-close">&times;</div>
<div id="leg-details-content" class="popup-content">
<div class="container">
<div id="leg-details-response"></div>
</div>
</div>
</div>
<div id="trip-details" class="popup popup-hidden">
<div class="popup-close">&times;</div>
<div id="trip-details-content" class="popup-content">
<div class="container">

View File

@@ -51,6 +51,6 @@ export function attachJourneysSearch(search_element) {
element_from.value = "";
element_to.value = "";
element_result.innerText = "";
element_journeys_search.style.display = "block";
element_journeys_search.popupShow();
});
}

37
web/leg-details.js Normal file
View File

@@ -0,0 +1,37 @@
import { EL } from "./dom.js";
import { displayTripDetails } from "./trip-details.js";
import { drawDraftingBoard } from './drafting-board.js';
let element_leg_details = document.querySelector("#leg-details");
let element_response = document.querySelector("#leg-details-response");
export function displayLegDetails(trip_id, origin_location_id, destination_location_id) {
let trip = window.dataStore.trips.data[trip_id];
let origin_location = window.dataStore.locations.data[origin_location_id];
let destination_location = window.dataStore.locations.data[destination_location_id];
element_response.innerText = "";
let el_headline = EL("h1", {});
el_headline.innerText = trip.line.name + ": " + origin_location.name + " -> " + destination_location.name;
element_response.appendChild(el_headline);
let el_display_trip = EL("div", {});
el_display_trip.innerText = "Display full trip";
el_display_trip.on("click", event => {
displayTripDetails(trip_id);
});
element_response.appendChild(el_display_trip);
let el_remove_leg = EL("div", {});
el_remove_leg.innerText = "Remove leg";
el_remove_leg.on("click", event => {
window.dataStore.legs.forget(trip_id, origin_location_id, destination_location_id);
drawDraftingBoard();
element_leg_details.popupHide();
});
element_response.appendChild(el_remove_leg);
element_leg_details.popupShow();
}

View File

@@ -26,7 +26,7 @@ function createLocationElement(location) {
location_element.addEventListener("click", event => {
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";
element_locations_search.popupHide();
});
return location_element;
@@ -46,6 +46,6 @@ export function attachLocationsSearch(search_element) {
let el = createLocationElement(location);
element_response.appendChild(el);
}
element_locations_search.style.display = "block";
element_locations_search.popupShow();
});
}

View File

@@ -1,7 +1,15 @@
export function setupPopups() {
document.querySelectorAll(".popup").forEach(element => {
element.popupShow = () => {
element.classList.remove("popup-hidden");
};
element.popupHide = () => {
element.classList.add("popup-hidden");
};
});
document.querySelectorAll(".popup-close").forEach(element => {
element.addEventListener("click", event => {
event.target.parentElement.style.display="none";
event.target.parentElement.popupHide();
});
});
}

View File

@@ -9,6 +9,38 @@ body {
background-color: #f5a4d1;
}
#menu {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 50px;
background-color: #ff85c9;
border-bottom-style: solid;
border-bottom-width: 1px;
}
#menu .menu-item {
float: left;
height: 100%;
padding: 10px;
padding-top: auto;
padding-bottom: auto;
}
#menu .menu-button:hover {
background-color: #ff4bb0;
border-left-style: solid;
border-left-width: 1px;
padding-left: 9px;
border-right-style: solid;
border-right-width: 1px;
padding-right: 9px;
}
.container {
margin-right: auto;
margin-left: auto;
@@ -29,6 +61,10 @@ body {
background-color: rgba(0, 0, 0, 0.9);
color: white;
overflow: scroll;
}
.popup-hidden {
display: none;
}
@@ -54,6 +90,10 @@ body {
margin-bottom: 20px;
}
.popup h1 {
margin-bottom: 30px;
}
.popup input {
color: white;
}
@@ -70,22 +110,25 @@ input.form-control {
#drafting-board {
width: 100%;
height: 100vh;
padding: 20px;
padding-top: 70px;
overflow: scroll;
}
#drafting-board-content {
display: grid;
min-width: 1000px;
min-height: 300px;
min-width: 100%;
min-height: 100%;
grid-template-columns: repeat(auto-fit, minmax(200px, auto) 2px minmax(200px, auto));
}
--drafting-board-number-locations: 0;
--drafting-board-number-legs: 0;
#drafting-board div {
min-height: 0px;
grid-template-columns: repeat(var(--drafting-board-number-locations), minmax(200px, auto) 2px minmax(200px, auto));
grid-template-rows: min-content repeat(var(--drafting-board-number-legs), minmax(0px, min-content)) auto;
}
#drafting-board .station-name {

View File

@@ -6,11 +6,16 @@ let element_response = document.querySelector("#trip-details-response");
export function displayTripDetails(trip_id) {
element_response.innerHTML = "Loading…";
element_trip_details.style.display = "block";
element_trip_details.popupShow();
fetchTrip(trip_id).then(result => {
window.dataStore.trips.remember(result.trip);
element_response.innerText = "";
let el_headline = EL("h1", {});
el_headline.innerText = result.trip.line.name + ": " + result.trip.destination.name;
element_response.appendChild(el_headline);
for (let stopover of result.trip.stopovers) {
let el = EL("div", {});
el.innerText = stopover.stop.name;