ctucx.git: trainsearch

web based trip-planner, fork of https://cyberchaos.dev/yuka/trainsearch

commit 5422ec49e86f1cf211be3f614decd1e0e6a4015f
parent 69c2f311e75919dbfd667f4db93f33a1a8a2ff50
Author: Yureka <yuka@yuka.dev>
Date: Thu, 3 Feb 2022 12:57:54 +0100

stateless server
9 files changed, 162 insertions(+), 81 deletions(-)
M
client/src/app_functions.js
|
143
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
M
client/src/canvas.js
|
3
++-
M
client/src/dataStorage.js
|
35
+++++++++++++++++++++++++++--------
M
client/src/helpers.js
|
10
++++++++++
M
client/src/journeyView.js
|
25
+++++++++++--------------
M
client/src/journeysView.js
|
17
++++-------------
M
client/src/main.js
|
2
+-
M
client/src/searchView.js
|
2
+-
M
client/src/settingsView.js
|
6
------
diff --git a/client/src/app_functions.js b/client/src/app_functions.js
@@ -21,7 +21,29 @@ const addJourneys = async data => {
 
 	ConsoleLog(data);
 
-	await db.put('journeys', data);
+	const historyEntry = {
+		fromPoint: getFrom(data.journeys),
+		//viaPoint: data.params.via,
+		toPoint: getTo(data.journeys),
+		slug: data.slug,
+		journeyId: ''
+	};
+
+	const tx = db.transaction(['journey', 'journeysOverview', 'journeysHistory'], 'readwrite');
+	const journeyStore = tx.objectStore('journey');
+	const journeysOverviewStore = tx.objectStore('journeysOverview');
+	const journeysHistoryStore = tx.objectStore('journeysHistory');
+	let proms = data.journeys.map(j => {
+		j.settings = data.settings;
+		journeyStore.put(j);
+	});
+	proms.push(journeysOverviewStore.put({
+		...data,
+		journeys: data.journeys.map(j => j.refreshToken),
+	}));
+	proms.push(journeysHistoryStore.put(historyEntry));
+	await Promise.all(proms);
+	await tx.done;
 
 	//const lastHistoryEntry = (await getJourneysHistory()).slice(-1);
 	//if (lastHistoryEntry[0]

@@ -30,17 +52,10 @@ const addJourneys = async data => {
 	//	dataStorage.journeysHistory[dataStorage.journeysHistory.length-1].slug = data.slug;										
 	//	dataStorage.journeysHistory[dataStorage.journeysHistory.length-1].journeyId = '';
 	//} else {
-	await addHistoryEntry({
-		fromPoint: getFrom(data.journeys),
-		//viaPoint: data.params.via,
-		toPoint: getTo(data.journeys),
-		slug: data.slug,
-		journeyId: ''
-	});
 	//}
 };
 
-const mkParams = () => {
+const mkSettings = () => {
 	return {
 		stopovers: true,
 		polylines: settings.showMap || false,

@@ -49,52 +64,106 @@ const mkParams = () => {
 	};
 };
 
-const processData = data => {
-	for (const journey of data.journeys) {
-		for (const leg of journey.legs) {
-			if (leg.plannedDeparture) leg.plannedDeparture = new Date(leg.plannedDeparture);
-			if (leg.plannedArrival) leg.plannedArrival = new Date(leg.plannedArrival);
-			if (leg.departure) leg.departure = new Date(leg.departure);
-			if (leg.arrival) leg.arrival = new Date(leg.arrival);
-			for (const stopover of (leg.stopovers || [])) {
-				if (stopover.plannedDeparture) stopover.plannedDeparture = new Date(stopover.plannedDeparture);
-				if (stopover.plannedArrival) stopover.plannedArrival = new Date(stopover.plannedArrival);
-				if (stopover.departure) stopover.departure = new Date(stopover.departure);
-				if (stopover.arrival) stopover.arrival = new Date(stopover.arrival);
-			}
+const processJourneys = data => {
+	for (const journey of data.journeys) processJourney(journey);
+	return data;
+}
+
+const processJourney = journey => {
+	for (const leg of journey.legs) {
+		if (leg.plannedDeparture) leg.plannedDeparture = new Date(leg.plannedDeparture);
+		if (leg.plannedArrival) leg.plannedArrival = new Date(leg.plannedArrival);
+		if (leg.departure) leg.departure = new Date(leg.departure);
+		if (leg.arrival) leg.arrival = new Date(leg.arrival);
+		for (const stopover of (leg.stopovers || [])) {
+			if (stopover.plannedDeparture) stopover.plannedDeparture = new Date(stopover.plannedDeparture);
+			if (stopover.plannedArrival) stopover.plannedArrival = new Date(stopover.plannedArrival);
+			if (stopover.departure) stopover.departure = new Date(stopover.departure);
+			if (stopover.arrival) stopover.arrival = new Date(stopover.arrival);
 		}
 	}
-	return data;
+	return journey;
 };
 
 export const getJourneys = async slug => {
 	if (!slug) return;
 
-	let data = await db.get('journeys', slug);
-	if (!data) {
-		data = await get(`savedJourneys/${slug}`, mkParams());
-		await addJourneys(data);
+	let data = await db.get('journeysOverview', slug);
+	data.journeys = await Promise.all(data.journeys.map(getJourney));
+
+	return processJourneys(data);
+};
+
+export const getJourney = async refreshToken => {
+	if (!refreshToken) return;
+
+	let data = await db.get('journey', refreshToken);
+	const settings = mkSettings();
+	if (!data || JSON.stringify(data.settings) != JSON.stringify(settings)) {
+		data = await get(`journeys/${encodeURIComponent(refreshToken)}`, settings);
+		data.settings = settings;
+		await db.put('journey', data);
 	}
 
-	return processData(data);
+	return processJourney(data);
 };
 
 export const getMoreJourneys = async (slug, mode, hideLoader) => {
-	const data = await post(`savedJourneys/${slug}/${mode}`, mkParams(), hideLoader);
-	await addJourneys(data);
-	return processData(data);
-};
+	if (!slug) return;
 
+	const saved = await db.get('journeysOverview', slug);
+	const params = { ...saved.params, ...mkSettings() };
+	params[mode+'Than'] = saved[mode+'Ref'];
+	let { departure, arrival, ...moreOpt } = params;
+	console.log(moreOpt);
+	const [newData, ...existingJourneys] = await Promise.all(
+		[ get('journeys', moreOpt) ]
+			.concat(saved.journeys.map(getJourney))
+	);
+	console.log(newData);
+
+        const res = {
+                ...saved,
+                ...newData,
+        };
+        if (mode === 'earlier') {
+                res.journeys = newData.journeys.concat(existingJourneys);
+                res.indexOffset += newData.journeys.length;
+                // keep old laterRef
+                res.laterRef = saved.laterRef;
+        } else {
+                res.journeys = existingJourneys.concat(newData.journeys);
+                // keep old earlierRef
+                res.earlierRef = saved.earlierRef;
+        }
+	console.log(res);
+
+	await addJourneys(res);
+	return processJourneys(res);
+};
 export const refreshJourneys = async (reqId, hideLoader) => {
-	const data = await get(`savedJourneys/${reqId}`, mkParams(), hideLoader);
-	await addJourneys(data);
-	return processData(data);
+};
+export const refreshJourney = async (refreshToken, hideLoader) => {
+};
+
+const generateSlug = () => {
+        const len = 8;
+        let result = '';
+        const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+        for (let i = 0; i < len; i++)
+                result += characters.charAt(Math.floor(Math.random() * characters.length));
+        return result;
 };
 
 export const newJourneys = async (params, hideLoader) => {
-	const data = await post('savedJourneys', { ...mkParams(), ...params }, hideLoader);
+	const settings = mkSettings();
+	const data = await get('journeys', { ...settings, ...params }, hideLoader);
+	data.slug = generateSlug();
+	data.indexOffset = 0;
+	data.params = params;
+	data.settings = settings;
 	await addJourneys(data);
-	return processData(data);
+	return processJourneys(data);
 };
 
 export const t = (key, ...params) => {
diff --git a/client/src/canvas.js b/client/src/canvas.js
@@ -321,7 +321,8 @@ const mouseUpHandler = (evt) => {
 		const num = Math.floor((x - canvasState.offsetX + 2 * padding) / rectWidthWithPadding) + canvasState.indexOffset;
 		if (num >= 0) {
 			if (num < canvasState.journeys.length) {
-				go(`/${canvasState.slug}/${num - canvasState.indexOffset}`);
+				const j = canvasState.journeys[num - canvasState.indexOffset];
+				go(`/j/${j.refreshToken}`);
 			} else {
 				moreJourneys(canvasState.slug, 'later');
 			}
diff --git a/client/src/dataStorage.js b/client/src/dataStorage.js
@@ -6,19 +6,38 @@ const dbName = devMode ? 'trainsearch_dev' : 'trainsearch';
 export let db;
 
 export const initDataStorage = async () => {
-	db = await openDB(dbName, 1, {
+	db = await openDB(dbName, 2, {
 		upgrade: (db, oldVersion, newVersion, transaction) => {
 			console.log(`upgrading database from ${oldVersion} to ${newVersion}`);
 			switch (oldVersion) {
 			case 0:
-				if (!db.objectStoreNames.contains('journeys'))
-					db.createObjectStore('journeys', {keyPath: 'slug'});
-				if (!db.objectStoreNames.contains('journeysHistory'))
-					db.createObjectStore('journeysHistory', {autoIncrement: true});
-				if (!db.objectStoreNames.contains('settings'))
-					db.createObjectStore('settings');
+				/*
+				 * in database scheme v1, the `journeys` object store stores whole searches, including journey data
+				 */
+				db.createObjectStore('journeys', {keyPath: 'slug'});
+				db.createObjectStore('journeysHistory', {autoIncrement: true});
+				db.createObjectStore('settings');
 			case 1:
-					//... add migrations for 1->2 here
+				/*
+				 * in database scheme v2, there are the following data stores:
+				 * `journey` for storing individual journeys with their refreshToken as index
+				 * `journeysOverview`t contains the search results with the `.journeys` field being an array of refreshTokens.
+				 *
+				 * in comparison to v1, there are these additional changes:
+				 * `journeys` object store is deleted.
+				 * `journeysHistory` object store is cleared/recreated.
+				 */
+
+				/* clean up old stuff */
+				db.deleteObjectStore('journeys');
+				db.deleteObjectStore('journeysHistory');
+
+				/* create new stores */
+				db.createObjectStore('journey', {keyPath: 'refreshToken'});
+				db.createObjectStore('journeysOverview', {keyPath: 'slug'});
+				db.createObjectStore('journeysHistory', {autoIncrement: true});
+			case 2:
+				//... add migrations for 2->3 here
 			}
 		},
 		blocking: async () => {
diff --git a/client/src/helpers.js b/client/src/helpers.js
@@ -93,3 +93,13 @@ export const getTo = journeys => {
 	return journeys[0].legs[journeys[0].legs.length-1].destination;
 };
 
+export const formatPrice = price => {
+	if (!price) return '-';
+	const currencies = { USD: '$', EUR: '€', GBP: '£' };
+	let ret = currencies[price.currency] || price.currency;
+	ret += Math.floor(price.amount);
+	ret += '.';
+	ret += padZeros(price.amount * 100 % 100, 2);
+	return ret;
+};
+
diff --git a/client/src/journeyView.js b/client/src/journeyView.js
@@ -1,6 +1,6 @@
 import { settings } from './settings.js';
-import { showDiv, hideDiv, ElementById, formatDateTime, formatDuration } from './helpers.js';
-import { ConsoleLog, parseName, ds100Names, t, timeTemplate, getJourneys, refreshJourneys } from './app_functions.js';
+import { showDiv, hideDiv, ElementById, formatDateTime, formatDuration, formatPrice } from './helpers.js';
+import { ConsoleLog, parseName, ds100Names, t, timeTemplate, getJourney, refreshJourneys } from './app_functions.js';
 import { showModal } from './overlays.js';
 import { get } from './api.js';
 import { go } from './router.js';

@@ -71,9 +71,9 @@ const legTemplate = leg => {
 					<tr>
 						<td colspan="4">
 							<span>${marudorUrl ? html`
-								<a href="${marudorUrl}">${leg.line.name} → ${leg.direction}</a>
+								<a href="${marudorUrl}">${leg.line.name}${leg.direction ? html` → ${leg.direction}` : ''}</a>
 							` : html `
-								${leg.line.name} → ${leg.direction}
+								${leg.line.name}${leg.direction ? html` → ${leg.direction}` : ''}
 							`}
 							${leg.cancelled ? html`<b class="cancelled-text">${t('cancelled-ride')}</b>` : ''}
 							${Object.entries(remarks).map(remarksTemplate)}

@@ -127,7 +127,7 @@ const legTemplate = leg => {
 	`;
 };
 
-const journeyTemplate = (data, requestId, journeyId) => {
+const journeyTemplate = (data) => {
 	const duration = data.legs[data.legs.length - 1].arrival - data.legs[0].departure;
 
 	const legs = [];

@@ -160,12 +160,12 @@ const journeyTemplate = (data, requestId, journeyId) => {
 	return html`
 		<div class="journey">
 			<header>
-				<a class="back icon-back" href="#/${requestId}"></a>
+				<a class="back icon-back" title="${t("back")}" href="#/">${t("back")}</a>
 				<div class="header-content">
 					<h3>${parseName(data.legs[0].origin)} → ${parseName(data.legs[data.legs.length - 1].destination)}</h3>
-					<p><b>${t('duration')}: ${formatDuration(duration)} | ${t('changes')}: ${changes-1} | ${t('date')}: ${formatDateTime(data.legs[0].plannedDeparture, 'date')}</b></p>
+					<p><b>${t('duration')}: ${formatDuration(duration)} | ${t('changes')}: ${changes-1} | ${t('date')}: ${formatDateTime(data.legs[0].plannedDeparture, 'date')}${settings.showPrices && data.price ? html` | ${t('price')}: <td><span>${formatPrice(data.price)}</span></td>` : ''}</b></p>
 				</div>
-				<a class="reload icon-reload" title="{{LABEL_RELOAD}}" @click=${() => refreshJourneyView(requestId, journeyId)}>{{LABEL_RELOAD}}</a>
+				<a class="reload icon-reload" title="${t("reload")}" @click=${() => refreshJourneyView(requestId, journeyId)}>${t("reload")}</a>
 			</header>
 
 			${legs.map(legTemplate)}

@@ -198,15 +198,12 @@ const stopPlatformTemplate = (data) => {
 };
 
 export const journeyView = async (match) => {
-	const reqId = match[0];
-	const journeyId = Number(match[1]);
+	const refreshToken = decodeURIComponent(match[0]);
 
-	let all = await getJourneys(reqId);
-	if (!((journeyId + all.indexOffset) in all.journeys)) all = await refreshJourneys(reqId, false);
-	const data = all.journeys[journeyId + all.indexOffset];
+	let data = await getJourney(refreshToken);
 
 	ConsoleLog(data);
-	render(journeyTemplate(data, reqId, journeyId), ElementById('content'));
+	render(journeyTemplate(data), ElementById('content'));
 
 	/*const history_id = dataStorage.journeysHistory.findIndex(obj => obj.reqId === reqId);
 
diff --git a/client/src/journeysView.js b/client/src/journeysView.js
@@ -1,4 +1,4 @@
-import { showDiv, hideDiv, ElementById, formatDuration, formatFromTo, getFrom, getTo, padZeros } from './helpers.js';
+import { showDiv, hideDiv, ElementById, formatDuration, formatFromTo, getFrom, getTo, padZeros, formatPrice } from './helpers.js';
 import { parseName, ConsoleLog, t, timeTemplate, getJourneys, getMoreJourneys } from './app_functions.js';
 import { settings, modifySettings } from './settings.js';
 import { setupCanvas } from './canvas.js';

@@ -85,7 +85,7 @@ const journeyOverviewTemplate = (entry, slug, key) => {
 
 	for (const leg of entry.legs) {
 		if (leg.cancelled) cancelled = true;
-		if (leg.isWalking || leg.isTransfer) continue;
+		if (leg.walking || leg.transfer) continue;
 
 		changes = changes+1;
 

@@ -106,7 +106,7 @@ const journeyOverviewTemplate = (entry, slug, key) => {
 	}).join(', ');
 
 	return html`
-	<tr @click=${() => go('/'+slug + '/' + key)}">
+	<tr @click=${() => go('/j/'+entry.refreshToken)}">
 		<td class="${cancelled ? 'cancelled' : ''}"><span>${timeTemplate(firstLeg, 'departure')}</span></td>
 		${cancelled ? html`
 			<td><span class="cancelled-text">${t('cancelled-ride')}</span></td>

@@ -121,20 +121,11 @@ const journeyOverviewTemplate = (entry, slug, key) => {
 	</tr>`;
 };
 
-const formatPrice = price => {
-	if (!price) return '-';
-	const currencies = { USD: '$', EUR: '€', GBP: '£' };
-	let ret = currencies[price.currency] || price.currency;
-	ret += Math.floor(price.amount);
-	ret += '.';
-	ret += padZeros(price.amount * 100 % 100, 2);
-	return ret;
-};
-
 export const journeysView = async (match, isUpdate) => {
 	const slug = match[0];
 
 	const data = await getJourneys(slug);
+	console.log(data);
 
 	render(journeysTemplate(data), ElementById('content'));
 
diff --git a/client/src/main.js b/client/src/main.js
@@ -13,7 +13,7 @@ import { showDiv, hideDiv, ElementById } from './helpers.js';
 
 	route(/^\/$/, searchView);
 	route(/^\/([a-zA-Z0-9]+)$/, journeysView);
-	route(/^\/([a-zA-Z0-9]+)\/([-0-9]+)$/, journeyView);
+	route(/^\/j\/(.+)$/, journeyView);
 
 	hideDiv('overlay');
 	if (!window.location.hash.length) go('/');
diff --git a/client/src/searchView.js b/client/src/searchView.js
@@ -273,7 +273,7 @@ export const search = async (requestId) => {
 		from: formatFromTo(from),
 		to: formatFromTo(to),
 		results: 6,
-		//"accessibility": settings.accessibility,
+		accessibility: settings.accessibility,
 		...settings.products,
 	};
 
diff --git a/client/src/settingsView.js b/client/src/settingsView.js
@@ -46,8 +46,6 @@ const newAll = () => {
 };
 
 const saveSettings = async () => {
-	let clearJourneys = false;
-	const clearJourneysHistory = false;
 	await modifySettings(settings => {
 		settings.showRIL100Names = ElementById('ril100').checked;
 		settings.writeDebugLog = ElementById('debug-messages').checked;

@@ -55,20 +53,16 @@ const saveSettings = async () => {
 		settings.advancedSelection = ElementById('advancedSelection').checked;
 
 		const language = document.querySelector('input[name="language"]:checked').value;
-		if (settings.language != language) clearJourneys = true;
 		settings.language = language;
 
 		const showMap = ElementById('feature-map').checked;
-		if (settings.showMap != showMap) clearJourneys = clearJourneys || showMap;
 		settings.showMap = showMap;
 
 		const showPrices = ElementById('feature-prices').checked;
-		if (settings.showPrices != showPrices) clearJourneys = clearJourneys || showPrices;
 		settings.showPrices = showPrices;
 
 		return settings;
 	});
-	if (clearJourneys) await db.clear('journeys');
 	searchView();
 	hideDiv('overlay');
 };