From 355882a0472a6c25de18a6f65e6df15588a33f27 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup Date: Thu, 13 Nov 2025 15:40:57 -0500 Subject: [PATCH 1/9] fix(util/viewer): Use last stop if no pattern headsign. --- __tests__/util/viewer.js | 11 +++++++++++ lib/util/viewer.js | 26 +++++++++++++++----------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/__tests__/util/viewer.js b/__tests__/util/viewer.js index 500e5d4d8..39bfac1c7 100644 --- a/__tests__/util/viewer.js +++ b/__tests__/util/viewer.js @@ -25,5 +25,16 @@ describe('util > viewer', () => { 'Sesame Street' ) }) + + it('should use the last stop name of a pattern if no headsign is provided', () => { + const stopNames = ['First stop', 'Second stop', 'Last stop'] + const pattern = { + // If no headsigns are provided in feed, OTP might use the route short name as pattern name/description. + desc: '49', + name: '49', + stops: stopNames.map((s) => ({ name: s })) + } + expect(extractHeadsignFromPattern(pattern, '49')).toBe('Last stop') + }) }) }) diff --git a/lib/util/viewer.js b/lib/util/viewer.js index ff589872e..7ddf622eb 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -68,11 +68,13 @@ export function routeIsValid(route, routeId) { * @returns headsign of pattern */ export function extractHeadsignFromPattern(pattern, routeShortName = null) { - let headsign = pattern.headsign + const { desc, headsign: originalHeadsign, stops } = pattern + + let headsign = originalHeadsign // In case stop time headsign is blank, extract headsign from the pattern 'desc' attribute // (format: '49 to ()[ from ( Date: Thu, 13 Nov 2025 16:38:06 -0500 Subject: [PATCH 2/9] test(util/viewer): Add test, tweak comment --- __tests__/util/viewer.js | 10 +++++++++- lib/util/viewer.js | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/__tests__/util/viewer.js b/__tests__/util/viewer.js index 39bfac1c7..422770eb3 100644 --- a/__tests__/util/viewer.js +++ b/__tests__/util/viewer.js @@ -26,12 +26,20 @@ describe('util > viewer', () => { ) }) + it('should use the description provided if it is not the same as the route short name', () => { + const pattern = { + desc: 'Southbound Express' + } + expect(extractHeadsignFromPattern(pattern, '49')).toBe( + 'Southbound Express' + ) + }) + it('should use the last stop name of a pattern if no headsign is provided', () => { const stopNames = ['First stop', 'Second stop', 'Last stop'] const pattern = { // If no headsigns are provided in feed, OTP might use the route short name as pattern name/description. desc: '49', - name: '49', stops: stopNames.map((s) => ({ name: s })) } expect(extractHeadsignFromPattern(pattern, '49')).toBe('Last stop') diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 7ddf622eb..d0f7ce6ab 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -107,6 +107,7 @@ export function extractHeadsignFromPattern(pattern, routeShortName = null) { headsign = desc || '' } + // If nothing else works, use the name of the last stop in the pattern. if (isBlank(headsign) && stops.length) { const lastStop = stops[stops.length - 1] headsign = lastStop.name From f478770b3c76b2186bed56b3e9e61d781f08340a Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup Date: Thu, 13 Nov 2025 17:10:52 -0500 Subject: [PATCH 3/9] improvement(RouteDetail): Remove dropdown if single pattern. --- lib/components/viewers/route-details.tsx | 52 +++++++++++++++--------- lib/components/viewers/styled.ts | 15 ++++++- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/lib/components/viewers/route-details.tsx b/lib/components/viewers/route-details.tsx index 6218e04dc..bcd9e0d89 100644 --- a/lib/components/viewers/route-details.tsx +++ b/lib/components/viewers/route-details.tsx @@ -22,6 +22,7 @@ import OperatorLogo from '../util/operator-logo' import { Container, + HeadsignLabel, HeadsignSelectLabel, LogoLinkContainer, PatternContainer, @@ -147,26 +148,37 @@ class RouteDetails extends Component { {headsigns && headsigns.length > 0 && ( - - - - - {headsigns.map((h: PatternSummary) => ( -
  • - this._headSignButtonClicked(h.id)} - value={h.id} - > - {h.headsign} - -
  • - ))} -
    + {headsigns.length > 1 ? ( + <> + + + + + {headsigns.map((h: PatternSummary) => ( +
  • + this._headSignButtonClicked(h.id)} + value={h.id} + > + {h.headsign} + +
  • + ))} +
    + + ) : ( + <> + + + + {patternSelectName} + + )}
    )} {pattern && ( diff --git a/lib/components/viewers/styled.ts b/lib/components/viewers/styled.ts index b2843e72f..93361891f 100644 --- a/lib/components/viewers/styled.ts +++ b/lib/components/viewers/styled.ts @@ -1,4 +1,4 @@ -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { getBaseColor, grey } from '../util/colors' @@ -43,11 +43,22 @@ export const LogoLinkContainer = styled.div<{ } } ` -export const HeadsignSelectLabel = styled.label` + +const headsignStyle = css` font-size: 18px; margin-bottom: 0; ` +export const HeadsignSelectLabel = styled.label` + ${headsignStyle} +` + +export const HeadsignLabel = styled.span` + ${headsignStyle} + font-weight: bold; + width: auto !important; +` + export const PatternContainer = styled.div` align-items: center; background-color: inherit; From 8ed1ac04fd0505b7d200854dd33ea9fb89621d9e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup Date: Thu, 13 Nov 2025 17:15:30 -0500 Subject: [PATCH 4/9] style(util/viewer): Move import, remove eslint exceptions --- lib/util/viewer.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/util/viewer.js b/lib/util/viewer.js index d0f7ce6ab..1b5f78c3e 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -1,10 +1,8 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable @typescript-eslint/no-use-before-define */ import { getMostReadableTextColor } from '@opentripplanner/core-utils/lib/route' +import { isTransitLeg } from '@opentripplanner/core-utils/lib/itinerary' import tinycolor from 'tinycolor2' import { DARK_TEXT_GREY } from '../components/util/colors' -import { isTransitLeg } from '@opentripplanner/core-utils/lib/itinerary' import { checkForRouteModeOverride } from './config' import { getOperatorAndRoute } from './state' From a5d8bf0f15c6db1fb8a720a19a34462807615c84 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup Date: Thu, 13 Nov 2025 17:27:24 -0500 Subject: [PATCH 5/9] improvement(util/viewer): Remove leading 'to ' text. --- __tests__/util/viewer.js | 7 +++++++ lib/util/viewer.js | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/__tests__/util/viewer.js b/__tests__/util/viewer.js index 422770eb3..82189882b 100644 --- a/__tests__/util/viewer.js +++ b/__tests__/util/viewer.js @@ -10,6 +10,13 @@ describe('util > viewer', () => { expect(extractHeadsignFromPattern(pattern)).toBe('Sesame Street') }) + it('should remove leading "To " text in a headsign (English only)', () => { + const pattern = { + headsign: 'To Angle Lake' + } + expect(extractHeadsignFromPattern(pattern)).toBe('Angle Lake') + }) + it('should extract headsign from pattern description if no headsign present', () => { const pattern = { desc: '49 to Sesame Street (70:3562) from Airport Station' diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 1b5f78c3e..37e5bd451 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -69,6 +69,15 @@ export function extractHeadsignFromPattern(pattern, routeShortName = null) { const { desc, headsign: originalHeadsign, stops } = pattern let headsign = originalHeadsign + + // Remove leading "To " text from headsign (English-specific) + if ( + !isBlank(headsign) && + (headsign.startsWith('To ') || headsign.startsWith('to ')) + ) { + headsign = headsign.substring(3) + } + // In case stop time headsign is blank, extract headsign from the pattern 'desc' attribute // (format: '49 to ()[ from ( Date: Fri, 14 Nov 2025 07:56:54 -0500 Subject: [PATCH 6/9] refactor(util/viewer): Reduce extract headsign complexity --- lib/util/viewer.js | 84 ++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 37e5bd451..fc030c9cc 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -66,61 +66,43 @@ export function routeIsValid(route, routeId) { * @returns headsign of pattern */ export function extractHeadsignFromPattern(pattern, routeShortName = null) { - const { desc, headsign: originalHeadsign, stops } = pattern + const { desc, headsign, stops } = pattern + const descNotRouteShortName = routeShortName && desc !== `${routeShortName}` - let headsign = originalHeadsign - - // Remove leading "To " text from headsign (English-specific) - if ( - !isBlank(headsign) && - (headsign.startsWith('To ') || headsign.startsWith('to ')) - ) { - headsign = headsign.substring(3) - } - - // In case stop time headsign is blank, extract headsign from the pattern 'desc' attribute - // (format: '49 to ()[ from ( ()[ from ( d?.match(/(?: to )(.*?)(?: \()/)?.[1], + // If that regex didn't work, try the old regex + (d) => d?.match(/ to ([^(from)]+) \(.+\)/)?.[1], + // In some cases the string generated by OTP doesn't include a " " + // see https://github.com/opentripplanner/OpenTripPlanner/blob/7ce72cf554e6cf469d683869db5684eae3ce86f8/src/main/java/org/opentripplanner/graph_builder/module/TripPatternNamer.java#L72-L183 + // In this case, we need to rely on the routeShortName to determine what to remove + (d) => + descNotRouteShortName && + d?.match(/^[^(]*$/)?.[0]?.replace(`${routeShortName} `, ''), + // If the headsign is still blank, use the description, if different than the route short name. + (d) => descNotRouteShortName && d, + // If nothing else works, use the name of the last stop in the pattern. + () => stops?.[stops.length - 1]?.name + ] + + for (let i = 0; i < attempts.length; i++) { + const newHeadsign = attempts[i](desc) + if (!isBlank(newHeadsign)) { + return newHeadsign + } } + return '' } - - // If the headsign is still blank, show the description if different than the route short name. - if (isBlank(headsign) && descNotSameAsRouteShortName) { - headsign = desc || '' - } - - // If nothing else works, use the name of the last stop in the pattern. - if (isBlank(headsign) && stops.length) { - const lastStop = stops[stops.length - 1] - headsign = lastStop.name - } - - return headsign } /** From b13ee2d7b6e6dfd72fdc3a53af5f6e21341644d1 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup Date: Fri, 14 Nov 2025 07:57:30 -0500 Subject: [PATCH 7/9] test(NearbyView): Update snapshot --- .../viewers/__snapshots__/nearby-view.js.snap | 228 +++++++++--------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/__tests__/components/viewers/__snapshots__/nearby-view.js.snap b/__tests__/components/viewers/__snapshots__/nearby-view.js.snap index d0e320c37..923b2c419 100644 --- a/__tests__/components/viewers/__snapshots__/nearby-view.js.snap +++ b/__tests__/components/viewers/__snapshots__/nearby-view.js.snap @@ -12905,7 +12905,7 @@ exports[`components > viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >
  • viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

    1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

        viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

      1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

          viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

        1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

            viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

          1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

              viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

            1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

              1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                  viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

                1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                    viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

                  1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                      viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

                    1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                        viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

                      1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                          viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

                        1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                            viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

                          1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                              viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

                            1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

                              1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                  viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

                                1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                    viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

                                  1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                      viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

                                    1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                        viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

                                      1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                          viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

                                        1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                            viewers > nearby view renders proper scooter dates 1`] = ` className="pattern-row-item" >

                                          1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                              viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

                                            1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >

                                              1. viewers > nearby view renders proper scooter dates 1`] = ` className="departure-times" >

                                                  viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` >