Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions web-app/src/app/components/CoveredAreaMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}

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

if (displayGtfsVisualizationView && boundingBox != undefined) {
if (
displayGtfsVisualizationView &&
boundingBox != undefined &&
feed.data_type === 'gtfs'
) {
const gtfsFeed = feed as GTFSFeedType;
return (
<>
<Fab
Expand All @@ -215,6 +221,9 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
<GtfsVisualizationMap
polygon={boundingBox}
latestDataset={latestDataset}
visualizationId={
gtfsFeed?.visualization_dataset_id ?? latestDataset?.id ?? ''
}
dataDisplayLimit={config.visualizationMapPreviewDataLimit}
preview={true}
filteredRoutes={[]} // this is necessary to re-renders
Expand Down
25 changes: 25 additions & 0 deletions web-app/src/app/components/GtfsVisualizationMap.functions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import {
type LngLatBoundsLike,
} from 'maplibre-gl';

export interface LatestDatasetLite {
hosted_url?: string;
id?: string;
stable_id?: string;
}

// Extract route_ids list from the PMTiles property (stringified JSON)
export function extractRouteIds(val: RouteIdsInput): string[] {
if (Array.isArray(val)) return val.map(String);
Expand Down Expand Up @@ -72,3 +78,22 @@ export const getBoundsFromCoordinates = (

return [minLng, minLat, maxLng, maxLat];
};

export const generatePmtilesUrls = (
latestDataset: LatestDatasetLite | undefined,
visualizationId: string,
): {
stopsPmtilesUrl: string;
routesPmtilesUrl: string;
} => {
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 };
};
50 changes: 50 additions & 0 deletions web-app/src/app/components/GtfsVisualizationMap.spec.tsx
Original file line number Diff line number Diff line change
@@ -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');
});
});
20 changes: 6 additions & 14 deletions web-app/src/app/components/GtfsVisualizationMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -94,14 +92,8 @@ export const GtfsVisualizationMap = ({
// Selected stop id from the panel (for cute highlight)
const [selectedStopId, setSelectedStopId] = useState<string | null>(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<MapRef>(null);
const didInitRef = useRef(false);
Expand Down
5 changes: 5 additions & 0 deletions web-app/src/app/screens/Feed/components/FullMapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,11 @@ export default function FullMapView(): React.ReactElement {
<GtfsVisualizationMap
polygon={boundingBox}
latestDataset={latestDatasetLite}
visualizationId={
(feed as GTFSFeedType)?.visualization_dataset_id ??
latestDatasetLite?.id ??
''
}
filteredRouteTypeIds={filteredRouteTypeIds}
filteredRoutes={filteredRoutes}
hideStops={hideStops}
Expand Down
49 changes: 8 additions & 41 deletions web-app/src/app/store/saga/supporting-files-saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@ import {
} from '../supporting-files-reducer';
import { getAppError } from '../../utils/error';
import { getJson } from '../../services/http';
import { loadingDatasetSuccess } from '../dataset-reducer';
import { selectLatestDatasetsData } from '../dataset-selectors';
import {
updateFeedId,
loadingFeedSuccess,
loadingFeedFail,
} from '../feed-reducer';
import { selectFeedData, selectLatestGtfsDatasetId } from '../feed-selectors';
import { selectFeedData } from '../feed-selectors';
import {
type GtfsRoute,
type GeoJSONData,
type GeoJSONDataGBFS,
} from '../../types';
import { getFeedFilesBaseUrl } from '../../utils/config';
import { type GTFSFeedType } from '../../services/feeds/utils';

export function buildRoutesUrl(feedId: string, datasetId: string): string {
return `${getFeedFilesBaseUrl()}/${feedId}/${datasetId}/pmtiles/routes.json`;
Expand Down Expand Up @@ -81,52 +80,20 @@ const handleFeedChange = function* (): Generator<unknown, void, unknown> {
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<unknown, void, unknown> {
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);
}
Loading