diff --git a/src/components/lineTimetable/allStopsList.css b/src/components/lineTimetable/allStopsList.css index cbd52cbc..f5de0443 100644 --- a/src/components/lineTimetable/allStopsList.css +++ b/src/components/lineTimetable/allStopsList.css @@ -1,6 +1,7 @@ .stopListsContainer { margin-top: 2rem; max-width: 1171px; + page-break-inside: avoid; } .stopList { @@ -16,9 +17,3 @@ .stopListText { font-family: GothamRounded-Medium; } - -@media print { - .stopListsContainer { - page-break-inside: avoid; - } -} diff --git a/src/components/lineTimetable/lineTableColumns.css b/src/components/lineTimetable/lineTableColumns.css index 5ae52c3f..3c535058 100644 --- a/src/components/lineTimetable/lineTableColumns.css +++ b/src/components/lineTimetable/lineTableColumns.css @@ -2,10 +2,7 @@ max-width: 1171px; min-width: 120px; font-family: GothamRounded-Book; -} - -.departureRowContainer > *:nth-child(odd) { - background-color: #e8e8e8; + margin: 0.5rem 0; } .departureRow { @@ -29,8 +26,27 @@ min-width: 400px !important; } -.firstStopDivider { - border-right: 3px solid darkgray; +.hour { + font-family: GothamXNarrow-Medium; + min-width: 3rem; + align-self: baseline; +} + +.minutesContainer { + max-width: 400px; + display: flex; + flex-wrap: wrap; +} + +.minutes { + display: flex; + min-width: 2em; + padding: 0.25em 0 0 0.6em; + font-family: GothamXNarrow-Book; + font-size: 0.75em; + letter-spacing: -0.025em; + line-height: 1; + margin-right: 0.25em; } @media print { diff --git a/src/components/lineTimetable/lineTableColumns.js b/src/components/lineTimetable/lineTableColumns.js index b37c0368..6c14462a 100644 --- a/src/components/lineTimetable/lineTableColumns.js +++ b/src/components/lineTimetable/lineTableColumns.js @@ -4,41 +4,92 @@ import { Column, Row, WrappingRow } from '../util'; import LineTableHeader from './lineTableHeader'; import styles from './lineTableColumns.css'; import classNames from 'classnames'; -import { isArray, filter, isEmpty } from 'lodash'; +import { isArray, filter, isEmpty, groupBy } from 'lodash'; +import { filterDuplicateDepartureHours, getDuplicateCutOff } from '../timetable/tableRows'; const LineTimetableRow = props => { - const { hours, minutes } = props; - const paddedMins = minutes.toString().padStart(2, '0'); + const { hour, departures } = props; + const sortedMinuteDepartures = departures.sort((a, b) => { + return a.minutes - b.minutes; + }); return ( - {hours}.{paddedMins} +
{hour}
+
+ {sortedMinuteDepartures.map((departure, index) => ( +
+ {departure.note === 'p' + ? `${departure.minutes.toString().padStart(2, '0')} pe` + : departure.minutes.toString().padStart(2, '0')} +
+ ))} +
); }; LineTimetableRow.propTypes = { - hours: PropTypes.number.isRequired, - minutes: PropTypes.number.isRequired, + hour: PropTypes.number.isRequired, + departures: PropTypes.array.isRequired, }; const DeparturesColumn = props => { const { departures, stop } = props; if (departures) { - const departureRows = departures.map(departure => { - return ; + const departuresByHour = groupBy( + departures, + departure => (departure.isNextDay ? 24 : 0) + departure.hours, + ); + + const rows = Object.entries(departuresByHour).map(([hours, hourlyDepartures]) => ({ + hour: hours, + departures: hourlyDepartures, + })); + + const formatHour = hour => `${hour % 24 < 10 ? '0' : ''}${hour % 24}`; + + const rowsByHour = []; + for (let i = 0; i < rows.length; i++) { + const cutOff = getDuplicateCutOff(i, rows); + let hours = + rows[i].hour === rows[cutOff].hour + ? `${formatHour(rows[i].hour)}` + : `${formatHour(rows[i].hour)}-${formatHour(rows[cutOff].hour)}`; + + if (rows.length === 2) { + hours = `${formatHour(rows[i].hour)}`; + const firstHour = { hour: hours, departures: rows[i].departures }; + const secondHour = { + hour: `${formatHour(rows[rows.length - 1].hour)}`, + departures: rows[rows.length - 1].departures, + }; + + rowsByHour.push(firstHour, secondHour); + i = cutOff; + } else { + rowsByHour.push({ + hour: hours, + departures: rows[i].departures, + }); + i = cutOff; + } + } + + const filteredDepartures = filterDuplicateDepartureHours(rowsByHour); + + const departureRows = filteredDepartures.map(hourlyDepartures => { + return ( + + ); }); + return (
-
- {departureRows} -
+
{departureRows}
); } diff --git a/src/components/lineTimetable/lineTableHeader.css b/src/components/lineTimetable/lineTableHeader.css index 3e860abf..db5490be 100644 --- a/src/components/lineTimetable/lineTableHeader.css +++ b/src/components/lineTimetable/lineTableHeader.css @@ -1,12 +1,20 @@ .stop { flex-grow: 1; - height: 100px; + height: 60px; padding-right: 16px; + margin-bottom: 2rem; } -.stopName { +.stopNamePrimary { font-size: 1.2em; margin: 0 0 0 2rem; font-family: GothamRounded-Medium; word-break: normal; } + +.stopNameSecondary { + font-size: 1.2em; + margin: 0 0 0 2rem; + font-family: GothamXNarrow-Book; + word-break: normal; +} diff --git a/src/components/lineTimetable/lineTableHeader.js b/src/components/lineTimetable/lineTableHeader.js index a0cd8fe3..2ca8c544 100644 --- a/src/components/lineTimetable/lineTableHeader.js +++ b/src/components/lineTimetable/lineTableHeader.js @@ -7,8 +7,8 @@ const LineTableHeader = props => { const { stop } = props; return (
-

{stop.nameFi}

-

{stop.nameSe}

+

{stop.nameFi}

+

{stop.nameSe}

); }; diff --git a/src/components/lineTimetable/lineTimetable.css b/src/components/lineTimetable/lineTimetable.css index c912a841..c6699eb2 100644 --- a/src/components/lineTimetable/lineTimetable.css +++ b/src/components/lineTimetable/lineTimetable.css @@ -149,13 +149,13 @@ } .timetableDates { - margin: 0 1rem 1rem 4rem; + margin: 0 1rem 1rem 2rem; font-size: 1.5em; font-family: GothamRounded-Book; } .pageBreak { - display: none; + page-break-inside: avoid; } .timetableDivider { @@ -164,20 +164,17 @@ border-bottom: 2px dotted gray; } +.timetableContainer { + page-break-after: always; +} + @media print { .noPrint, .noPrint * { display: none !important; } - .pageBreak { - display: block; - page-break-after: always; - } body { overflow-y: visible; } - div { - break-inside: avoid; - } } diff --git a/src/components/lineTimetable/lineTimetable.js b/src/components/lineTimetable/lineTimetable.js index 4d84ed10..7d38558f 100644 --- a/src/components/lineTimetable/lineTimetable.js +++ b/src/components/lineTimetable/lineTimetable.js @@ -4,9 +4,22 @@ import styles from './lineTimetable.css'; import LineTimetableHeader from './lineTimetableHeader'; import LineTableColumns from './lineTableColumns'; import AllStopsList from './allStopsList'; -import { filter, isEmpty, uniqBy, flatten, forEach, groupBy, find } from 'lodash'; +import { + filter, + isEmpty, + uniqBy, + flatten, + forEach, + groupBy, + find, + unionWith, + omit, + isEqual, + some, +} from 'lodash'; import { scheduleSegments } from '../../util/domain'; -import { combineConsecutiveDays } from '../timetable/timetableContainer'; +import { addMissingFridayNote, combineConsecutiveDays } from '../timetable/timetableContainer'; +import { shortenTrainParsedLineId } from '../../util/routes'; const MAX_STOPS = 6; // Maximum amount of timed stops rendered on the timetable @@ -54,7 +67,65 @@ const RouteDepartures = props => { showTimedStops, } = props; - const mappedWeekdayDepartures = departuresByStop.map(departuresForStop => { + // This fixes some errors in combined departures, such as ['saturdays-sundays'] for first 3 stops, and ['saturdays', 'sundays'] + const fixPartialWeekendDepartures = departureRange => { + const firstStopDayKeys = Object.keys(departureRange[0].combinedDays); + + const hasPartialDepartures = some(departureRange, stop => { + return !isEqual(Object.keys(stop.combinedDays), firstStopDayKeys); + }); + + if (hasPartialDepartures) { + // 'saturdays-sundays' combined departures + if (isEqual(firstStopDayKeys, [scheduleSegments.weekends])) { + const remappedStopDepartures = departureRange.map(stop => { + const stopKeys = Object.keys(stop.combinedDays); + // Has differing departure keys compared to first stop + if (!isEqual(stopKeys, firstStopDayKeys)) { + const stopDepartures = stop.combinedDays.saturdays + ? stop.combinedDays.saturdays + : stop.combinedDays.sundays; + return { + ...stop, + combinedDays: { + [scheduleSegments.weekends]: { ...stopDepartures }, + }, + }; + } + return stop; + }); + return Object.values( + omit(remappedStopDepartures, ['combinedDays.saturdays', 'combinedDays.sundays']), // Remove redundant departure arrays + ); + } + // 'saturdays' and 'sundays' departures separately + if (isEqual(firstStopDayKeys, [scheduleSegments.saturdays, scheduleSegments.sundays])) { + const remappedStopDepartures = departureRange.map(stop => { + const stopKeys = Object.keys(stop.combinedDays); + // Has differing departure keys compared to first stop + if (!isEqual(stopKeys, firstStopDayKeys)) { + return { + ...stop, + combinedDays: { + [scheduleSegments.saturdays]: filter(stop.combinedDays.weekends, departure => { + return departure.dayType[0] === 'La'; + }), + [scheduleSegments.sundays]: filter(stop.combinedDays.weekends, departure => { + return departure.dayType[0] === 'Su'; + }), + }, + }; + } + return stop; + }); + return Object.values(omit(remappedStopDepartures, ['combinedDays.saturdays-sundays'])); // Remove redundant departure array + } + } + + return departureRange; + }; + + const mappedDepartures = departuresByStop.map(departuresForStop => { const { mondays, tuesdays, @@ -65,7 +136,7 @@ const RouteDepartures = props => { sundays, } = departuresForStop.departures; - return { + const stopWithCombinedDepartures = { stop: departuresForStop.stop, combinedDays: combineConsecutiveDays({ mondays, @@ -77,11 +148,43 @@ const RouteDepartures = props => { sundays, }), }; + return stopWithCombinedDepartures; }); - const combinedDepartureTables = Object.keys(mappedWeekdayDepartures[0].combinedDays).map(key => { + const sanityCheckedDepartures = fixPartialWeekendDepartures(mappedDepartures); + const mergedWeekdaysDepartures = sanityCheckedDepartures.map(mappedDeparturesForStop => { + if ( + mappedDeparturesForStop.combinedDays[scheduleSegments.fridays] && + mappedDeparturesForStop.combinedDays[scheduleSegments.weekdaysExclFriday] + ) { + // Merge friday departures onto Monday-Thursday departures and include a note so it can be displayed + const combinedWeekdayDepartures = unionWith( + mappedDeparturesForStop.combinedDays[scheduleSegments.weekdaysExclFriday], + mappedDeparturesForStop.combinedDays[scheduleSegments.fridays], + (weekday, friday) => { + return weekday.hours === friday.hours && weekday.minutes === friday.minutes; + }, + ); + const combinedWithNotes = combinedWeekdayDepartures.map(departure => { + return { ...departure, note: addMissingFridayNote(departure) }; + }); + + const mergedDepartures = { + ...mappedDeparturesForStop, + combinedDays: { + [scheduleSegments.weekdays]: combinedWithNotes, + ...mappedDeparturesForStop.combinedDays, + }, + }; + return omit(mergedDepartures, ['combinedDays.mondays-thursdays', 'combinedDays.fridays']); // Remove redundant departure days, since we just combined them. + } + + return { ...mappedDeparturesForStop }; + }); + + const combinedDepartureTables = Object.keys(mergedWeekdaysDepartures[0].combinedDays).map(key => { return ( -
+
{
@@ -139,14 +242,25 @@ const dateRangeHasDepartures = routeDepartures => { return hasDepartures; }; +const checkForTrainRoutes = routes => { + return routes.map(route => { + if (route.mode === 'RAIL') { + return { ...route, routeIdParsed: shortenTrainParsedLineId(route.routeIdParsed) }; + } + return route; + }); +}; + function LineTimetable(props) { const { routes } = props; const showTimedStops = hasTimedStopRoutes(routes); + const checkedRoutes = checkForTrainRoutes(routes); + if (showTimedStops) { return (
- {routes.map(routeWithDepartures => { + {checkedRoutes.map(routeWithDepartures => { const routesByDateRanges = routeWithDepartures.departuresByDateRanges.map( departuresForDateRange => { const { nameFi, nameSe, routeIdParsed } = routeWithDepartures; @@ -173,7 +287,7 @@ function LineTimetable(props) { return ( routeForDateRange.departuresByStop.length > 0 && ( -
+
-
 
) ); @@ -204,7 +317,7 @@ function LineTimetable(props) { // The logic below is for timetables that do not have timed stops to display, only the starting stop for a route. // These stops are displayed with both directions side by side in the timetable. - const groupedRoutes = groupBy(routes, 'routeId'); + const groupedRoutes = groupBy(checkedRoutes, 'routeId'); // Map the departures from both directions into unique date ranges, so that we can display both directions for a date range side by side const routeGroupsMappedDepartures = Object.values(groupedRoutes).map(routeGroup => { diff --git a/src/components/lineTimetable/lineTimetableHeader.css b/src/components/lineTimetable/lineTimetableHeader.css index b54ac00d..bca7631c 100644 --- a/src/components/lineTimetable/lineTimetableHeader.css +++ b/src/components/lineTimetable/lineTimetableHeader.css @@ -1,6 +1,5 @@ .header { display: flex; - page-break-before: always; margin-bottom: 1rem; word-wrap: break-word; } @@ -30,9 +29,3 @@ display: inline; margin-top: 1rem; } - -@media print { - .header { - page-break-before: always; - } -} diff --git a/src/components/timetable/tableRows.js b/src/components/timetable/tableRows.js index d1dd9ef1..f31ffd6a 100644 --- a/src/components/timetable/tableRows.js +++ b/src/components/timetable/tableRows.js @@ -73,7 +73,7 @@ const isEqualDepartureHour = (a, b) => { return true; }; -const getDuplicateCutOff = (startIndex, rows) => { +export const getDuplicateCutOff = (startIndex, rows) => { const startRow = rows[startIndex]; let cutOffIndex = startIndex; for (let i = startIndex; i < rows.length; i++) { @@ -86,7 +86,7 @@ const getDuplicateCutOff = (startIndex, rows) => { return cutOffIndex; }; -const filterDuplicateDepartureHours = departureRows => { +export const filterDuplicateDepartureHours = departureRows => { return uniqBy(departureRows, 'departures'); }; diff --git a/src/components/timetable/timetableContainer.js b/src/components/timetable/timetableContainer.js index fbf66e39..b4f1666a 100644 --- a/src/components/timetable/timetableContainer.js +++ b/src/components/timetable/timetableContainer.js @@ -253,7 +253,7 @@ function getDuplicateRouteNote(duplicateRoutes, departure) { return duplicateRoutes.includes(departure.routeId) ? '*'.repeat(departure.direction) : null; } -function addMissingFridayNote(departure) { +export function addMissingFridayNote(departure) { return departure.dayType.length === 1 && departure.dayType.includes('Pe') && (!departure.note || !departure.note.includes('p')) diff --git a/src/util/routes.js b/src/util/routes.js index b96e83b0..8114c6cf 100644 --- a/src/util/routes.js +++ b/src/util/routes.js @@ -115,6 +115,8 @@ function routesToTree(routes, { stopZone, shortId }, height = 'auto', width = MA return root; } -export { - routesToTree, // eslint-disable-line import/prefer-default-export -}; +function shortenTrainParsedLineId(lineId) { + return lineId.replace(/^\d/, ''); +} + +export { routesToTree, shortenTrainParsedLineId };