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 };