Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ jobs:
with:
checkAndTestOutsideDocker: true
codeCoverageEnabled: true
performRelease: false
performRelease: true
checkAndTestInsideDocker: false
43 changes: 43 additions & 0 deletions src/components/timetable/departureUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,46 @@ export const prepareOrderedDepartureHoursByRoute = departures => {
lastDepartures,
};
};

/**
* Groups route IDs by their mode+trunk combination.
* Routes in the same group share mode and trunkRoute flag.
*
* @param {string[]} routeIds
* @param {Object.<string, {mode: string, trunkRoute: boolean}>} routeIdToModeMap
* @returns {Array<{key: string, routeIds: string[], mode: string, trunkRoute: boolean}>}
* Ordered list of groups preserving original route order.
*/
export const groupRoutesByModeAndTrunk = (routeIds, routeIdToModeMap) => {
const groupMap = new Map();
const groupOrder = [];

for (const routeId of routeIds) {
const desc = routeIdToModeMap[routeId];
if (!desc) continue;
const key = `${desc.mode}_${desc.trunkRoute ? '1' : '0'}`;
if (!groupMap.has(key)) {
groupMap.set(key, { key, routeIds: [], mode: desc.mode, trunkRoute: !!desc.trunkRoute });
groupOrder.push(key);
}
groupMap.get(key).routeIds.push(routeId);
}

return groupOrder.map(k => groupMap.get(k));
};

/**
* For a group with 2+ routes, computes the synthetic "combined" column data:
* - intervals: max interval per hour range across all routes in the group
* - firstDepartures / lastDepartures: always null (shown as blank in the UI)
*
* @param {string[]} groupRouteIds
* @param {Array<{hours: string, intervals: Object.<string, number|null>}>} groupedDepartures
* @returns {Array<{hours: string, maxInterval: number|null}>}
*/
export const computeCombinedColumn = (groupRouteIds, groupedDepartures) => {
return groupedDepartures.map(({ hours, intervals }) => {
const vals = groupRouteIds.map(id => intervals[id]).filter(v => v != null);
return { hours, maxInterval: vals.length > 0 ? Math.max(...vals) : null };
});
};
218 changes: 157 additions & 61 deletions src/components/timetable/intervalTimetable.css
Original file line number Diff line number Diff line change
@@ -1,116 +1,212 @@
.routeHeadings {
/* ─── Outer container ────────────────────────────────────────────────────── */

.intervalDisplay {
position: relative;
color: black;
/* Stretch to fill .root including its horizontal padding */
margin-left: calc(-1 * var(--border-radius) / 2);
margin-right: calc(-1 * var(--border-radius) / 2);
padding-left: calc(var(--border-radius) / 2);
padding-right: calc(var(--border-radius) / 2);
}

/* ─── Stripe background — absolutely fills the container ─────────────────── */

.stripeBackground {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 0.2em;
font-size: 20px;
align-items: center;
flex-direction: column;
pointer-events: none;
/* padding-top/bottom set inline to align stripes with interval rows */
}

.interval {
color: var(--hsl-blue);
.stripeRow {
width: 100%;
flex-shrink: 0;
}

.timetableRoot {
color: black;
.stripeRowAlt {
background-color: var(--timetable-accent-color);
}

.timetableRoot > * {
margin: 0 0 0 calc(-1 * var(--border-radius));
padding: 0.2em calc(0.45em + var(--border-radius));
/* ─── Column layout — on top of stripes ──────────────────────────────────── */

.columnLayout {
position: relative;
z-index: 1;
display: flex;
flex-direction: row;
align-items: stretch;
}

.timetableRoot > *:nth-child(odd) {
background-color: var(--timetable-accent-color);
.routeGroupsContainer {
flex: 1;
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: space-evenly;
}

.firstAndLastDepartures {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
gap: 0.2em;
padding: 0.2em 0 0.2em calc(0.45em + var(--border-radius));
margin: 0 0 0 calc(-1 * var(--border-radius));
/* ─── Hours column ───────────────────────────────────────────────────────── */

.hoursColumn {
display: flex;
flex-direction: column;
flex-shrink: 0;
}

.firstAndLastDepartures div {
/* ─── Route group wrapper — single border element ────────────────────────── */

.routeGroup {
display: flex;
justify-content: center;
flex-direction: row;
border-radius: 8px;
margin: 0 3px;
}

.departureTitles {
.routeGroupBordered {
border: 2px solid;
}

/* ─── Individual route column ────────────────────────────────────────────── */

.routeColumn {
display: flex;
flex-direction: column;
align-items: center;
min-width: 60px;
padding: 0 4px;
}

/* ─── Combined headway column — left border separator ────────────────────── */

.combinedColumn {
position: relative;
width: 90px;
min-width: 90px;
flex-shrink: 0;
}

.firstAndLastDepartureValues {
.combinedColumn::before {
content: '';
position: absolute;
left: 0;
top: 8px;
bottom: 8px;
width: 2px;
background-color: var(--combined-separator-color);
}

/* ─── Heading cell (fixed height set inline) ─────────────────────────────── */

.headingCell {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
box-sizing: border-box;
}

.timetableRoutes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
padding: 0 0 0.5em 0.45em;
gap: 0.2em;
}
/* ─── Departure value cell (fixed height set inline) ─────────────────────── */

.timetableRoutes div {
.departureCell {
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
width: 100%;
box-sizing: border-box;
}

.timetableMinutes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
/* ─── Label cell in hours column (fixed height set inline) ───────────────── */

.labelCell {
display: flex;
align-items: center;
box-sizing: border-box;
}

.icon {
width: 20px;
height: 20px;
/* ─── Individual interval cell (height set inline) ───────────────────────── */

.intervalCell {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
box-sizing: border-box;
}

.compactPaddingRight {
padding-right: calc(0.45em + var(--border-radius));
/* ─── Hours interval cell (height set inline) ────────────────────────────── */

.intervalHoursCell {
display: flex;
align-items: center;
box-sizing: border-box;
}

.routeHeadingsNonCompact {
padding: 0.2em 0 0.2em calc(0.45em + var(--border-radius));
/* ─── Route heading ──────────────────────────────────────────────────────── */

.routeHeadings {
display: flex;
gap: 0.2em;
font-size: 20px;
align-items: center;
}

/* ─── Combined headway heading text ─────────────────────────────────────── */

.combinedHeading {
display: flex;
flex-direction: column;
font-size: 11px;
line-height: 1.3;
text-align: center;
word-break: break-word;
overflow-wrap: break-word;
}

.timetableMinutesNonCompact {
padding: 0.2em 0 0.2em calc(0.45em + var(--border-radius));
/* ─── Departure label stacks ─────────────────────────────────────────────── */

.departureTitles {
display: flex;
flex-direction: column;
font-size: 13px;
line-height: 1.3;
}

/* ─── Two-panel split layout ─────────────────────────────────────────────── */

.flexContainer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: calc((1 * var(--border-radius)) + 8px);
}

.leftPanel {
flex: 1;
flex: 1 1 auto;
min-width: 0;
}

.rightPanel {
flex: 1 1 300px;
min-width: 300px;
}

.busRoutesContainer {
display: flex;
gap: 12px;
margin-left: calc(-1 * var(--border-radius));
padding: 0 0 0.4em 0.938em;
}

.rightPanel {
flex: 1;
}

.hours {
/* Default styles for hours div */
}

.hoursLong {
font-size: 19px;
}
/* ─── Icon ───────────────────────────────────────────────────────────────── */

.timetableMinutesLong {
height: 30px;
}

.intervalLong {
font-size: 19px;
.icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
Loading
Loading