diff --git a/web-app/src/app/components/CoveredAreaMap.tsx b/web-app/src/app/components/CoveredAreaMap.tsx index 55a459e11..d3016b81e 100644 --- a/web-app/src/app/components/CoveredAreaMap.tsx +++ b/web-app/src/app/components/CoveredAreaMap.tsx @@ -25,7 +25,7 @@ import { Map } from './Map'; import { useTranslation } from 'react-i18next'; import type { LatLngExpression } from 'leaflet'; import { useTheme } from '@mui/material/styles'; -import { type AllFeedType } from '../services/feeds/utils'; +import { type GTFSFeedType, type AllFeedType } from '../services/feeds/utils'; import { OpenInNew } from '@mui/icons-material'; import { computeBoundingBox } from '../screens/Feed/Feed.functions'; import { displayFormattedDate } from '../utils/date'; @@ -37,10 +37,11 @@ import { useRemoteConfig } from '../context/RemoteConfigProvider'; import ReactGA from 'react-ga4'; import { selectLatestGbfsVersion } from '../store/feed-selectors'; import { selectGtfsDatasetRoutesLoadingStatus } from '../store/supporting-files-selectors'; +import { type LatestDatasetLite } from './GtfsVisualizationMap.functions'; interface CoveredAreaMapProps { boundingBox?: LatLngExpression[]; - latestDataset?: { hosted_url?: string }; + latestDataset?: LatestDatasetLite; feed: AllFeedType; } @@ -201,7 +202,12 @@ const CoveredAreaMap: React.FC = ({ return ; } - if (displayGtfsVisualizationView && boundingBox != undefined) { + if ( + displayGtfsVisualizationView && + boundingBox != undefined && + feed.data_type === 'gtfs' + ) { + const gtfsFeed = feed as GTFSFeedType; return ( <> = ({ { + const baseUrl = + latestDataset?.hosted_url != null + ? latestDataset.hosted_url.replace(/[^/]+$/, '') + : undefined; + const updatedUrl = baseUrl + ?.replace(/[^/]+$/, '') + .replace(/\/[^/]+\/?$/, `/${visualizationId}/`); + const stopsPmtilesUrl = `${updatedUrl ?? ''}pmtiles/stops.pmtiles`; + const routesPmtilesUrl = `${updatedUrl ?? ''}pmtiles/routes.pmtiles`; + return { stopsPmtilesUrl, routesPmtilesUrl }; +}; diff --git a/web-app/src/app/components/GtfsVisualizationMap.spec.tsx b/web-app/src/app/components/GtfsVisualizationMap.spec.tsx new file mode 100644 index 000000000..dce2a2662 --- /dev/null +++ b/web-app/src/app/components/GtfsVisualizationMap.spec.tsx @@ -0,0 +1,50 @@ +import { generatePmtilesUrls } from './GtfsVisualizationMap.functions'; + +describe('generatePmtilesUrls', () => { + const latestDataset = { + hosted_url: + 'https://files.mobilitydatabase.org/mdb-437/mdb-437-202511031503/mdb-437-202511031503.zip', + }; + + const latestDatasetJbda = { + hosted_url: + 'https://files.mobilitydatabase.org/jbda-4371/jbda-4371-202511050124/jbda-4371-202511031503.zip', + }; + + it('should generate correct PMTiles URLs when given valid dataset and visualizationId', () => { + const result = generatePmtilesUrls(latestDataset, 'mdb-120-202511060901'); + + expect(result).toEqual({ + stopsPmtilesUrl: + 'https://files.mobilitydatabase.org/mdb-437/mdb-120-202511060901/pmtiles/stops.pmtiles', + routesPmtilesUrl: + 'https://files.mobilitydatabase.org/mdb-437/mdb-120-202511060901/pmtiles/routes.pmtiles', + }); + }); + + it('should return URLs with different system ids', () => { + const result = generatePmtilesUrls( + latestDatasetJbda, + 'jbda-120-202511060901', + ); + expect(result.stopsPmtilesUrl).toContain( + 'https://files.mobilitydatabase.org/jbda-4371/jbda-120-202511060901/pmtiles/stops.pmtiles', + ); + }); + + it('should handle undefined dataset gracefully', () => { + const result = generatePmtilesUrls(undefined, 'jbda-120-202511060901'); + + expect(result).toEqual({ + stopsPmtilesUrl: 'pmtiles/stops.pmtiles', + routesPmtilesUrl: 'pmtiles/routes.pmtiles', + }); + }); + + it('should handle empty visualizationId', () => { + const result = generatePmtilesUrls(latestDataset, ''); + + expect(result.stopsPmtilesUrl).toContain('pmtiles/stops.pmtiles'); + expect(result.routesPmtilesUrl).toContain('pmtiles/routes.pmtiles'); + }); +}); diff --git a/web-app/src/app/components/GtfsVisualizationMap.tsx b/web-app/src/app/components/GtfsVisualizationMap.tsx index cc0ff24b3..00ecbdd4e 100644 --- a/web-app/src/app/components/GtfsVisualizationMap.tsx +++ b/web-app/src/app/components/GtfsVisualizationMap.tsx @@ -24,7 +24,9 @@ import { SelectedRoutesStopsPanel } from './Map/SelectedRoutesStopsPanel'; import { ScanningOverlay } from './Map/ScanningOverlay'; import { extractRouteIds, + generatePmtilesUrls, getBoundsFromCoordinates, + type LatestDatasetLite, } from './GtfsVisualizationMap.functions'; import { RouteHighlightLayer, @@ -36,12 +38,6 @@ import { StopsIndexLayer, } from './GtfsVisualizationMap.layers'; -interface LatestDatasetLite { - hosted_url?: string; - id?: string; - stable_id?: string; -} - export interface GtfsVisualizationMapProps { polygon: LatLngExpression[]; latestDataset?: LatestDatasetLite; @@ -54,11 +50,13 @@ export interface GtfsVisualizationMapProps { stopRadius?: number; /** If true, skip precomputation + scanning overlay + auto-fit-on-idle */ preview?: boolean; + visualizationId: string; } export const GtfsVisualizationMap = ({ polygon, latestDataset, + visualizationId, filteredRoutes = [], filteredRouteTypeIds = [], hideStops = false, @@ -94,14 +92,8 @@ export const GtfsVisualizationMap = ({ // Selected stop id from the panel (for cute highlight) const [selectedStopId, setSelectedStopId] = useState(null); const { stopsPmtilesUrl, routesPmtilesUrl } = useMemo(() => { - const baseUrl = - latestDataset?.hosted_url != null - ? latestDataset.hosted_url.replace(/[^/]+$/, '') - : undefined; - const stops = `${baseUrl}/pmtiles/stops.pmtiles`; - const routes = `${baseUrl}/pmtiles/routes.pmtiles`; - return { stopsPmtilesUrl: stops, routesPmtilesUrl: routes }; - }, [latestDataset?.id, latestDataset?.stable_id]); + return generatePmtilesUrls(latestDataset, visualizationId); + }, [latestDataset?.id, latestDataset?.stable_id, visualizationId]); const mapRef = useRef(null); const didInitRef = useRef(false); diff --git a/web-app/src/app/screens/Feed/components/FullMapView.tsx b/web-app/src/app/screens/Feed/components/FullMapView.tsx index 692f907b7..b9958ad98 100644 --- a/web-app/src/app/screens/Feed/components/FullMapView.tsx +++ b/web-app/src/app/screens/Feed/components/FullMapView.tsx @@ -654,6 +654,11 @@ export default function FullMapView(): React.ReactElement { { yield put(clearSupportingFiles()); if (feed?.data_type === 'gtfs') { - const datasetId = (yield select(selectLatestGtfsDatasetId)) as - | string - | undefined; - if (datasetId !== undefined && feedId !== undefined) { - const url = buildRoutesUrl(feedId, datasetId); + if (feedId !== undefined) { + const gtfsFeed: GTFSFeedType = feed as GTFSFeedType; + const url = buildRoutesUrl( + feedId, + gtfsFeed?.visualization_dataset_id ?? '', + ); yield put(loadingSupportingFile({ key: 'gtfsDatasetRoutesJson', url })); } } }; -const handleDatasetChange = function* (): Generator { - const dataset = (yield select(selectLatestDatasetsData)) as - | { id?: string; feed_id?: string } - | undefined; - const feedId = dataset?.feed_id; - const datasetId = dataset?.id; - - // Read previous feedId from supporting-files context so we only clear/reload - // when the feed actually changed. - const previousContext = (yield select((s) => s.supportingFiles)) as - | { context?: { datasetId?: string } } - | undefined; - const previousDatasetId = previousContext?.context?.datasetId; - - // If datasetId hasn't changed or it's, do nothing. - if (previousDatasetId === datasetId) { - return; - } - - // Set the context in the supporting-files state so we can track which - // feed the files belong to. - yield put(setSupportingFilesContext({ feedId, dataType: 'gtfs' })); - - // Clear any supporting files loaded for a previous feed. - yield put(clearSupportingFiles()); - - if (datasetId !== undefined && feedId !== undefined) { - const url = buildRoutesUrl(feedId, datasetId); - yield put(loadingSupportingFile({ key: 'gtfsDatasetRoutesJson', url })); - } -}; - export function* watchSupportingFiles(): Generator { yield takeLatest(updateFeedId.type, handleFeedChange); yield takeLatest(loadingFeedSuccess.type, handleFeedChange); yield takeLatest(loadingFeedFail.type, handleFeedChangeFail); - yield takeLatest(loadingDatasetSuccess.type, handleDatasetChange); yield takeLatest(loadingSupportingFile.type, loadSupportingFileSaga); }