refactor: one endpoint-name cleaner for both map renderers

The scan that replaced the parenthetical-stripping regex went into the Leaflet
overlay and its Mapbox GL counterpart as two byte-identical copies, which is
what the duplication gate caught. trackColors.ts next door exists for exactly
this reason - both renderers read a track's colour through one function so it
cannot come out different on one map than the other - so the cleaner follows it.

Behaviour is unchanged. The test brute-forces the new function against the regex
it replaced over all 55,987 names up to six characters of "ab() \t", and pins the
32k-space input that used to take a second.
This commit is contained in:
Maurice
2026-08-25 21:45:13 +02:00
committed by Maurice
parent a3ad53b3bf
commit c435033343
4 changed files with 88 additions and 44 deletions
@@ -12,6 +12,7 @@ import type mapboxgl from 'mapbox-gl'
import { Plane, Train, Ship, Car, Bus, Sailboat, Bike, CarTaxiFront, Route, TramFront } from 'lucide-react'
import { getTransitMapSegments } from './transitGeometry'
import { geodesicArcs } from './flightGeodesy'
import { cleanEndpointName } from './reservationName'
import { escapeHtml } from '@trek/shared'
import type { Reservation, ReservationEndpoint } from '../../types'
@@ -94,27 +95,6 @@ function computeDuration(from: ReservationEndpoint, to: ReservationEndpoint, fal
return h > 0 ? `${h}h ${m}m` : `${m}m`
}
// "Wien Hbf (Vienna)" → "Wien Hbf". Done as a scan, not /\s*\([^)]*\)/g: the leading
// \s* overlaps the engine's own restart-at-every-position scan, so a name that is a
// long run of spaces with no "(" after it backtracks quadratically (1.2s at 40k).
// Same matches: the whitespace directly before a "(" that has a ")" after it goes
// too, and an unclosed "(" is left alone.
const cleanName = (name: string) => {
let out = ''
let cursor = 0
for (;;) {
const open = name.indexOf('(', cursor)
if (open === -1) break
const close = name.indexOf(')', open + 1)
if (close === -1) break
let start = open
while (start > cursor && /\s/.test(name[start - 1])) start--
out += name.slice(cursor, start)
cursor = close + 1
}
return (out + name.slice(cursor)).trim()
}
// ── item building ─────────────────────────────────────────────────────────
interface TransportItem {
res: Reservation
@@ -368,7 +348,7 @@ export class ReservationMapboxOverlay {
for (const item of visibleItems) {
const showLabel = this.opts.showEndpointLabels && labelVisibleIds.has(item.res.id)
for (const ep of item.waypoints) {
const label = showLabel ? (ep.code || cleanName(ep.name)) : null
const label = showLabel ? (ep.code || cleanEndpointName(ep.name)) : null
const el = document.createElement('div')
el.innerHTML = endpointMarkerHtml(item.type, label)
const inner = el.firstElementChild as HTMLElement | null