Compare commits
29 Commits
125dc820eb
...
main
Author | SHA1 | Date | |
---|---|---|---|
1c05de0af5 | |||
309d9aab39 | |||
7cb6030387 | |||
e3a4f87fe4 | |||
3b14fa6c51 | |||
098addc4e8 | |||
5b7ba0ad69 | |||
0bef06139c | |||
455d54355c | |||
3698f79731 | |||
4060e29ade | |||
b6610d70f3 | |||
bb8c578ade | |||
7b2d58600e | |||
79e50ced0d | |||
4de14b250d | |||
53eed67dfc | |||
c1df1e092e | |||
ca732c1306 | |||
4871052a4b | |||
ca35dbf92b | |||
ab9f192f6f | |||
edee295fe1 | |||
6571d053e0 | |||
433157278d | |||
1d4fc62668 | |||
65b91034d9 | |||
8e3c440d70 | |||
597884f5dd |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
node_modules
|
||||
|
||||
result*
|
||||
|
25
README.md
Normal file
25
README.md
Normal 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
|
||||
```
|
15
app.js
Normal file → Executable file
15
app.js
Normal file → Executable file
@@ -1,12 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {createClient} from 'db-vendo-client/index.js';
|
||||
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',
|
||||
@@ -22,7 +30,6 @@ const config = {
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
|
||||
const app = express();
|
||||
|
||||
const vendo = createClient(
|
||||
@@ -33,9 +40,9 @@ const start = async () => {
|
||||
const api = await createApi(vendo, config);
|
||||
|
||||
app.use("/api", api);
|
||||
app.use('/web', express.static('web'));
|
||||
app.use('/', express.static(config.webdir));
|
||||
|
||||
app.listen(config.port, (err) => {
|
||||
app.listen(config.port, config.address, (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1751637120,
|
||||
"narHash": "sha256-xVNy/XopSfIG9c46nRmPaKfH1Gn/56vQ8++xWA8itO4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c724ed1388e53cc231ed98330a60eb2f7be4be3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
36
flake.nix
Normal file
36
flake.nix
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
outputs = { self, nixpkgs, ... }: let
|
||||
forAllSystems = f: (nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" ] (system: let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
in f { inherit pkgs system; } ));
|
||||
in {
|
||||
packages = forAllSystems ({pkgs, system, ...}: {
|
||||
traveldrafter = pkgs.buildNpmPackage rec {
|
||||
pname = "traveldrafter";
|
||||
version = "0.0.1";
|
||||
|
||||
src = ./.;
|
||||
|
||||
npmDeps = pkgs.importNpmLock {
|
||||
npmRoot = src;
|
||||
};
|
||||
|
||||
npmConfigHook = pkgs.importNpmLock.npmConfigHook;
|
||||
|
||||
dontNpmBuild = true;
|
||||
|
||||
meta.mainProgram = "traveldrafter";
|
||||
};
|
||||
default = self.packages.${system}.traveldrafter;
|
||||
});
|
||||
|
||||
hydraJobs = {
|
||||
inherit (self)
|
||||
packages;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@@ -3,9 +3,11 @@
|
||||
"version": "0.0.1",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"traveldrafter": "app.js"
|
||||
},
|
||||
"author": "clerie",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "",
|
||||
|
@@ -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], {});
|
||||
}
|
||||
|
128
web/datastore.js
128
web/datastore.js
@@ -1,38 +1,120 @@
|
||||
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_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_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();
|
||||
}
|
||||
}
|
||||
|
||||
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 +127,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();
|
||||
}
|
||||
}
|
||||
|
17
web/dom.js
Normal file
17
web/dom.js
Normal file
@@ -0,0 +1,17 @@
|
||||
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);
|
||||
}
|
||||
|
||||
el.on = (event_name, callback) => el.addEventListener(event_name, callback);
|
||||
|
||||
return el;
|
||||
}
|
241
web/drafting-board.js
Normal file
241
web/drafting-board.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import { EL } from "./dom.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);
|
||||
awaiting_promises.push(fetchTrip(leg.tripId).then(result => {
|
||||
window.dataStore.trips.remember(result.trip);
|
||||
}));
|
||||
}
|
||||
|
||||
Promise.all(awaiting_promises)
|
||||
.then(results => {
|
||||
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;
|
||||
});
|
||||
|
||||
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" ],
|
||||
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 + 3,
|
||||
|
||||
},
|
||||
});
|
||||
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 => {
|
||||
displayLegDetails(display_leg.trip_id, display_leg.origin_location_id, display_leg.destination_location_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);
|
||||
|
||||
// 1: b vor a
|
||||
// -1: a vor 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_location_id != selected_location_id && leg.origin_location_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;
|
||||
}
|
@@ -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">×</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">×</div>
|
||||
<div id="locations-search-content" class="popup-content">
|
||||
<div class="container">
|
||||
@@ -37,5 +42,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="leg-details" class="popup popup-hidden">
|
||||
<div class="popup-close">×</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">×</div>
|
||||
<div id="trip-details-content" class="popup-content">
|
||||
<div class="container">
|
||||
<div id="trip-details-response"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -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;
|
||||
|
||||
}
|
||||
@@ -45,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
37
web/leg-details.js
Normal 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();
|
||||
}
|
@@ -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,9 +24,9 @@ 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";
|
||||
element_locations_search.popupHide();
|
||||
});
|
||||
|
||||
return location_element;
|
||||
@@ -41,12 +40,12 @@ 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);
|
||||
}
|
||||
element_locations_search.style.display = "block";
|
||||
element_locations_search.popupShow();
|
||||
});
|
||||
}
|
||||
|
10
web/popup.js
10
web/popup.js
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
107
web/style.css
107
web/style.css
@@ -5,6 +5,42 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -25,6 +61,10 @@
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.popup-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -50,6 +90,10 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.popup h1 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.popup input {
|
||||
color: white;
|
||||
}
|
||||
@@ -65,18 +109,67 @@ input.form-control {
|
||||
}
|
||||
|
||||
#drafting-board {
|
||||
background-color: #f5a4d1;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
height: 100vh;
|
||||
|
||||
padding: 20px;
|
||||
padding-top: 70px;
|
||||
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#drafting-board-content {
|
||||
border-color: red;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
min-width: 1000px;
|
||||
min-height: 300px;
|
||||
display: grid;
|
||||
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
--drafting-board-number-locations: 0;
|
||||
--drafting-board-number-legs: 0;
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
@@ -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();
|
||||
|
25
web/trip-details.js
Normal file
25
web/trip-details.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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.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;
|
||||
element_response.appendChild(el);
|
||||
}
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user