-
Notifications
You must be signed in to change notification settings - Fork 6
Feat: gtfs visualization refactor, UX enhancements and bug fixes #1419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import { type RouteIdsInput } from '../utils/precompute'; | ||
| import { | ||
| type ExpressionSpecification, | ||
| type LngLatBoundsLike, | ||
| } from 'maplibre-gl'; | ||
|
|
||
| // Extract route_ids list from the PMTiles property (stringified JSON) | ||
| export function extractRouteIds(val: RouteIdsInput): string[] { | ||
| if (Array.isArray(val)) return val.map(String); | ||
| if (typeof val === 'string') { | ||
| try { | ||
| const parsed = JSON.parse(val); | ||
| if (Array.isArray(parsed)) return parsed.map(String); | ||
| } catch {} | ||
| // fallback: pull "quoted" tokens | ||
| const out: string[] = []; | ||
| val.replace(/"([^"]+)"/g, (_: unknown, id: string) => { | ||
| out.push(id); | ||
| return ''; | ||
| }); | ||
| if (out.length > 0) return out; | ||
| // fallback2: CSV-ish | ||
| return val | ||
| .split(',') | ||
| .map((s: string) => s.trim()) | ||
| .filter(Boolean); | ||
| } | ||
| return []; | ||
| } | ||
|
|
||
| export function generateStopColorExpression( | ||
| routeIdToColor: Record<string, string>, | ||
| fallback: string = '#888', | ||
| ): string | ExpressionSpecification { | ||
| const expression: Array<string | ExpressionSpecification> = []; | ||
|
|
||
| const isHex = (s: string): boolean => | ||
| /^[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(s); | ||
|
|
||
| for (const [routeId, raw] of Object.entries(routeIdToColor)) { | ||
| if (raw == null) continue; | ||
| const hex = String(raw).trim().replace(/^#/, ''); | ||
| if (!isHex(hex)) continue; // skip empty/invalid colors | ||
|
|
||
| // route_ids is a string of quoted ids; keep your quoted match style | ||
| expression.push(['in', `"${routeId}"`, ['get', 'route_ids']], `#${hex}`); | ||
| } | ||
|
|
||
| // If nothing valid was added, just use the fallback color directly | ||
| if (expression.length === 0) { | ||
| return fallback; | ||
| } | ||
|
|
||
| expression.push(fallback); | ||
| return ['case', ...expression] as ExpressionSpecification; | ||
| } | ||
|
|
||
| export const getBoundsFromCoordinates = ( | ||
| coordinates: Array<[number, number]>, | ||
| ): LngLatBoundsLike => { | ||
| let minLng = Number.POSITIVE_INFINITY; | ||
| let minLat = Number.POSITIVE_INFINITY; | ||
| let maxLng = Number.NEGATIVE_INFINITY; | ||
| let maxLat = Number.NEGATIVE_INFINITY; | ||
|
|
||
| coordinates.forEach(([lat, lng]) => { | ||
| minLat = Math.min(minLat, lat); | ||
| maxLat = Math.max(maxLat, lat); | ||
| minLng = Math.min(minLng, lng); | ||
| maxLng = Math.max(maxLng, lng); | ||
| }); | ||
|
|
||
| return [minLng, minLat, maxLng, maxLat]; | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,241 @@ | ||||||
| /* eslint-disable no-useless-escape */ | ||||||
| /** Rule disabled due to data being stored with " that need to be escaped */ | ||||||
|
|
||||||
| import { | ||||||
| type ExpressionSpecification, | ||||||
| type LayerSpecification, | ||||||
| } from 'maplibre-gl'; | ||||||
| import { generateStopColorExpression } from './GtfsVisualizationMap.functions'; | ||||||
| import { type Theme } from '@mui/material'; | ||||||
|
|
||||||
| // layer helpers | ||||||
|
|
||||||
| export const routeTypeFilter = ( | ||||||
| filteredRouteTypeIds: string[], | ||||||
| ): ExpressionSpecification | boolean => | ||||||
| filteredRouteTypeIds.length > 0 | ||||||
| ? ['in', ['get', 'route_type'], ['literal', filteredRouteTypeIds]] | ||||||
| : true; // if no filter applied, show all | ||||||
|
|
||||||
| // Base filter for visible stops (main "stops" layer) | ||||||
| export const stopsBaseFilter = ( | ||||||
| hideStops: boolean, | ||||||
| allSelectedRouteIds: string[], | ||||||
| ): ExpressionSpecification | boolean => { | ||||||
| // Base filter for visible stops (main "stops" layer) | ||||||
| return hideStops | ||||||
| ? false | ||||||
| : allSelectedRouteIds.length === 0 | ||||||
| ? true // no filters → show all | ||||||
| : [ | ||||||
| 'any', | ||||||
| ...allSelectedRouteIds.map( | ||||||
| (id) => | ||||||
| [ | ||||||
| 'in', | ||||||
| `\"${id}\"`, | ||||||
| ['get', 'route_ids'], | ||||||
| ] as ExpressionSpecification, // route_ids stored as quoted-string list | ||||||
| ), | ||||||
| ]; | ||||||
| }; | ||||||
|
|
||||||
| // layers | ||||||
| export const RoutesWhiteLayer = ( | ||||||
| filteredRouteTypeIds: string[], | ||||||
| theme: Theme, | ||||||
| ): LayerSpecification => { | ||||||
| return { | ||||||
| id: 'routes-white', | ||||||
| source: 'routes', | ||||||
| filter: routeTypeFilter(filteredRouteTypeIds), | ||||||
| 'source-layer': 'routesoutput', | ||||||
| type: 'line', | ||||||
| paint: { | ||||||
| 'line-color': theme.palette.background.default, | ||||||
| 'line-width': ['match', ['get', 'route_type'], '3', 10, '1', 15, 3], | ||||||
|
||||||
| 'line-width': ['match', ['get', 'route_type'], '3', 10, '1', 15, 3], | |
| 'line-width': ['match', ['get', 'route_type'], '3', 4, '1', 15, 3], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React hooks like
useTheme()cannot be called inside regular functions. The theme should be passed as a parameter toRoutesWhiteLayerinstead of being called within the function body.