From 252805e4cdbe81d41d2cfd848ebb0dacff7dff2d Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Fri, 7 Jun 2024 18:23:18 -0400
Subject: [PATCH 1/6] fix(RouteDetails): Extract and fix method for keeping
 patterns.

---
 __tests__/util/viewer.js                 | 78 ++++++++++++++++++-
 lib/components/viewers/route-details.tsx | 98 +++++++-----------------
 lib/util/viewer.js                       | 54 ++++++++++++-
 3 files changed, 158 insertions(+), 72 deletions(-)

diff --git a/__tests__/util/viewer.js b/__tests__/util/viewer.js
index 500e5d4d8..fbed8ef32 100644
--- a/__tests__/util/viewer.js
+++ b/__tests__/util/viewer.js
@@ -1,5 +1,19 @@
 import '../test-utils/mock-window-url'
-import { extractHeadsignFromPattern } from '../../lib/util/viewer'
+import {
+  extractHeadsignFromPattern,
+  extractMainHeadsigns
+} from '../../lib/util/viewer'
+
+function createStop(id) {
+  return {
+    id,
+    name: id
+  }
+}
+
+function prefixHeadsign(pattern) {
+  pattern.headsign = `To ${pattern.headsign}`
+}
 
 describe('util > viewer', () => {
   describe('extractHeadsignFromPattern', () => {
@@ -26,4 +40,66 @@ describe('util > viewer', () => {
       )
     })
   })
+
+  describe('extractMainHeadsigns', () => {
+    it('should retain the essential patterns', () => {
+      // Consider the following patterns P1, P2, P3 of the same route with the same headsigns:
+      // Stops 1  2  3  4  5  6  7 --> direction of travel
+      // P1:   o--o--o--o--o
+      // P2:         o--o-----o--o
+      // P3:         o--o--o
+      //
+      // P3 should be removed because it is a subset of P1.
+      // P1 and P2 should be kept.
+      // Patterns are assumed in descending length order because
+      // pre-sorting happened before extractMainHeadsigns is invoked (key order matters).
+      const headsign = 'Everett via Lynnwood'
+      const route = '512'
+      const patterns = {
+        P1: {
+          headsign,
+          id: 'P1',
+          name: '512 to Everett Station (CommTrans:2861) from SODO Busway & S Royal Brougham Way (kcm:99267)',
+          patternGeometry: {
+            length: 1404,
+            points: 'p1-points'
+          },
+          stops: [
+            createStop('S1'),
+            createStop('S2'),
+            createStop('S3'),
+            createStop('S4'),
+            createStop('S5')
+          ]
+        },
+        P2: {
+          headsign,
+          id: 'P2',
+          name: '512 to Hewitt Ave & Virginia Ave (CommTrans:427)',
+          patternGeometry: {
+            length: 1072,
+            points: 'p2-points'
+          },
+          stops: [
+            createStop('S3'),
+            createStop('S4'),
+            createStop('S6'),
+            createStop('S7')
+          ]
+        },
+        P3: {
+          headsign,
+          id: 'P3',
+          name: '512 to Everett Station (CommTrans:2861) from Northgate Station Bay 2 (CommTrans:2192)',
+          patternGeometry: {
+            length: 987,
+            points: 'p3-points'
+          },
+          stops: [createStop('S3'), createStop('S4'), createStop('S5')]
+        }
+      }
+      const headsignData = extractMainHeadsigns(patterns, route, prefixHeadsign)
+      expect(headsignData.length).toBe(2)
+    })
+  })
 })
diff --git a/lib/components/viewers/route-details.tsx b/lib/components/viewers/route-details.tsx
index aaacb4a42..8a3ac937b 100644
--- a/lib/components/viewers/route-details.tsx
+++ b/lib/components/viewers/route-details.tsx
@@ -6,8 +6,9 @@ import React, { Component } from 'react'
 import styled from 'styled-components'
 
 import * as uiActions from '../../actions/ui'
+import { DEFAULT_ROUTE_COLOR } from '../util/colors'
 import {
-  extractHeadsignFromPattern,
+  extractMainHeadsigns,
   getRouteColorBasedOnSettings
 } from '../../util/viewer'
 import { getOperatorName } from '../../util/state'
@@ -30,7 +31,6 @@ import {
   StopLink,
   Stop as StyledStop
 } from './styled'
-import { DEFAULT_ROUTE_COLOR } from '../util/colors'
 
 const PatternSelectButton = styled(UnstyledButton)`
   span {
@@ -77,6 +77,13 @@ class RouteDetails extends Component<Props> {
     setViewedStop(stop)
   }
 
+  _prefixHeadsign = (pattern: Pattern) => {
+    return this.props.intl.formatMessage(
+      { id: 'components.RouteDetails.headsignTo' },
+      { ...pattern }
+    )
+  }
+
   render() {
     const { intl, operator, patternId, route, setHoveredStop } = this.props
     const { agency, patterns = {}, shortName, url } = route
@@ -86,74 +93,25 @@ class RouteDetails extends Component<Props> {
 
     const routeColor = getRouteColorBasedOnSettings(operator, route)
 
-    const headsigns = Object.entries(patterns)
-      .map(
-        ([id, pat]): PatternSummary => ({
-          geometryLength: pat.patternGeometry?.length || 0,
-          headsign: extractHeadsignFromPattern(pat, shortName),
-          id,
-          lastStop: pat.stops?.[pat.stops?.length - 1]?.name
-        })
-      )
-      // Address duplicate headsigns.
-      .reduce((prev: PatternSummary[], cur) => {
-        const amended = prev
-        const alreadyExistingIndex = prev.findIndex(
-          (h) => h.headsign === cur.headsign
-        )
-        // If the headsign is a duplicate, and the last stop of the pattern is not the headsign,
-        // amend the headsign with the last stop name in parenthesis.
-        // e.g. "Headsign (Last Stop)"
-        if (
-          alreadyExistingIndex >= 0 &&
-          cur.lastStop &&
-          cur.headsign !== cur.lastStop
-        ) {
-          cur.headsign = intl.formatMessage(
-            { id: 'components.RouteDetails.headsignTo' },
-            { ...cur }
-          )
-
-          // If there are only two total patterns, then we should rename
-          // both of them
-          if (amended.length === 1 && Object.entries(patterns).length === 2) {
-            amended[0].headsign = intl.formatMessage(
-              { id: 'components.RouteDetails.headsignTo' },
-              { ...amended[0] }
-            )
-            amended.push(cur)
-            return amended
-          }
-        }
-
-        // With all remaining duplicate headsigns, only keep the pattern with the
-        // longest geometry.
-        if (alreadyExistingIndex >= 0) {
-          if (
-            amended[alreadyExistingIndex].geometryLength < cur.geometryLength
-          ) {
-            amended[alreadyExistingIndex] = cur
-          }
-        } else {
-          amended.push(cur)
-        }
-        return amended
-      }, [])
-      .sort((a, b) => {
-        // sort by number of vehicles on that pattern
-        const aVehicleCount =
-          route.vehicles?.filter((vehicle) => vehicle.patternId === a.id)
-            .length || 0
-        const bVehicleCount =
-          route.vehicles?.filter((vehicle) => vehicle.patternId === b.id)
-            .length || 0
-
-        // if both have the same count, sort by pattern geometry length
-        if (aVehicleCount === bVehicleCount) {
-          return b.geometryLength - a.geometryLength
-        }
-        return bVehicleCount - aVehicleCount
-      })
+    const headsigns = extractMainHeadsigns(
+      patterns,
+      shortName,
+      this._prefixHeadsign
+    ).sort((a, b) => {
+      // sort by number of vehicles on that pattern
+      const aVehicleCount =
+        route.vehicles?.filter((vehicle) => vehicle.patternId === a.id)
+          .length || 0
+      const bVehicleCount =
+        route.vehicles?.filter((vehicle) => vehicle.patternId === b.id)
+          .length || 0
+
+      // if both have the same count, sort by pattern geometry length
+      if (aVehicleCount === bVehicleCount) {
+        return b.geometryLength - a.geometryLength
+      }
+      return bVehicleCount - aVehicleCount
+    })
 
     const patternSelectLabel = intl.formatMessage({
       id: 'components.RouteDetails.selectADirection'
diff --git a/lib/util/viewer.js b/lib/util/viewer.js
index d5abd67c4..b4ba34258 100644
--- a/lib/util/viewer.js
+++ b/lib/util/viewer.js
@@ -3,8 +3,9 @@
 import { getMostReadableTextColor } from '@opentripplanner/core-utils/lib/route'
 import tinycolor from 'tinycolor2'
 
-import { checkForRouteModeOverride } from './config'
 import { DARK_TEXT_GREY } from '../components/util/colors'
+
+import { checkForRouteModeOverride } from './config'
 import { getOperatorAndRoute } from './state'
 import { isBlank } from './ui'
 
@@ -96,6 +97,57 @@ export function extractHeadsignFromPattern(pattern, routeShortName = null) {
   return headsign
 }
 
+export function extractMainHeadsigns(patterns, shortName, prefixHeadsign) {
+  const mapped = Object.entries(patterns).map(
+    ([id, pat]) /* : PatternSummary */ => ({
+      geometryLength: pat.patternGeometry?.length || 0,
+      headsign: extractHeadsignFromPattern(pat, shortName),
+      id,
+      lastStop: pat.stops?.[pat.stops?.length - 1]?.name
+    })
+  )
+
+  // Address duplicate headsigns.
+  return mapped.reduce((prev /* : PatternSummary[] */, cur) => {
+    const amended = prev
+    const alreadyExistingIndex = prev.findIndex(
+      (h) => h.headsign === cur.headsign
+    )
+    // If the headsign is a duplicate, and the last stop of the pattern is not the headsign,
+    // amend the headsign with the last stop name in parenthesis.
+    // e.g. "Headsign (Last Stop)"
+    if (
+      alreadyExistingIndex >= 0 &&
+      cur.lastStop &&
+      cur.headsign !== cur.lastStop
+    ) {
+      prefixHeadsign(cur)
+
+      // If there are only two total patterns, then we should rename
+      // both of them
+      if (amended.length === 1 && Object.entries(patterns).length === 2) {
+        prefixHeadsign(amended[0])
+        amended.push(cur)
+        return amended
+      }
+    }
+
+    // With all remaining duplicate headsigns with the same last stops, only keep the pattern with the
+    // longest geometry.
+    if (
+      alreadyExistingIndex >= 0 &&
+      amended[alreadyExistingIndex].lastStop === cur.lastStop
+    ) {
+      if (amended[alreadyExistingIndex].geometryLength < cur.geometryLength) {
+        amended[alreadyExistingIndex] = cur
+      }
+    } else {
+      amended.push(cur)
+    }
+    return amended
+  }, [])
+}
+
 /**
  * Gets the mode string from either an OTP Route or RouteShort model. The OTP
  * Route model returns the mode as an integer type whereas the RouteShort model

From 72ce2faeb5936c3d249a103dc47a6ffda9ca14b9 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Fri, 7 Jun 2024 18:28:05 -0400
Subject: [PATCH 2/6] test(util/viewer): Refactor test for extractMainPatterns

---
 __tests__/util/viewer.js | 23 ++++++-----------------
 1 file changed, 6 insertions(+), 17 deletions(-)

diff --git a/__tests__/util/viewer.js b/__tests__/util/viewer.js
index fbed8ef32..27faa8045 100644
--- a/__tests__/util/viewer.js
+++ b/__tests__/util/viewer.js
@@ -4,11 +4,11 @@ import {
   extractMainHeadsigns
 } from '../../lib/util/viewer'
 
-function createStop(id) {
-  return {
+function createStops(ids) {
+  return ids.map((id) => ({
     id,
     name: id
-  }
+  }))
 }
 
 function prefixHeadsign(pattern) {
@@ -64,13 +64,7 @@ describe('util > viewer', () => {
             length: 1404,
             points: 'p1-points'
           },
-          stops: [
-            createStop('S1'),
-            createStop('S2'),
-            createStop('S3'),
-            createStop('S4'),
-            createStop('S5')
-          ]
+          stops: createStops(['S1', 'S2', 'S3', 'S4', 'S5'])
         },
         P2: {
           headsign,
@@ -80,12 +74,7 @@ describe('util > viewer', () => {
             length: 1072,
             points: 'p2-points'
           },
-          stops: [
-            createStop('S3'),
-            createStop('S4'),
-            createStop('S6'),
-            createStop('S7')
-          ]
+          stops: createStops(['S3', 'S4', 'S6', 'S7'])
         },
         P3: {
           headsign,
@@ -95,7 +84,7 @@ describe('util > viewer', () => {
             length: 987,
             points: 'p3-points'
           },
-          stops: [createStop('S3'), createStop('S4'), createStop('S5')]
+          stops: createStops(['S3', 'S4', 'S5'])
         }
       }
       const headsignData = extractMainHeadsigns(patterns, route, prefixHeadsign)

From 218ba1cf01452666ba02ee7c8c3f6d1b3bd66c7b Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Fri, 7 Jun 2024 18:41:00 -0400
Subject: [PATCH 3/6] refactor(util/viewer): Rename editHeadsign method

---
 __tests__/util/viewer.js                 | 8 +++++---
 lib/components/viewers/route-details.tsx | 6 +++---
 lib/util/viewer.js                       | 6 +++---
 3 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/__tests__/util/viewer.js b/__tests__/util/viewer.js
index 27faa8045..6fe0a6d28 100644
--- a/__tests__/util/viewer.js
+++ b/__tests__/util/viewer.js
@@ -11,8 +11,8 @@ function createStops(ids) {
   }))
 }
 
-function prefixHeadsign(pattern) {
-  pattern.headsign = `To ${pattern.headsign}`
+function editHeadsign(pattern) {
+  pattern.headsign = `${pattern.headsign} (${pattern.lastStop})`
 }
 
 describe('util > viewer', () => {
@@ -87,8 +87,10 @@ describe('util > viewer', () => {
           stops: createStops(['S3', 'S4', 'S5'])
         }
       }
-      const headsignData = extractMainHeadsigns(patterns, route, prefixHeadsign)
+      const headsignData = extractMainHeadsigns(patterns, route, editHeadsign)
       expect(headsignData.length).toBe(2)
+      expect(headsignData[0].headsign).toBe(headsign)
+      expect(headsignData[1].headsign).toBe('Everett via Lynnwood (S7)')
     })
   })
 })
diff --git a/lib/components/viewers/route-details.tsx b/lib/components/viewers/route-details.tsx
index 8a3ac937b..b172c1f48 100644
--- a/lib/components/viewers/route-details.tsx
+++ b/lib/components/viewers/route-details.tsx
@@ -1,7 +1,7 @@
 import { connect } from 'react-redux'
 import { FormattedMessage, injectIntl, IntlShape } from 'react-intl'
 import { getMostReadableTextColor } from '@opentripplanner/core-utils/lib/route'
-import { Stop, TransitOperator } from '@opentripplanner/types'
+import { Pattern, Stop, TransitOperator } from '@opentripplanner/types'
 import React, { Component } from 'react'
 import styled from 'styled-components'
 
@@ -77,7 +77,7 @@ class RouteDetails extends Component<Props> {
     setViewedStop(stop)
   }
 
-  _prefixHeadsign = (pattern: Pattern) => {
+  _editHeadsign = (pattern: Pattern) => {
     return this.props.intl.formatMessage(
       { id: 'components.RouteDetails.headsignTo' },
       { ...pattern }
@@ -96,7 +96,7 @@ class RouteDetails extends Component<Props> {
     const headsigns = extractMainHeadsigns(
       patterns,
       shortName,
-      this._prefixHeadsign
+      this._editHeadsign
     ).sort((a, b) => {
       // sort by number of vehicles on that pattern
       const aVehicleCount =
diff --git a/lib/util/viewer.js b/lib/util/viewer.js
index b4ba34258..a943ef37a 100644
--- a/lib/util/viewer.js
+++ b/lib/util/viewer.js
@@ -97,7 +97,7 @@ export function extractHeadsignFromPattern(pattern, routeShortName = null) {
   return headsign
 }
 
-export function extractMainHeadsigns(patterns, shortName, prefixHeadsign) {
+export function extractMainHeadsigns(patterns, shortName, editHeadsign) {
   const mapped = Object.entries(patterns).map(
     ([id, pat]) /* : PatternSummary */ => ({
       geometryLength: pat.patternGeometry?.length || 0,
@@ -121,12 +121,12 @@ export function extractMainHeadsigns(patterns, shortName, prefixHeadsign) {
       cur.lastStop &&
       cur.headsign !== cur.lastStop
     ) {
-      prefixHeadsign(cur)
+      editHeadsign(cur)
 
       // If there are only two total patterns, then we should rename
       // both of them
       if (amended.length === 1 && Object.entries(patterns).length === 2) {
-        prefixHeadsign(amended[0])
+        editHeadsign(amended[0])
         amended.push(cur)
         return amended
       }

From eb6cfb216a199632e3b98a18e96173e0e8182263 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Fri, 7 Jun 2024 18:49:40 -0400
Subject: [PATCH 4/6] fix(RouteDetails): Actually edit the headsign

---
 lib/components/viewers/route-details.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/components/viewers/route-details.tsx b/lib/components/viewers/route-details.tsx
index b172c1f48..88304731e 100644
--- a/lib/components/viewers/route-details.tsx
+++ b/lib/components/viewers/route-details.tsx
@@ -78,7 +78,7 @@ class RouteDetails extends Component<Props> {
   }
 
   _editHeadsign = (pattern: Pattern) => {
-    return this.props.intl.formatMessage(
+    pattern.headsign = this.props.intl.formatMessage(
       { id: 'components.RouteDetails.headsignTo' },
       { ...pattern }
     )

From b93cf10ff42bfb3ae48ac31e2df9009139686caf Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Mon, 10 Jun 2024 11:29:00 -0400
Subject: [PATCH 5/6] refactor(util/pattern-viewer): Move pattern extraction to
 new file, rename imports

---
 __tests__/util/pattern-viewer.ts         | 68 +++++++++++++++++++++++
 __tests__/util/viewer.js                 | 69 +-----------------------
 lib/components/viewers/route-details.tsx | 19 ++-----
 lib/util/pattern-viewer.ts               | 65 ++++++++++++++++++++++
 lib/util/viewer.js                       | 51 ------------------
 5 files changed, 139 insertions(+), 133 deletions(-)
 create mode 100644 __tests__/util/pattern-viewer.ts
 create mode 100644 lib/util/pattern-viewer.ts

diff --git a/__tests__/util/pattern-viewer.ts b/__tests__/util/pattern-viewer.ts
new file mode 100644
index 000000000..c7a76ee16
--- /dev/null
+++ b/__tests__/util/pattern-viewer.ts
@@ -0,0 +1,68 @@
+import '../test-utils/mock-window-url'
+import { extractMainHeadsigns } from '../../lib/util/pattern-viewer'
+
+function createStops(ids) {
+  return ids.map((id) => ({
+    id,
+    name: id
+  }))
+}
+
+function editHeadsign(pattern) {
+  pattern.headsign = `${pattern.headsign} (${pattern.lastStop})`
+}
+
+describe('util > pattern-viewer', () => {
+  describe('extractMainHeadsigns', () => {
+    it('should retain the essential patterns', () => {
+      // Consider the following patterns P1, P2, P3 of the same route with the same headsigns:
+      // Stops 1  2  3  4  5  6  7 --> direction of travel
+      // P1:   o--o--o--o--o
+      // P2:         o--o-----o--o
+      // P3:         o--o--o
+      //
+      // P3 should be removed because it is a subset of P1.
+      // P1 and P2 should be kept.
+      // Patterns are assumed in descending length order because
+      // pre-sorting happened before extractMainHeadsigns is invoked (key order matters).
+      const headsign = 'Everett via Lynnwood'
+      const route = '512'
+      const patterns = {
+        P1: {
+          headsign,
+          id: 'P1',
+          name: '512 to Everett Station (CommTrans:2861) from SODO Busway & S Royal Brougham Way (kcm:99267)',
+          patternGeometry: {
+            length: 1404,
+            points: 'p1-points'
+          },
+          stops: createStops(['S1', 'S2', 'S3', 'S4', 'S5'])
+        },
+        P2: {
+          headsign,
+          id: 'P2',
+          name: '512 to Hewitt Ave & Virginia Ave (CommTrans:427)',
+          patternGeometry: {
+            length: 1072,
+            points: 'p2-points'
+          },
+          stops: createStops(['S3', 'S4', 'S6', 'S7'])
+        },
+        P3: {
+          headsign,
+          id: 'P3',
+          name: '512 to Everett Station (CommTrans:2861) from Northgate Station Bay 2 (CommTrans:2192)',
+          patternGeometry: {
+            length: 987,
+            points: 'p3-points'
+          },
+          stops: createStops(['S3', 'S4', 'S5'])
+        }
+      }
+      const headsignData = extractMainHeadsigns(patterns, route, editHeadsign)
+      expect(headsignData.length).toBe(2)
+      expect(headsignData[0].headsign).toBe(headsign)
+      expect(headsignData[1].headsign).toBe('Everett via Lynnwood (S7)')
+    })
+  })
+})
diff --git a/__tests__/util/viewer.js b/__tests__/util/viewer.js
index 6fe0a6d28..500e5d4d8 100644
--- a/__tests__/util/viewer.js
+++ b/__tests__/util/viewer.js
@@ -1,19 +1,5 @@
 import '../test-utils/mock-window-url'
-import {
-  extractHeadsignFromPattern,
-  extractMainHeadsigns
-} from '../../lib/util/viewer'
-
-function createStops(ids) {
-  return ids.map((id) => ({
-    id,
-    name: id
-  }))
-}
-
-function editHeadsign(pattern) {
-  pattern.headsign = `${pattern.headsign} (${pattern.lastStop})`
-}
+import { extractHeadsignFromPattern } from '../../lib/util/viewer'
 
 describe('util > viewer', () => {
   describe('extractHeadsignFromPattern', () => {
@@ -40,57 +26,4 @@ describe('util > viewer', () => {
       )
     })
   })
-
-  describe('extractMainHeadsigns', () => {
-    it('should retain the essential patterns', () => {
-      // Consider the following patterns P1, P2, P3 of the same route with the same headsigns:
-      // Stops 1  2  3  4  5  6  7 --> direction of travel
-      // P1:   o--o--o--o--o
-      // P2:         o--o-----o--o
-      // P3:         o--o--o
-      //
-      // P3 should be removed because it is a subset of P1.
-      // P1 and P2 should be kept.
-      // Patterns are assumed in descending length order because
-      // pre-sorting happened before extractMainHeadsigns is invoked (key order matters).
-      const headsign = 'Everett via Lynnwood'
-      const route = '512'
-      const patterns = {
-        P1: {
-          headsign,
-          id: 'P1',
-          name: '512 to Everett Station (CommTrans:2861) from SODO Busway & S Royal Brougham Way (kcm:99267)',
-          patternGeometry: {
-            length: 1404,
-            points: 'p1-points'
-          },
-          stops: createStops(['S1', 'S2', 'S3', 'S4', 'S5'])
-        },
-        P2: {
-          headsign,
-          id: 'P2',
-          name: '512 to Hewitt Ave & Virginia Ave (CommTrans:427)',
-          patternGeometry: {
-            length: 1072,
-            points: 'p2-points'
-          },
-          stops: createStops(['S3', 'S4', 'S6', 'S7'])
-        },
-        P3: {
-          headsign,
-          id: 'P3',
-          name: '512 to Everett Station (CommTrans:2861) from Northgate Station Bay 2 (CommTrans:2192)',
-          patternGeometry: {
-            length: 987,
-            points: 'p3-points'
-          },
-          stops: createStops(['S3', 'S4', 'S5'])
-        }
-      }
-      const headsignData = extractMainHeadsigns(patterns, route, editHeadsign)
-      expect(headsignData.length).toBe(2)
-      expect(headsignData[0].headsign).toBe(headsign)
-      expect(headsignData[1].headsign).toBe('Everett via Lynnwood (S7)')
-    })
-  })
 })
diff --git a/lib/components/viewers/route-details.tsx b/lib/components/viewers/route-details.tsx
index 88304731e..594ea31a4 100644
--- a/lib/components/viewers/route-details.tsx
+++ b/lib/components/viewers/route-details.tsx
@@ -1,17 +1,15 @@
 import { connect } from 'react-redux'
 import { FormattedMessage, injectIntl, IntlShape } from 'react-intl'
 import { getMostReadableTextColor } from '@opentripplanner/core-utils/lib/route'
-import { Pattern, Stop, TransitOperator } from '@opentripplanner/types'
+import { Stop, TransitOperator } from '@opentripplanner/types'
 import React, { Component } from 'react'
 import styled from 'styled-components'
 
 import * as uiActions from '../../actions/ui'
 import { DEFAULT_ROUTE_COLOR } from '../util/colors'
-import {
-  extractMainHeadsigns,
-  getRouteColorBasedOnSettings
-} from '../../util/viewer'
+import { extractMainHeadsigns, PatternSummary } from '../../util/pattern-viewer'
 import { getOperatorName } from '../../util/state'
+import { getRouteColorBasedOnSettings } from '../../util/viewer'
 import { LinkOpensNewWindow } from '../util/externalLink'
 import {
   SetViewedRouteHandler,
@@ -40,13 +38,6 @@ const PatternSelectButton = styled(UnstyledButton)`
   }
 `
 
-interface PatternSummary {
-  geometryLength: number
-  headsign: string
-  id: string
-  lastStop?: string
-}
-
 interface Props {
   intl: IntlShape
   operator: TransitOperator
@@ -77,11 +68,11 @@ class RouteDetails extends Component<Props> {
     setViewedStop(stop)
   }
 
-  _editHeadsign = (pattern: Pattern) => {
+  _editHeadsign = (pattern: PatternSummary) => {
     pattern.headsign = this.props.intl.formatMessage(
       { id: 'components.RouteDetails.headsignTo' },
       { ...pattern }
-    )
+    ) as string
   }
 
   render() {
diff --git a/lib/util/pattern-viewer.ts b/lib/util/pattern-viewer.ts
new file mode 100644
index 000000000..1b974df36
--- /dev/null
+++ b/lib/util/pattern-viewer.ts
@@ -0,0 +1,65 @@
+import { Pattern } from '../components/util/types'
+
+import { extractHeadsignFromPattern } from './viewer'
+
+export interface PatternSummary {
+  geometryLength: number
+  headsign: string
+  id: string
+  lastStop?: string
+}
+
+export function extractMainHeadsigns(
+  patterns: Record<string, Pattern>,
+  shortName: string,
+  editHeadsign: (pattern: PatternSummary) => void
+): PatternSummary[] {
+  const mapped = Object.entries(patterns).map(
+    ([id, pat]): PatternSummary => ({
+      geometryLength: pat.patternGeometry?.length || 0,
+      headsign: extractHeadsignFromPattern(pat, shortName),
+      id,
+      lastStop: pat.stops?.[pat.stops?.length - 1]?.name
+    })
+  )
+
+  // Address duplicate headsigns.
+  return mapped.reduce((prev: PatternSummary[], cur) => {
+    const amended = prev
+    const alreadyExistingIndex = prev.findIndex(
+      (h) => h.headsign === cur.headsign
+    )
+    // If the headsign is a duplicate, and the last stop of the pattern is not the headsign,
+    // amend the headsign with the last stop name in parenthesis.
+    // e.g. "Headsign (Last Stop)"
+    if (
+      alreadyExistingIndex >= 0 &&
+      cur.lastStop &&
+      cur.headsign !== cur.lastStop
+    ) {
+      editHeadsign(cur)
+
+      // If there are only two total patterns, then we should rename
+      // both of them
+      if (amended.length === 1 && Object.entries(patterns).length === 2) {
+        editHeadsign(amended[0])
+        amended.push(cur)
+        return amended
+      }
+    }
+
+    // With all remaining duplicate headsigns with the same last stops, only keep the pattern with the
+    // longest geometry.
+    if (
+      alreadyExistingIndex >= 0 &&
+      amended[alreadyExistingIndex].lastStop === cur.lastStop
+    ) {
+      if (amended[alreadyExistingIndex].geometryLength < cur.geometryLength) {
+        amended[alreadyExistingIndex] = cur
+      }
+    } else {
+      amended.push(cur)
+    }
+    return amended
+  }, [])
+}
diff --git a/lib/util/viewer.js b/lib/util/viewer.js
index a943ef37a..9aabdacc2 100644
--- a/lib/util/viewer.js
+++ b/lib/util/viewer.js
@@ -97,57 +97,6 @@ export function extractHeadsignFromPattern(pattern, routeShortName = null) {
   return headsign
 }
 
-export function extractMainHeadsigns(patterns, shortName, editHeadsign) {
-  const mapped = Object.entries(patterns).map(
-    ([id, pat]) /* : PatternSummary */ => ({
-      geometryLength: pat.patternGeometry?.length || 0,
-      headsign: extractHeadsignFromPattern(pat, shortName),
-      id,
-      lastStop: pat.stops?.[pat.stops?.length - 1]?.name
-    })
-  )
-
-  // Address duplicate headsigns.
-  return mapped.reduce((prev /* : PatternSummary[] */, cur) => {
-    const amended = prev
-    const alreadyExistingIndex = prev.findIndex(
-      (h) => h.headsign === cur.headsign
-    )
-    // If the headsign is a duplicate, and the last stop of the pattern is not the headsign,
-    // amend the headsign with the last stop name in parenthesis.
-    // e.g. "Headsign (Last Stop)"
-    if (
-      alreadyExistingIndex >= 0 &&
-      cur.lastStop &&
-      cur.headsign !== cur.lastStop
-    ) {
-      editHeadsign(cur)
-
-      // If there are only two total patterns, then we should rename
-      // both of them
-      if (amended.length === 1 && Object.entries(patterns).length === 2) {
-        editHeadsign(amended[0])
-        amended.push(cur)
-        return amended
-      }
-    }
-
-    // With all remaining duplicate headsigns with the same last stops, only keep the pattern with the
-    // longest geometry.
-    if (
-      alreadyExistingIndex >= 0 &&
-      amended[alreadyExistingIndex].lastStop === cur.lastStop
-    ) {
-      if (amended[alreadyExistingIndex].geometryLength < cur.geometryLength) {
-        amended[alreadyExistingIndex] = cur
-      }
-    } else {
-      amended.push(cur)
-    }
-    return amended
-  }, [])
-}
-
 /**
  * Gets the mode string from either an OTP Route or RouteShort model. The OTP
  * Route model returns the mode as an integer type whereas the RouteShort model

From 2b6028609085ddb8bbb15211105ce6c9e0a9ddf6 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Mon, 10 Jun 2024 13:13:15 -0400
Subject: [PATCH 6/6] test(util/pattern-viewer): Remove specific pattern names.

---
 __tests__/util/pattern-viewer.ts | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/__tests__/util/pattern-viewer.ts b/__tests__/util/pattern-viewer.ts
index c7a76ee16..79b3957f7 100644
--- a/__tests__/util/pattern-viewer.ts
+++ b/__tests__/util/pattern-viewer.ts
@@ -16,7 +16,7 @@ describe('util > pattern-viewer', () => {
   describe('extractMainHeadsigns', () => {
     it('should retain the essential patterns', () => {
       // Consider the following patterns P1, P2, P3 of the same route with the same headsigns:
-      // Stops 1  2  3  4  5  6  7 --> direction of travel
+      // Stops S1 S2 S3 S4 S5 S6 S7 --> direction of travel
       // P1:   o--o--o--o--o
       // P2:         o--o-----o--o
       // P3:         o--o--o
@@ -31,7 +31,7 @@ describe('util > pattern-viewer', () => {
         P1: {
           headsign,
           id: 'P1',
-          name: '512 to Everett Station (CommTrans:2861) from SODO Busway & S Royal Brougham Way (kcm:99267)',
+          name: 'P1 Pattern name',
           patternGeometry: {
             length: 1404,
             points: 'p1-points'
@@ -41,7 +41,7 @@ describe('util > pattern-viewer', () => {
         P2: {
           headsign,
           id: 'P2',
-          name: '512 to Hewitt Ave & Virginia Ave (CommTrans:427)',
+          name: 'P2 Pattern name',
           patternGeometry: {
             length: 1072,
             points: 'p2-points'
@@ -51,7 +51,7 @@ describe('util > pattern-viewer', () => {
         P3: {
           headsign,
           id: 'P3',
-          name: '512 to Everett Station (CommTrans:2861) from Northgate Station Bay 2 (CommTrans:2192)',
+          name: 'P3 Pattern name',
           patternGeometry: {
             length: 987,
             points: 'p3-points'
@@ -62,7 +62,7 @@ describe('util > pattern-viewer', () => {
       const headsignData = extractMainHeadsigns(patterns, route, editHeadsign)
       expect(headsignData.length).toBe(2)
       expect(headsignData[0].headsign).toBe(headsign)
-      expect(headsignData[1].headsign).toBe('Everett via Lynnwood (S7)')
+      expect(headsignData[1].headsign).toBe(`${headsign} (S7)`)
     })
   })
 })