import { html, css, nothing } from 'lit'; import { classMap } from 'lit/directives/class-map.js'; import { when } from 'lit/directives/when.js'; import { BaseView } from './baseView.js'; import { createEvents } from 'ics'; import { sleep, queryBackgroundColor, setThemeColor } from './helpers.js'; import { db } from './dataStorage.js'; import { t } from './translate.js'; import { getJourney, refreshJourney } from './app_functions.js'; import { remarksModal, platformTemplate, stopTemplate, timeTemplate } from './templates.js'; import { formatPoint, formatDuration, formatPrice, formatTrainTypes, formatLineAdditionalName, formatLineDisplayName } from './formatters.js'; import { cachedCoachSequence } from './coach-sequence/index.js'; import { headerStyles, tableStyles, journeyViewStyles } from './styles.js'; class JourneyView extends BaseView { static styles = [ super.styles, headerStyles, tableStyles, journeyViewStyles ]; static properties = { profile: { attribute: true }, refreshToken: { attribute: true, converter: (value) => decodeURIComponent(value)}, viewState: { state: true }, }; constructor () { super(); this.viewState = null; } async connectedCallback () { super.connectedCallback(); await sleep(200); setThemeColor(queryBackgroundColor(this.renderRoot, 'header')); } disconnectedCallback () { super.disconnectedCallback(); this.viewState = null; } async willUpdate (previous) { if (previous.has('refreshToken')) this.viewState = null; if (!this.viewState && !this.overlayState.visible) await this.updateViewState(); } updated (previous) { super.updated(previous, 'JourneyView'); if (isDevServer && previous.has('viewState')) console.info('JourneyView(viewState): ', this.viewState); } renderView = () => [ html`
history.back()}>
${when(!this.viewState, () => html`

... ...

${t('duration')}: ... ${t('changes')}: ... ${t('date')}: ...

`, () => html`

${formatPoint(this.viewState.legs[0].origin)} ${formatPoint(this.viewState.legs[this.viewState.legs.length - 1].destination)}

${t('duration')}: ${formatDuration(this.viewState.duration)} ${t('changes')}: ${this.viewState.changes-1} ${t('date')}: ${this.viewState.legs[0].plannedDeparture.formatDate()} ${when( this.settingsState.showPrices && this.viewState.price, () => html` ${t('price')}: ${formatPrice(this.viewState.price)}` )}

` )}
`, when(!this.viewState, () => html`
`, () => html`
${this.viewState.legs.map(leg => this.legTemplate(leg))}
` ) ]; legTemplate = leg => { if (leg.walking) { return html`

${t(leg.distance === null ? 'walkinfo' : 'walkinfoMeters', formatPoint(leg.destination), leg.distance)}

`; } else if (leg.transfer) { return html`

${t('transferinfo', formatPoint(leg.destination))}

`; } else if (leg.change) { return html`

${t('changeinfo', formatDuration(leg.duration))}

`; } else { return html`
${formatLineDisplayName(leg.line)}${!leg.direction ? nothing : html` → ${leg.direction}`} ${when(leg.remarks.length !== 0, () => html` remarksModal(this, leg.remarks)}>`)}
${when( leg.cancelled, () => html`${t('cancelled')}` )} ${when( formatLineAdditionalName(leg.line) !== null, () => html`Trip: ${formatLineAdditionalName(leg.line)}` )} ${when( leg.line.trainType, () => html`${t('trainType')}: ${leg.line.trainType}` )} ${when( leg.arrival && leg.departure, () => html`${t('duration')}: ${formatDuration(leg.arrival - leg.departure)}` )} ${when( leg.loadFactor, () => html`${t(`load-${leg.loadFactor}`)}` )}
${t('arrival')} ${t('departure')} ${t('station')} ${t('platform')}
${(leg.stopovers || []).map(stop => html`
${timeTemplate(stop, 'arrival')} ${timeTemplate(stop, 'departure')} ${stopTemplate(this.profile, stop.stop)} ${platformTemplate(stop)}
`)}
`; } }; updateViewState = async viewState => { const wasUpdating = this.isUpdating; this.isUpdating = true; try { if (viewState === undefined) viewState = await getJourney(this.profile, this.refreshToken); if (viewState.slug) { let overviewObject = await db.getJourneysOverview(viewState.slug); let historyObject = await db.getHistoryEntry(overviewObject.historyEntryId); historyObject.lastSelectedJourneyId = viewState.refreshToken; await db.updateHistoryEntry(overviewObject.historyEntryId, historyObject); } viewState.changes = 0; viewState.duration = null; if (viewState.legs[viewState.legs.length - 1].arrival && viewState.legs[0].departure) viewState.duration = viewState.legs[viewState.legs.length - 1].arrival - viewState.legs[0].departure; let lastArrival; viewState.legs = viewState.legs.flatMap(leg => { if (!leg.remarks) leg.remarks = []; let legs = []; const remarksStatus = leg.remarks.some((obj) => obj.type === 'status'); const remarksWarning = leg.remarks.some((obj) => obj.type === 'warning'); leg.remarksIcon = remarksWarning ? 'icon-warning' : (remarksStatus ? 'icon-status' : 'icon-hint'); if (!leg.walking && !leg.transfer) { // add change if (lastArrival) { let duration = null; if (leg.departure && lastArrival) duration = leg.departure - lastArrival; legs.push({ change: true, duration, }); } viewState.changes++; lastArrival = leg.arrival; } else if (legs.length) { // if this is a walking leg and it is the first one, we don't want to // insert a 0 minutes change entry for this lastArrival = leg.arrival; } legs.push(leg); return legs }); this.viewState = viewState; if (isDevServer) console.info('JourneyView(updateViewState): coach sequence fetch start'); //fetch train types after setting the viewState if (!this.isOffline) { for (const leg of this.viewState.legs) { if (leg.line && leg.line.name) { const [category, number] = leg.line.name.split(' '); const info = cachedCoachSequence(category, leg.line.fahrtNr || number, leg.origin.id, leg.plannedDeparture); if (info) leg.line.trainType = formatTrainTypes(info); this.requestUpdate(); } } } if (isDevServer) console.info('JourneyView(updateViewState): coach sequence fetch end'); } catch(e) { this.showAlertOverlay( e.toString(), () => { window.location = '#/'; } ); console.error(e); } if (!wasUpdating) this.isUpdating = false; }; refreshJourney = async () => { if (this.isOffline !== false) return; if (this.isUpdating !== false) return false; this.isUpdating = true; try { const data = await refreshJourney(this.profile, this.refreshToken); await this.updateViewState(data); } catch(e) { this.showAlertOverlay(e.toString()); console.error(e); } this.isUpdating = false; }; moreModal = async () => { const options = [ { 'label': !navigator.canShare ? 'copyURL' : 'shareURL', 'action': async () => await this.shareAction() }, ]; if (isDevServer) options.push({ 'label': 'addCalendar', 'action': async () => await this.calendarAction() }); if (this.profile === 'db') options.push({ 'label': 'tickets', 'action': async () => await this.ticketsAction() }); this.showSelectOverlay(options); }; shareAction = async () => { try { await navigator.share({ url: window.location }); } catch (error) { await navigator.clipboard.writeText(window.location); this.showAlertOverlay('URL has been copied to clipboard.'); } }; ticketsAction = async () => { try { this.showLoaderOverlay(); if (!this.isOffline) await this.refreshJourney(); if (this.viewState.tickets === undefined) { await this.showAlertOverlay('No ticket data available'); return false; } this.showDialogOverlay('tickets', html` ${this.viewState.tickets.map(ticket => html`
${ticket.name} ${ticket.firstClass ? `1. ${t('class')}` : nothing}
${ticket.addDataTicketDetails}
${ticket.priceObj.amount / 100}
`)} `); } catch(e) { this.showAlertOverlay(e.toString()); console.error(e); } }; calendarAction = async () => { let events = []; this.viewState.legs.forEach(leg => { if (!leg.line) return; const departureStop = leg.origin.location events.push({ productId: APP_NAME, created: (new Date).getTime(), start: leg.plannedDeparture.getTime(), end: leg.plannedArrival.getTime(), title: `${leg.line.name} → ${formatPoint(leg.destination)}`, url: window.location.href, geo: { lat: departureStop.latitude , lon: departureStop.longitude }, description: `${leg.line.name} → ${leg.direction}\n dep ${timeTemplate(departureStop, 'departure')} ${formatPoint(departureStop)}`, }); }); const file = await new Promise((resolve, reject) => { const { error, value } = createEvents(events); if (error) reject(error); resolve(new File([value], 'event.ics', { type: 'text/calendar' })); }); window.location = URL.createObjectURL(file); }; } customElements.define('journey-view', JourneyView);