basic shapes.txt with brouter #120

Closed
opened 2026-05-11 20:15:16 +00:00 by maxtkc · 0 comments
Owner

Summary

Add shape editing support by integrating with brouter-web as an external router. Users open the ShapesManager (new nav bar button), see all shapes in their feed, can delete them or replace them by uploading a GPX exported from brouter. The timetable view gains a brouter deep-link (all stops as waypoints, profile auto-detected from route_type) and the existing shape_id property row per trip becomes a <select> dropdown populated with all shape IDs in the database.

No custom routing or geometry drawing is built into the editor — brouter handles that externally.

Relevant context

Existing utilities to reuse — do not reinvent:

  • showModal(options) in src/modules/modal-utils.ts — the only way to show modals in this app. Takes { title, body, actions, onMount, escapeAction, enterAction, boxClassName }. The onMount(close) callback runs after the modal is in the DOM and is where event listeners and sub-modals are wired up. Returns a Promise<void>. No <dialog> elements needed in index.html.
  • patchUpdate(db, pm, table, id, before, after) in src/utils/patch-utils.ts — the correct helper for all user-initiated updates. Calls db.updateRow() then pm.recordUpdate() atomically. Use this instead of calling patchManager directly.
  • renderFormFields(configs) / readFormValues() in src/utils/field-component.ts — auto-generate form HTML from FieldConfig arrays; used by fares-modal for all add/edit sub-modals. Reuse for the "new shape" name input form.
  • showFormError() — shows validation errors inline; return true from an onClick handler to keep the modal open.
  • Event delegation pattern — fares-modal.ts wires all button clicks via a single listener on the panel container using data-action attributes. Use the same pattern in the shapes manager.
  • Sub-modal pattern — call showModal() from inside an onMount handler of a parent modal (see fares-modal.ts).

shapes DB:

  • src/modules/gtfs-database.tsshapes table; rows are individual points keyed by composite shape_id:shape_pt_sequence. To list unique shapes: query all rows and deduplicate by shape_id. Delete a shape: delete all rows where shape_id matches.
  • invalidateShape(shapeId) in src/modules/route-renderer.ts — must be called after any shape insert/delete to update the map.

Timetable:

  • src/modules/timetable-renderer.tsrenderPropertyCell() (lines 267–329) renders trip property inputs. Dispatches on config.type: 'select'<select>, 'number'<input type="number">, default → <input type="text">. The shape_id field currently falls through to text input. Need a special branch for config.field === 'shape_id' to render a <select> with available shape IDs.
  • updateTripProperty(trip_id, field, value) in src/modules/schedule-controller.ts — already handles the onchange logic including patchManager; reuse as-is for the shape_id select's onchange handler.
  • renderScheduleHeader() in timetable-renderer.ts — the right place to add the brouter link; already receives route entity (has route_type) and stop data is in TimetableData.stops.
  • brouter URL format: https://brouter.de/brouter-web/#map={zoom}/{lat}/{lon}/standard&lonlats={lon1},{lat1};{lon2},{lat2}&profile={profile} (brouter uses lon,lat order in lonlats).

Nav bar:

  • Static <button> in the navbar-end of src/index.html; click handler wired in src/index.ts.

Phase 1: GPX parser utility

Create a utility to parse a GPX file into shapes.txt rows using the browser's native DOMParser (no new dependencies).

  • Create src/utils/gpx-parser.ts
    • Export parseGPX(file: File, shapeId: string): Promise<Shapes[]>
    • Use DOMParser to parse the file text as XML
    • Collect all <trkpt lat="..." lon="..."> elements in document order across all <trk>/<trkseg> elements
    • Return Shapes[] with shape_id, shape_pt_lat (number), shape_pt_lon (number), shape_pt_sequence (1-indexed)
    • Throw a descriptive Error if no track points are found

Gotcha: shape_pt_lat / shape_pt_lon come from GPX lat/lon attributes (lat/lon order), but the brouter lonlats param is lon,lat — these are different concerns; the parser just reads lat/lon from GPX attributes correctly.


Phase 2: ShapesManager module

New module that uses showModal() from modal-utils.ts. No new <dialog> elements in index.html — everything is rendered dynamically.

  • Create src/modules/shapes-manager.ts

    • Constructor takes gtfsParser: GTFSParser, patchManager: PatchManager (no routeRenderer needed — map invalidation is automatic via the patch system's mapController.handlePatchChange)
    • Export ShapesManager class with a single public method: open(): Promise<void>
  • open() implementation:

    • Query all shape rows from DB; group by shape_id to get Map<string, number> (shape_id → point count)
    • Call showModal({ title: 'Shapes', body: ..., escapeAction: 0, onMount, actions: [{ label: 'Close', ... }] })
    • body renders a DaisyUI table: columns — Shape ID | Points | Actions. Each row has data-shape-id and buttons with data-action="replace" / data-action="delete".
    • Include a "+ New shape from GPX" button below the table with data-action="new".
    • In onMount, wire a single click listener on #shapes-panel using event delegation
  • Delete action (inside onMount handler):

    • Confirm with a showModal sub-modal ("Delete shape X and all its N points?") — Delete/Cancel
    • Query all rows for that shape_id, delete via deleteRows() + recordBatchDelete(), map updates automatically
    • Re-render the shapes list in the parent modal
  • Replace action (inside onMount handler):

    • pickGPXFile() helper creates a temporary <input type="file"> element (not in DOM), triggers .click(), handles change/cancel/window-focus events
    • On file selected: parseGPX(file, shapeId), delete old rows via deleteRows() + recordBatchDelete(), insert new via insertRows() + recordBatchInsert(), re-render
  • New shape action (inside onMount handler):

    • Sub-modal with shape_id text input, validates non-empty and not already taken, picks GPX file, inserts rows
  • Add shapes nav bar button in src/index.html navbar-end (before fares button) with a route/map SVG icon

  • In src/index.ts:

    • Instantiate ShapesManager (inject gtfsParser, patchManager)
    • Wire click to shapesManager.open()

Discovery: routeRenderer is not needed in ShapesManager — the patch system already handles shape invalidation automatically in MapController.handlePatchChange. The batch patch methods (recordBatchDelete, recordBatchInsert) are the correct API for multi-row shape operations, producing a single undoable batch entry.


Add a brouter deep-link to the timetable schedule header. The link opens brouter-web pre-populated with all stops as waypoints.

  • Add getBrouterProfile(routeType: string | number): string as a module-level function in src/modules/timetable-renderer.ts:

    • 0 (tram), 1 (metro), 2 (rail), 12 (monorail) → 'rail'
    • 3 (bus), 11 (trolleybus) → 'car-fast'
    • 4 (ferry) → 'river'
    • default → 'car-fast'
  • In renderDirectionTabs() in timetable-renderer.ts (not renderScheduleHeader() — that method is unused; the direction tabs section is the actual rendered header):

    • Added buildBrouterUrl(data: TimetableData): string | null helper (module-level) that filters stops with valid lat/lon, builds lonlats, averages coords for map center, calls getBrouterProfile
    • Renders <a href="{url}" target="_blank" rel="noopener" class="btn btn-xs btn-outline ml-auto">Open in brouter ↗</a> in the tabs row when ≥2 stops have coordinates

Gotcha: Stops are stored as strings in GTFS — parse as float before averaging and formatting to avoid precision issues.

Discovery: renderScheduleHeader() is defined but never called — the direction tabs div is the actual visible header. The brouter link was added there instead, using ml-auto to push it to the right of the tabs.


Phase 4: shape_id dropdown in the timetable

The shape_id row in trip property columns currently renders as a plain text input. Make it a <select> populated from the DB.

  • In TimetableRenderer, add availableShapeIds: string[] = [] as an instance field.

  • In schedule-controller.ts, before calling renderTimetableHTML(), load shape IDs: queryRows('shapes', {}), deduplicate by shape_id, sort, and store on this.renderer.availableShapeIds.

  • In renderPropertyCell(), added a branch before the existing type dispatch for config.field === 'shape_id':

    • Renders <select> with — none — first option + all availableShapeIds
    • Marks current trip's shape_id as selected
    • Uses identical onchange attribute pattern as the existing select branch

Gotcha: Loading shape IDs is async. Ensure availableShapeIds is populated before renderTimetableHTML() is called — since the timetable rendering chain is already async, await the query there.


Three small but related UI improvements.

Per-trip brouter link (move from direction tabs to each trip header column):

  • Refactor buildBrouterUrl in timetable-renderer.ts to accept (stops: Stops[], routeType: string | number): string | null instead of (data: TimetableData).
  • In renderDirectionTabs(), remove the brouterUrl/brouterLink block and the ml-auto link entirely.
  • In renderTimetableHeader(), for each trip compute tripStops = data.stops.filter((_, i) => trip.stopTimes.has(i)) (supersequence index i present in trip.stopTimes means this trip visits that stop). Pass tripStops and data.route.route_type to the refactored buildBrouterUrl.
  • Render the link (when non-null) inside the trip header <td>, below the trip ID and delete button, as <a href="{url}" target="_blank" rel="noopener" class="btn btn-xs btn-outline mt-1" title="Open in brouter">↗</a> (icon-only to keep the header compact).

Gotcha: trip.stopTimes keys are supersequence positions (numbers), same as the index in data.stops. A stop is only included if the trip actually has a time at that position. Trips with fewer than 2 geocoded stops get no link.

Upload icon for Replace button (shapes-manager.ts):

  • Add renderUploadIcon(sizeClass = 'h-4 w-4'): string to modal-utils.ts, exporting an upload/arrow-up-tray SVG from heroicons inline (same pattern as renderTrashIcon).
  • Import renderUploadIcon in shapes-manager.ts. Replace the ↑ Replace text button with an icon-only button: <button class="btn btn-xs btn-ghost" data-action="replace" data-shape-id="…" title="Replace with GPX">${renderUploadIcon()}</button>.

Click-away to close modals (modal-utils.ts):

  • When escapeAction is defined, add a 'click' listener on the .modal div (the backdrop element). In the handler, check e.target === modal (click landed on the backdrop, not inside the box) and call void triggerAction(options.escapeAction!). This fixes all modals that use showModal including the fares modal.

Gotcha: Must guard with e.target === modal — not !modal-box.contains(target) — because clicks on scrollbar tracks can also satisfy that check. The simplest reliable test is pointer on the outermost element.


Phase 6: Bug fix — shape not re-rendered after GPX replace

Root cause: replaceShape() does a delete then an insert as two separate patch records. The delete fires invalidateShape(shapeId, 'delete')handleShapeRemoved(shapeId), which removes the shape's features from routeFeatures and reassigns affected trips to stop-sequence geometry. The subsequent insert fires invalidateShape(shapeId, 'insert'), updates shapeIndex, but finds no features in routeFeatures using ::shape:${shapeId} (they were removed), so the trips remain on stop-sequence geometry until a full page reload.

Fix (in route-renderer.ts, invalidateShape):

  • After setting this.shapeIndex.set(shape_id, newCoords) and updating existing features in-place, count how many features were actually updated. If zero features matched the geomKey (i.e., the shape was deleted earlier and feature entries were removed by handleShapeRemoved), find all trips in the in-memory trips data that reference this shape_id and call this.invalidateTrip(trip.trip_id, 'insert', null, shape_id) for each. This re-creates the shape-based features for those trips.

Specifically:

  • Track let anyUpdated = false in the pts.length >= 2 branch. Set anyUpdated = true inside the loop over routeFeatures when a matching feature is found.
  • After the loop, if !anyUpdated: read this.gtfsParser.getFileDataSyncTyped<Trips>('trips.txt'), filter to trips where trip.shape_id === shape_id, and call this.invalidateTrip(trip.trip_id, 'insert', null, shape_id) for each.
  • Add a console.log noting the re-assignment (e.g. [RouteRenderer] invalidateShape: no existing features for shape ${shape_id}, re-assigning ${n} trips).

Implemented: invalidateShape now tracks anyUpdated in the pts.length >= 2 branch. When no features matched (post-delete scenario), it reads trips.txt, filters to trips referencing the shape, and calls invalidateTrip('insert') for each. invalidateTrip calls addTripToBucket which uses the newly-set shapeIndex entry, so trips are re-bucketed with shape geometry without a reload. scheduleSetData() is still called once at the end of invalidateShape.

Gotcha: invalidateTrip is already defined on RouteRenderer and handles the 'insert' case by setting up shape-based geometry when afterShapeId is provided. Calling it here reuses that existing logic without new paths. Do not call scheduleSetData() an extra time — invalidateTrip calls it internally, and the outer invalidateShape also calls it at the end (so it may fire twice; that's a no-op since it debounces).


Original Issue

To support editing shapes.txt, instead of building a fancy router and editor into the tool, lets leverage brouter-web.

We can link the user to:

https://brouter.de/brouter-web/#map=8/47.294/7.855/standard&lonlats=6.383057,47.953145;8.580322,47.465236;9.673462,47.479329&profile=car-fast

or profile=rail for train routes. Here is the repo for brouter: https://github.com/abrensch/brouter

They will be able to export the route as gpx, then import it in the gtfs editor.

We will likely need a shapes manager modal and nav bar button for managing shapes, allowing some sort of filtering, as well as replacing them with uploaded gpx files and uploading new ones. In the timetable, we should change the shape id select to a dropdown and provide all possible shapes.

We should link to brouter from the timetable that includes every stop for that trip.

## Summary Add shape editing support by integrating with brouter-web as an external router. Users open the ShapesManager (new nav bar button), see all shapes in their feed, can delete them or replace them by uploading a GPX exported from brouter. The timetable view gains a brouter deep-link (all stops as waypoints, profile auto-detected from route_type) and the existing shape_id property row per trip becomes a `<select>` dropdown populated with all shape IDs in the database. No custom routing or geometry drawing is built into the editor — brouter handles that externally. ## Relevant context **Existing utilities to reuse — do not reinvent:** - **`showModal(options)`** in `src/modules/modal-utils.ts` — the only way to show modals in this app. Takes `{ title, body, actions, onMount, escapeAction, enterAction, boxClassName }`. The `onMount(close)` callback runs after the modal is in the DOM and is where event listeners and sub-modals are wired up. Returns a `Promise<void>`. No `<dialog>` elements needed in index.html. - **`patchUpdate(db, pm, table, id, before, after)`** in `src/utils/patch-utils.ts` — the correct helper for all user-initiated updates. Calls `db.updateRow()` then `pm.recordUpdate()` atomically. Use this instead of calling patchManager directly. - **`renderFormFields(configs)`** / **`readFormValues()`** in `src/utils/field-component.ts` — auto-generate form HTML from FieldConfig arrays; used by fares-modal for all add/edit sub-modals. Reuse for the "new shape" name input form. - **`showFormError()`** — shows validation errors inline; return `true` from an `onClick` handler to keep the modal open. - **Event delegation pattern** — fares-modal.ts wires all button clicks via a single listener on the panel container using `data-action` attributes. Use the same pattern in the shapes manager. - **Sub-modal pattern** — call `showModal()` from inside an `onMount` handler of a parent modal (see fares-modal.ts). **shapes DB:** - `src/modules/gtfs-database.ts` — `shapes` table; rows are individual points keyed by composite `shape_id:shape_pt_sequence`. To list unique shapes: query all rows and deduplicate by `shape_id`. Delete a shape: delete all rows where `shape_id` matches. - **`invalidateShape(shapeId)`** in `src/modules/route-renderer.ts` — must be called after any shape insert/delete to update the map. **Timetable:** - `src/modules/timetable-renderer.ts` — `renderPropertyCell()` (lines 267–329) renders trip property inputs. Dispatches on `config.type`: `'select'` → `<select>`, `'number'` → `<input type="number">`, default → `<input type="text">`. The `shape_id` field currently falls through to text input. Need a special branch for `config.field === 'shape_id'` to render a `<select>` with available shape IDs. - `updateTripProperty(trip_id, field, value)` in `src/modules/schedule-controller.ts` — already handles the onchange logic including patchManager; reuse as-is for the shape_id select's onchange handler. - `renderScheduleHeader()` in `timetable-renderer.ts` — the right place to add the brouter link; already receives route entity (has `route_type`) and stop data is in `TimetableData.stops`. - brouter URL format: `https://brouter.de/brouter-web/#map={zoom}/{lat}/{lon}/standard&lonlats={lon1},{lat1};{lon2},{lat2}&profile={profile}` (brouter uses lon,lat order in lonlats). **Nav bar:** - Static `<button>` in the navbar-end of `src/index.html`; click handler wired in `src/index.ts`. --- ## Phase 1: GPX parser utility Create a utility to parse a GPX file into `shapes.txt` rows using the browser's native `DOMParser` (no new dependencies). - [x] Create `src/utils/gpx-parser.ts` - Export `parseGPX(file: File, shapeId: string): Promise<Shapes[]>` - Use `DOMParser` to parse the file text as XML - Collect all `<trkpt lat="..." lon="...">` elements in document order across all `<trk>/<trkseg>` elements - Return `Shapes[]` with `shape_id`, `shape_pt_lat` (number), `shape_pt_lon` (number), `shape_pt_sequence` (1-indexed) - Throw a descriptive `Error` if no track points are found *Gotcha*: `shape_pt_lat` / `shape_pt_lon` come from GPX `lat`/`lon` attributes (lat/lon order), but the brouter `lonlats` param is lon,lat — these are different concerns; the parser just reads lat/lon from GPX attributes correctly. --- ## Phase 2: ShapesManager module New module that uses `showModal()` from modal-utils.ts. No new `<dialog>` elements in index.html — everything is rendered dynamically. - [x] Create `src/modules/shapes-manager.ts` - Constructor takes `gtfsParser: GTFSParser`, `patchManager: PatchManager` (no routeRenderer needed — map invalidation is automatic via the patch system's `mapController.handlePatchChange`) - Export `ShapesManager` class with a single public method: `open(): Promise<void>` - [x] `open()` implementation: - Query all shape rows from DB; group by `shape_id` to get `Map<string, number>` (shape_id → point count) - Call `showModal({ title: 'Shapes', body: ..., escapeAction: 0, onMount, actions: [{ label: 'Close', ... }] })` - `body` renders a DaisyUI table: columns — Shape ID | Points | Actions. Each row has `data-shape-id` and buttons with `data-action="replace"` / `data-action="delete"`. - Include a "+ New shape from GPX" button below the table with `data-action="new"`. - In `onMount`, wire a single click listener on `#shapes-panel` using event delegation - [x] Delete action (inside onMount handler): - Confirm with a `showModal` sub-modal ("Delete shape X and all its N points?") — Delete/Cancel - Query all rows for that `shape_id`, delete via `deleteRows()` + `recordBatchDelete()`, map updates automatically - Re-render the shapes list in the parent modal - [x] Replace action (inside onMount handler): - `pickGPXFile()` helper creates a temporary `<input type="file">` element (not in DOM), triggers `.click()`, handles `change`/`cancel`/window-focus events - On file selected: `parseGPX(file, shapeId)`, delete old rows via `deleteRows()` + `recordBatchDelete()`, insert new via `insertRows()` + `recordBatchInsert()`, re-render - [x] New shape action (inside onMount handler): - Sub-modal with shape_id text input, validates non-empty and not already taken, picks GPX file, inserts rows - [x] Add shapes nav bar button in `src/index.html` navbar-end (before fares button) with a route/map SVG icon - [x] In `src/index.ts`: - Instantiate `ShapesManager` (inject `gtfsParser`, `patchManager`) - Wire click to `shapesManager.open()` *Discovery*: `routeRenderer` is not needed in ShapesManager — the patch system already handles shape invalidation automatically in `MapController.handlePatchChange`. The batch patch methods (`recordBatchDelete`, `recordBatchInsert`) are the correct API for multi-row shape operations, producing a single undoable batch entry. --- ## Phase 3: Brouter link in the timetable Add a brouter deep-link to the timetable schedule header. The link opens brouter-web pre-populated with all stops as waypoints. - [x] Add `getBrouterProfile(routeType: string | number): string` as a module-level function in `src/modules/timetable-renderer.ts`: - `0` (tram), `1` (metro), `2` (rail), `12` (monorail) → `'rail'` - `3` (bus), `11` (trolleybus) → `'car-fast'` - `4` (ferry) → `'river'` - default → `'car-fast'` - [x] In `renderDirectionTabs()` in `timetable-renderer.ts` (not `renderScheduleHeader()` — that method is unused; the direction tabs section is the actual rendered header): - Added `buildBrouterUrl(data: TimetableData): string | null` helper (module-level) that filters stops with valid lat/lon, builds `lonlats`, averages coords for map center, calls `getBrouterProfile` - Renders `<a href="{url}" target="_blank" rel="noopener" class="btn btn-xs btn-outline ml-auto">Open in brouter ↗</a>` in the tabs row when ≥2 stops have coordinates *Gotcha*: Stops are stored as strings in GTFS — parse as float before averaging and formatting to avoid precision issues. *Discovery*: `renderScheduleHeader()` is defined but never called — the direction tabs div is the actual visible header. The brouter link was added there instead, using `ml-auto` to push it to the right of the tabs. --- ## Phase 4: shape_id dropdown in the timetable The shape_id row in trip property columns currently renders as a plain text input. Make it a `<select>` populated from the DB. - [x] In `TimetableRenderer`, add `availableShapeIds: string[] = []` as an instance field. - [x] In `schedule-controller.ts`, before calling `renderTimetableHTML()`, load shape IDs: `queryRows('shapes', {})`, deduplicate by `shape_id`, sort, and store on `this.renderer.availableShapeIds`. - [x] In `renderPropertyCell()`, added a branch before the existing type dispatch for `config.field === 'shape_id'`: - Renders `<select>` with `— none —` first option + all `availableShapeIds` - Marks current trip's `shape_id` as selected - Uses identical `onchange` attribute pattern as the existing select branch *Gotcha*: Loading shape IDs is async. Ensure `availableShapeIds` is populated before `renderTimetableHTML()` is called — since the timetable rendering chain is already async, `await` the query there. --- ## Phase 5: UI polish — per-trip brouter link, upload icon, click-away modal Three small but related UI improvements. **Per-trip brouter link** (move from direction tabs to each trip header column): - [x] Refactor `buildBrouterUrl` in `timetable-renderer.ts` to accept `(stops: Stops[], routeType: string | number): string | null` instead of `(data: TimetableData)`. - [x] In `renderDirectionTabs()`, remove the `brouterUrl`/`brouterLink` block and the `ml-auto` link entirely. - [x] In `renderTimetableHeader()`, for each trip compute `tripStops = data.stops.filter((_, i) => trip.stopTimes.has(i))` (supersequence index `i` present in `trip.stopTimes` means this trip visits that stop). Pass `tripStops` and `data.route.route_type` to the refactored `buildBrouterUrl`. - [x] Render the link (when non-null) inside the trip header `<td>`, below the trip ID and delete button, as `<a href="{url}" target="_blank" rel="noopener" class="btn btn-xs btn-outline mt-1" title="Open in brouter">↗</a>` (icon-only to keep the header compact). *Gotcha*: `trip.stopTimes` keys are supersequence positions (numbers), same as the index in `data.stops`. A stop is only included if the trip actually has a time at that position. Trips with fewer than 2 geocoded stops get no link. **Upload icon for Replace button** (`shapes-manager.ts`): - [x] Add `renderUploadIcon(sizeClass = 'h-4 w-4'): string` to `modal-utils.ts`, exporting an upload/arrow-up-tray SVG from heroicons inline (same pattern as `renderTrashIcon`). - [x] Import `renderUploadIcon` in `shapes-manager.ts`. Replace the `↑ Replace` text button with an icon-only button: `<button class="btn btn-xs btn-ghost" data-action="replace" data-shape-id="…" title="Replace with GPX">${renderUploadIcon()}</button>`. **Click-away to close modals** (`modal-utils.ts`): - [x] When `escapeAction` is defined, add a `'click'` listener on the `.modal` div (the backdrop element). In the handler, check `e.target === modal` (click landed on the backdrop, not inside the box) and call `void triggerAction(options.escapeAction!)`. This fixes all modals that use `showModal` including the fares modal. *Gotcha*: Must guard with `e.target === modal` — not `!modal-box.contains(target)` — because clicks on scrollbar tracks can also satisfy that check. The simplest reliable test is pointer on the outermost element. --- ## Phase 6: Bug fix — shape not re-rendered after GPX replace **Root cause**: `replaceShape()` does a delete then an insert as two separate patch records. The delete fires `invalidateShape(shapeId, 'delete')` → `handleShapeRemoved(shapeId)`, which removes the shape's features from `routeFeatures` and reassigns affected trips to stop-sequence geometry. The subsequent insert fires `invalidateShape(shapeId, 'insert')`, updates `shapeIndex`, but finds no features in `routeFeatures` using `::shape:${shapeId}` (they were removed), so the trips remain on stop-sequence geometry until a full page reload. **Fix** (in `route-renderer.ts`, `invalidateShape`): - [x] After setting `this.shapeIndex.set(shape_id, newCoords)` and updating existing features in-place, count how many features were actually updated. If zero features matched the geomKey (i.e., the shape was deleted earlier and feature entries were removed by `handleShapeRemoved`), find all trips in the in-memory trips data that reference this `shape_id` and call `this.invalidateTrip(trip.trip_id, 'insert', null, shape_id)` for each. This re-creates the shape-based features for those trips. Specifically: - [x] Track `let anyUpdated = false` in the `pts.length >= 2` branch. Set `anyUpdated = true` inside the loop over `routeFeatures` when a matching feature is found. - [x] After the loop, if `!anyUpdated`: read `this.gtfsParser.getFileDataSyncTyped<Trips>('trips.txt')`, filter to trips where `trip.shape_id === shape_id`, and call `this.invalidateTrip(trip.trip_id, 'insert', null, shape_id)` for each. - [x] Add a `console.log` noting the re-assignment (e.g. `[RouteRenderer] invalidateShape: no existing features for shape ${shape_id}, re-assigning ${n} trips`). *Implemented*: `invalidateShape` now tracks `anyUpdated` in the `pts.length >= 2` branch. When no features matched (post-delete scenario), it reads trips.txt, filters to trips referencing the shape, and calls `invalidateTrip('insert')` for each. `invalidateTrip` calls `addTripToBucket` which uses the newly-set shapeIndex entry, so trips are re-bucketed with shape geometry without a reload. `scheduleSetData()` is still called once at the end of `invalidateShape`. *Gotcha*: `invalidateTrip` is already defined on `RouteRenderer` and handles the `'insert'` case by setting up shape-based geometry when `afterShapeId` is provided. Calling it here reuses that existing logic without new paths. Do not call `scheduleSetData()` an extra time — `invalidateTrip` calls it internally, and the outer `invalidateShape` also calls it at the end (so it may fire twice; that's a no-op since it debounces). --- ## Original Issue To support editing shapes.txt, instead of building a fancy router and editor into the tool, lets leverage brouter-web. We can link the user to: https://brouter.de/brouter-web/#map=8/47.294/7.855/standard&lonlats=6.383057,47.953145;8.580322,47.465236;9.673462,47.479329&profile=car-fast or profile=rail for train routes. Here is the repo for brouter: https://github.com/abrensch/brouter They will be able to export the route as gpx, then import it in the gtfs editor. We will likely need a shapes manager modal and nav bar button for managing shapes, allowing some sort of filtering, as well as replacing them with uploaded gpx files and uploading new ones. In the timetable, we should change the shape id select to a dropdown and provide all possible shapes. We should link to brouter from the timetable that includes every stop for that trip.
maxtkc self-assigned this 2026-05-11 20:15:16 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gtfs.zone/coloring-book#120
No description provided.