Skip to content

Commit adc3b6c

Browse files
fix: new pmtile url generation using visualization_dataset_id (#1440)
* new pmtile url geenration * updated the build routes url function * lint fix
1 parent 0d808c7 commit adc3b6c

File tree

6 files changed

+106
-58
lines changed

6 files changed

+106
-58
lines changed

web-app/src/app/components/CoveredAreaMap.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { Map } from './Map';
2525
import { useTranslation } from 'react-i18next';
2626
import type { LatLngExpression } from 'leaflet';
2727
import { useTheme } from '@mui/material/styles';
28-
import { type AllFeedType } from '../services/feeds/utils';
28+
import { type GTFSFeedType, type AllFeedType } from '../services/feeds/utils';
2929
import { OpenInNew } from '@mui/icons-material';
3030
import { computeBoundingBox } from '../screens/Feed/Feed.functions';
3131
import { displayFormattedDate } from '../utils/date';
@@ -37,10 +37,11 @@ import { useRemoteConfig } from '../context/RemoteConfigProvider';
3737
import ReactGA from 'react-ga4';
3838
import { selectLatestGbfsVersion } from '../store/feed-selectors';
3939
import { selectGtfsDatasetRoutesLoadingStatus } from '../store/supporting-files-selectors';
40+
import { type LatestDatasetLite } from './GtfsVisualizationMap.functions';
4041

4142
interface CoveredAreaMapProps {
4243
boundingBox?: LatLngExpression[];
43-
latestDataset?: { hosted_url?: string };
44+
latestDataset?: LatestDatasetLite;
4445
feed: AllFeedType;
4546
}
4647

@@ -201,7 +202,12 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
201202
return <Map polygon={boundingBox} />;
202203
}
203204

204-
if (displayGtfsVisualizationView && boundingBox != undefined) {
205+
if (
206+
displayGtfsVisualizationView &&
207+
boundingBox != undefined &&
208+
feed.data_type === 'gtfs'
209+
) {
210+
const gtfsFeed = feed as GTFSFeedType;
205211
return (
206212
<>
207213
<Fab
@@ -215,6 +221,9 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
215221
<GtfsVisualizationMap
216222
polygon={boundingBox}
217223
latestDataset={latestDataset}
224+
visualizationId={
225+
gtfsFeed?.visualization_dataset_id ?? latestDataset?.id ?? ''
226+
}
218227
dataDisplayLimit={config.visualizationMapPreviewDataLimit}
219228
preview={true}
220229
filteredRoutes={[]} // this is necessary to re-renders

web-app/src/app/components/GtfsVisualizationMap.functions.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import {
44
type LngLatBoundsLike,
55
} from 'maplibre-gl';
66

7+
export interface LatestDatasetLite {
8+
hosted_url?: string;
9+
id?: string;
10+
stable_id?: string;
11+
}
12+
713
// Extract route_ids list from the PMTiles property (stringified JSON)
814
export function extractRouteIds(val: RouteIdsInput): string[] {
915
if (Array.isArray(val)) return val.map(String);
@@ -72,3 +78,22 @@ export const getBoundsFromCoordinates = (
7278

7379
return [minLng, minLat, maxLng, maxLat];
7480
};
81+
82+
export const generatePmtilesUrls = (
83+
latestDataset: LatestDatasetLite | undefined,
84+
visualizationId: string,
85+
): {
86+
stopsPmtilesUrl: string;
87+
routesPmtilesUrl: string;
88+
} => {
89+
const baseUrl =
90+
latestDataset?.hosted_url != null
91+
? latestDataset.hosted_url.replace(/[^/]+$/, '')
92+
: undefined;
93+
const updatedUrl = baseUrl
94+
?.replace(/[^/]+$/, '')
95+
.replace(/\/[^/]+\/?$/, `/${visualizationId}/`);
96+
const stopsPmtilesUrl = `${updatedUrl ?? ''}pmtiles/stops.pmtiles`;
97+
const routesPmtilesUrl = `${updatedUrl ?? ''}pmtiles/routes.pmtiles`;
98+
return { stopsPmtilesUrl, routesPmtilesUrl };
99+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { generatePmtilesUrls } from './GtfsVisualizationMap.functions';
2+
3+
describe('generatePmtilesUrls', () => {
4+
const latestDataset = {
5+
hosted_url:
6+
'https://files.mobilitydatabase.org/mdb-437/mdb-437-202511031503/mdb-437-202511031503.zip',
7+
};
8+
9+
const latestDatasetJbda = {
10+
hosted_url:
11+
'https://files.mobilitydatabase.org/jbda-4371/jbda-4371-202511050124/jbda-4371-202511031503.zip',
12+
};
13+
14+
it('should generate correct PMTiles URLs when given valid dataset and visualizationId', () => {
15+
const result = generatePmtilesUrls(latestDataset, 'mdb-120-202511060901');
16+
17+
expect(result).toEqual({
18+
stopsPmtilesUrl:
19+
'https://files.mobilitydatabase.org/mdb-437/mdb-120-202511060901/pmtiles/stops.pmtiles',
20+
routesPmtilesUrl:
21+
'https://files.mobilitydatabase.org/mdb-437/mdb-120-202511060901/pmtiles/routes.pmtiles',
22+
});
23+
});
24+
25+
it('should return URLs with different system ids', () => {
26+
const result = generatePmtilesUrls(
27+
latestDatasetJbda,
28+
'jbda-120-202511060901',
29+
);
30+
expect(result.stopsPmtilesUrl).toContain(
31+
'https://files.mobilitydatabase.org/jbda-4371/jbda-120-202511060901/pmtiles/stops.pmtiles',
32+
);
33+
});
34+
35+
it('should handle undefined dataset gracefully', () => {
36+
const result = generatePmtilesUrls(undefined, 'jbda-120-202511060901');
37+
38+
expect(result).toEqual({
39+
stopsPmtilesUrl: 'pmtiles/stops.pmtiles',
40+
routesPmtilesUrl: 'pmtiles/routes.pmtiles',
41+
});
42+
});
43+
44+
it('should handle empty visualizationId', () => {
45+
const result = generatePmtilesUrls(latestDataset, '');
46+
47+
expect(result.stopsPmtilesUrl).toContain('pmtiles/stops.pmtiles');
48+
expect(result.routesPmtilesUrl).toContain('pmtiles/routes.pmtiles');
49+
});
50+
});

web-app/src/app/components/GtfsVisualizationMap.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import { SelectedRoutesStopsPanel } from './Map/SelectedRoutesStopsPanel';
2424
import { ScanningOverlay } from './Map/ScanningOverlay';
2525
import {
2626
extractRouteIds,
27+
generatePmtilesUrls,
2728
getBoundsFromCoordinates,
29+
type LatestDatasetLite,
2830
} from './GtfsVisualizationMap.functions';
2931
import {
3032
RouteHighlightLayer,
@@ -36,12 +38,6 @@ import {
3638
StopsIndexLayer,
3739
} from './GtfsVisualizationMap.layers';
3840

39-
interface LatestDatasetLite {
40-
hosted_url?: string;
41-
id?: string;
42-
stable_id?: string;
43-
}
44-
4541
export interface GtfsVisualizationMapProps {
4642
polygon: LatLngExpression[];
4743
latestDataset?: LatestDatasetLite;
@@ -54,11 +50,13 @@ export interface GtfsVisualizationMapProps {
5450
stopRadius?: number;
5551
/** If true, skip precomputation + scanning overlay + auto-fit-on-idle */
5652
preview?: boolean;
53+
visualizationId: string;
5754
}
5855

5956
export const GtfsVisualizationMap = ({
6057
polygon,
6158
latestDataset,
59+
visualizationId,
6260
filteredRoutes = [],
6361
filteredRouteTypeIds = [],
6462
hideStops = false,
@@ -94,14 +92,8 @@ export const GtfsVisualizationMap = ({
9492
// Selected stop id from the panel (for cute highlight)
9593
const [selectedStopId, setSelectedStopId] = useState<string | null>(null);
9694
const { stopsPmtilesUrl, routesPmtilesUrl } = useMemo(() => {
97-
const baseUrl =
98-
latestDataset?.hosted_url != null
99-
? latestDataset.hosted_url.replace(/[^/]+$/, '')
100-
: undefined;
101-
const stops = `${baseUrl}/pmtiles/stops.pmtiles`;
102-
const routes = `${baseUrl}/pmtiles/routes.pmtiles`;
103-
return { stopsPmtilesUrl: stops, routesPmtilesUrl: routes };
104-
}, [latestDataset?.id, latestDataset?.stable_id]);
95+
return generatePmtilesUrls(latestDataset, visualizationId);
96+
}, [latestDataset?.id, latestDataset?.stable_id, visualizationId]);
10597

10698
const mapRef = useRef<MapRef>(null);
10799
const didInitRef = useRef(false);

web-app/src/app/screens/Feed/components/FullMapView.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,11 @@ export default function FullMapView(): React.ReactElement {
654654
<GtfsVisualizationMap
655655
polygon={boundingBox}
656656
latestDataset={latestDatasetLite}
657+
visualizationId={
658+
(feed as GTFSFeedType)?.visualization_dataset_id ??
659+
latestDatasetLite?.id ??
660+
''
661+
}
657662
filteredRouteTypeIds={filteredRouteTypeIds}
658663
filteredRoutes={filteredRoutes}
659664
hideStops={hideStops}

web-app/src/app/store/saga/supporting-files-saga.ts

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,19 @@ import {
88
} from '../supporting-files-reducer';
99
import { getAppError } from '../../utils/error';
1010
import { getJson } from '../../services/http';
11-
import { loadingDatasetSuccess } from '../dataset-reducer';
12-
import { selectLatestDatasetsData } from '../dataset-selectors';
1311
import {
1412
updateFeedId,
1513
loadingFeedSuccess,
1614
loadingFeedFail,
1715
} from '../feed-reducer';
18-
import { selectFeedData, selectLatestGtfsDatasetId } from '../feed-selectors';
16+
import { selectFeedData } from '../feed-selectors';
1917
import {
2018
type GtfsRoute,
2119
type GeoJSONData,
2220
type GeoJSONDataGBFS,
2321
} from '../../types';
2422
import { getFeedFilesBaseUrl } from '../../utils/config';
23+
import { type GTFSFeedType } from '../../services/feeds/utils';
2524

2625
export function buildRoutesUrl(feedId: string, datasetId: string): string {
2726
return `${getFeedFilesBaseUrl()}/${feedId}/${datasetId}/pmtiles/routes.json`;
@@ -81,52 +80,20 @@ const handleFeedChange = function* (): Generator<unknown, void, unknown> {
8180
yield put(clearSupportingFiles());
8281

8382
if (feed?.data_type === 'gtfs') {
84-
const datasetId = (yield select(selectLatestGtfsDatasetId)) as
85-
| string
86-
| undefined;
87-
if (datasetId !== undefined && feedId !== undefined) {
88-
const url = buildRoutesUrl(feedId, datasetId);
83+
if (feedId !== undefined) {
84+
const gtfsFeed: GTFSFeedType = feed as GTFSFeedType;
85+
const url = buildRoutesUrl(
86+
feedId,
87+
gtfsFeed?.visualization_dataset_id ?? '',
88+
);
8989
yield put(loadingSupportingFile({ key: 'gtfsDatasetRoutesJson', url }));
9090
}
9191
}
9292
};
9393

94-
const handleDatasetChange = function* (): Generator<unknown, void, unknown> {
95-
const dataset = (yield select(selectLatestDatasetsData)) as
96-
| { id?: string; feed_id?: string }
97-
| undefined;
98-
const feedId = dataset?.feed_id;
99-
const datasetId = dataset?.id;
100-
101-
// Read previous feedId from supporting-files context so we only clear/reload
102-
// when the feed actually changed.
103-
const previousContext = (yield select((s) => s.supportingFiles)) as
104-
| { context?: { datasetId?: string } }
105-
| undefined;
106-
const previousDatasetId = previousContext?.context?.datasetId;
107-
108-
// If datasetId hasn't changed or it's, do nothing.
109-
if (previousDatasetId === datasetId) {
110-
return;
111-
}
112-
113-
// Set the context in the supporting-files state so we can track which
114-
// feed the files belong to.
115-
yield put(setSupportingFilesContext({ feedId, dataType: 'gtfs' }));
116-
117-
// Clear any supporting files loaded for a previous feed.
118-
yield put(clearSupportingFiles());
119-
120-
if (datasetId !== undefined && feedId !== undefined) {
121-
const url = buildRoutesUrl(feedId, datasetId);
122-
yield put(loadingSupportingFile({ key: 'gtfsDatasetRoutesJson', url }));
123-
}
124-
};
125-
12694
export function* watchSupportingFiles(): Generator {
12795
yield takeLatest(updateFeedId.type, handleFeedChange);
12896
yield takeLatest(loadingFeedSuccess.type, handleFeedChange);
12997
yield takeLatest(loadingFeedFail.type, handleFeedChangeFail);
130-
yield takeLatest(loadingDatasetSuccess.type, handleDatasetChange);
13198
yield takeLatest(loadingSupportingFile.type, loadSupportingFileSaga);
13299
}

0 commit comments

Comments
 (0)