Skip to content

Commit c0c4875

Browse files
authored
feat: visualization map loading and error state (#1362)
1 parent a552071 commit c0c4875

File tree

12 files changed

+626
-331
lines changed

12 files changed

+626
-331
lines changed

web-app/public/locales/en/common.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@
5252
"gtfsSpec": {
5353
"routeType": {
5454
"0": {
55-
"name": "Tram, Streetcar, Light rail",
55+
"name": "Tram/Streetcar/Light rail",
5656
"description": "Any light rail or street level system within a metropolitan area."
5757
},
5858
"1": {
59-
"name": "Subway, Metro",
59+
"name": "Subway/Metro",
6060
"description": "Any underground rail system within a metropolitan area."
6161
},
6262
"2": {
@@ -76,7 +76,7 @@
7676
"description": "Used for street-level rail cars where the cable runs beneath the vehicle (e.g., cable car in San Francisco)."
7777
},
7878
"6": {
79-
"name": "Aerial lift, suspended cable car (e.g., gondola lift, aerial tramway)",
79+
"name": "Aerial lift/Suspended cable car (e.g., gondola lift, aerial tramway)",
8080
"description": "Cable transport where cabins, cars, gondolas or open chairs are suspended by means of one or more cables."
8181
},
8282
"7": {

web-app/public/locales/en/feeds.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
"fullName": "Include the full name for transit provider, e.g “Toronto Transit Commission” instead of “TTC”",
1818
"checkSpelling": "Double check the spelling"
1919
},
20+
"routeIds": "Route IDs",
21+
"stopCode": "Stop Code",
22+
"viewStopInfo": "View Stop Info",
23+
"gtfsVisualizationViewLabel": "GTFS Visualization View",
2024
"errorAndContact": "Please check your internet connection and try again. If the problem persists <1>contact us</1> for for further assistance.",
2125
"errorLoadingFeed": "There was an error loading the feed.",
2226
"errorLoadingQualityReport": "Unable to generate data quality report.",
@@ -152,7 +156,7 @@
152156
},
153157
"viewRealtimeVisualization": "View real-time visualization",
154158
"versions": "Versions",
155-
"dataAattribution": "Transit data provided by",
159+
"dataAttribution": "Transit data provided by",
156160
"emptyLicenseUsage": "Can this feed be used commercially by trip planners and other third parties?",
157161
"common": {
158162
"form": {
@@ -161,5 +165,13 @@
161165
"notSure": "Unsure"
162166
}
163167
},
164-
"openDetailedMap": "Open Detailed Map"
168+
"openDetailedMap": "Open Detailed Map",
169+
"retry": "Retry",
170+
"visualizationMapErrors": {
171+
"errorDescription": "An error occurred while loading the map.",
172+
"noFeedMetadata": "Could not load feed metadata.",
173+
"noRoutesData": "Could not load routes data for the map.",
174+
"invalidDataType": "This feed is not a GTFS Schedule feed.",
175+
"noBoundingBox": "No bounding box available."
176+
}
165177
}

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

Lines changed: 71 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useMemo } from 'react';
22
import {
33
Box,
44
ToggleButtonGroup,
@@ -25,17 +25,18 @@ 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 GBFSFeedType, type AllFeedType } from '../services/feeds/utils';
28+
import { 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';
3232
import { useSelector } from 'react-redux';
33-
import { selectAutodiscoveryGbfsVersion } from '../store/feed-selectors';
3433
import ModeOfTravelIcon from '@mui/icons-material/ModeOfTravel';
3534
import { GtfsVisualizationMap } from './GtfsVisualizationMap';
3635
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
3736
import { useRemoteConfig } from '../context/RemoteConfigProvider';
3837
import ReactGA from 'react-ga4';
38+
import { selectLatestGbfsVersion } from '../store/feed-selectors';
39+
import { selectGtfsDatasetRoutesLoadingStatus } from '../store/supporting-files-selectors';
3940

4041
interface CoveredAreaMapProps {
4142
boundingBox?: LatLngExpression[];
@@ -77,20 +78,26 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
7778
>(null);
7879
const [geoJsonError, setGeoJsonError] = useState(false);
7980
const [geoJsonLoading, setGeoJsonLoading] = useState(false);
80-
const [view, setView] = useState<MapViews>('detailedCoveredAreaView');
81-
const latestGbfsVersion = useSelector(selectAutodiscoveryGbfsVersion);
81+
const [view, setView] = useState<MapViews>(
82+
feed?.data_type === 'gtfs'
83+
? 'gtfsVisualizationView'
84+
: 'detailedCoveredAreaView',
85+
);
86+
87+
const latestGbfsVersion = useSelector(selectLatestGbfsVersion);
88+
const routesJsonLoadingStatus = useSelector(
89+
selectGtfsDatasetRoutesLoadingStatus,
90+
);
8291

8392
const getAndSetGeoJsonData = (urlToExtract: string): void => {
8493
setGeoJsonLoading(true);
8594
fetchGeoJson(urlToExtract)
8695
.then((data) => {
8796
setGeoJsonData(data);
8897
setGeoJsonError(false);
89-
setView('detailedCoveredAreaView');
9098
})
9199
.catch(() => {
92100
setGeoJsonError(true);
93-
setView('boundingBoxView');
94101
})
95102
.finally(() => {
96103
setGeoJsonLoading(false);
@@ -119,9 +126,23 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
119126
}
120127
setGeoJsonData(null);
121128
setGeoJsonError(true);
122-
setView('boundingBoxView');
123129
}, [latestDataset, feed]);
124130

131+
// effect to determine which view to display
132+
useEffect(() => {
133+
if (feed == undefined) return;
134+
if (feed?.data_type === 'gbfs') return;
135+
if (routesJsonLoadingStatus != 'failed' && boundingBox != undefined) {
136+
setView('gtfsVisualizationView');
137+
return;
138+
}
139+
if (geoJsonData != null && boundingBox != undefined) {
140+
setView('detailedCoveredAreaView');
141+
return;
142+
}
143+
setView('boundingBoxView');
144+
}, [feed, routesJsonLoadingStatus, boundingBox, geoJsonData]);
145+
125146
const handleViewChange = (
126147
_: React.MouseEvent<HTMLElement>,
127148
newView: MapViews | null,
@@ -137,9 +158,7 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
137158
});
138159
};
139160

140-
const getGbfsLatestVersionVisualizationUrl = (
141-
feed: GBFSFeedType,
142-
): string | undefined => {
161+
const getGbfsLatestVersionVisualizationUrl = (): string | undefined => {
143162
const latestAutodiscoveryUrl = latestGbfsVersion?.endpoints?.find(
144163
(endpoint) => endpoint.name === 'gbfs',
145164
)?.url;
@@ -154,11 +173,13 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
154173
view === 'boundingBoxView' && feed?.data_type === 'gtfs';
155174

156175
const displayGtfsVisualizationView =
157-
view === 'gtfsVisualizationView' &&
158-
feed?.data_type === 'gtfs' &&
159-
config.enableGtfsVisualizationMap;
176+
view === 'gtfsVisualizationView' && feed?.data_type === 'gtfs';
177+
160178
let gbfsBoundingBox: LatLngExpression[] = [];
161179
if (feed?.data_type === 'gbfs') {
180+
if (geoJsonData == null) {
181+
return <></>;
182+
}
162183
gbfsBoundingBox = computeBoundingBox(geoJsonData) ?? [];
163184
if (gbfsBoundingBox.length === 0) {
164185
setGeoJsonError(true);
@@ -187,19 +208,33 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
187208
</>
188209
);
189210
}
211+
if (geoJsonData != null) {
212+
return (
213+
<MapGeoJSON
214+
geoJSONData={geoJsonData}
215+
polygon={boundingBox ?? gbfsBoundingBox}
216+
displayMapDetails={feed?.data_type === 'gtfs'}
217+
/>
218+
);
219+
}
220+
return <></>;
221+
};
190222

223+
const latestAutodiscoveryUrl = getGbfsLatestVersionVisualizationUrl();
224+
const enableGtfsVisualizationView = useMemo(() => {
191225
return (
192-
<MapGeoJSON
193-
geoJSONData={geoJsonData}
194-
polygon={boundingBox ?? gbfsBoundingBox}
195-
displayMapDetails={feed?.data_type === 'gtfs'}
196-
/>
226+
config.enableGtfsVisualizationMap &&
227+
feed?.data_type === 'gtfs' &&
228+
routesJsonLoadingStatus != 'failed' &&
229+
boundingBox != undefined
197230
);
198-
};
231+
}, [
232+
feed?.data_type,
233+
config.enableGtfsVisualizationMap,
234+
routesJsonLoadingStatus,
235+
boundingBox,
236+
]);
199237

200-
const latestAutodiscoveryUrl = getGbfsLatestVersionVisualizationUrl(
201-
feed as GBFSFeedType,
202-
);
203238
return (
204239
<ContentBox
205240
sx={{
@@ -271,6 +306,17 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
271306
aria-label='map view selection'
272307
onChange={handleViewChange}
273308
>
309+
{config.enableGtfsVisualizationMap && (
310+
<Tooltip title={t('gtfsVisualizationTooltip')}>
311+
<ToggleButton
312+
value='gtfsVisualizationView'
313+
disabled={!enableGtfsVisualizationView}
314+
aria-label={t('gtfsVisualizationViewLabel')}
315+
>
316+
<ModeOfTravelIcon />
317+
</ToggleButton>
318+
</Tooltip>
319+
)}
274320
<Tooltip title={t('detailedCoveredAreaViewTooltip')}>
275321
<ToggleButton
276322
value='detailedCoveredAreaView'
@@ -290,19 +336,6 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
290336
<MapIcon />
291337
</ToggleButton>
292338
</Tooltip>
293-
{config.enableGtfsVisualizationMap && (
294-
<Tooltip title={t('gtfsVisualizationTooltip')}>
295-
<span>
296-
<ToggleButton
297-
value='gtfsVisualizationView'
298-
disabled={!config.enableGtfsVisualizationMap}
299-
aria-label='Bounding Box View'
300-
>
301-
<ModeOfTravelIcon />
302-
</ToggleButton>
303-
</span>
304-
</Tooltip>
305-
)}
306339
</ToggleButtonGroup>
307340
)}
308341
</Box>
@@ -320,15 +353,15 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
320353

321354
{(boundingBox != undefined || !geoJsonError) && (
322355
<Box sx={mapBoxPositionStyle}>
323-
{geoJsonLoading ? (
356+
{geoJsonLoading || routesJsonLoadingStatus === 'loading' ? (
324357
<Skeleton
325358
variant='rectangular'
326359
width='100%'
327360
height='100%'
328361
animation='wave'
329362
/>
330363
) : (
331-
<>{geoJsonData !== null && <>{renderMap()}</>}</>
364+
<>{renderMap()}</>
332365
)}
333366
</Box>
334367
)}

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@ export const GtfsVisualizationMap = ({
4242
hideStops = false,
4343
}: GtfsVisualizationMapProps): JSX.Element => {
4444

45-
const { stopsPmtilesUrl, routesPmtilesUrl } = useMemo(() => {
46-
const baseUrl = latestDataset?.hosted_url ? latestDataset.hosted_url.replace(/[^/]+$/, '') : undefined;
47-
const stops = `${baseUrl}/pmtiles/stops.pmtiles`;
48-
const routes = `${baseUrl}/pmtiles/routes.pmtiles`;
49-
50-
return { stopsPmtilesUrl: stops, routesPmtilesUrl: routes };
51-
}, [latestDataset?.id, latestDataset?.stable_id]);
45+
const { stopsPmtilesUrl, routesPmtilesUrl } = useMemo(
46+
() =>
47+
{
48+
const baseUrl = latestDataset?.hosted_url ? latestDataset.hosted_url.replace(/[^/]+$/, '') : undefined;
49+
const stops = `${baseUrl}/pmtiles/stops.pmtiles`;
50+
const routes = `${baseUrl}/pmtiles/routes.pmtiles`;
51+
return { stopsPmtilesUrl: stops, routesPmtilesUrl: routes };
52+
},
53+
[latestDataset?.id, latestDataset?.stable_id]
54+
);
5255

5356
const theme = useTheme();
5457
const [hoverInfo, setHoverInfo] = useState<string[]>([]);
@@ -58,10 +61,7 @@ export const GtfsVisualizationMap = ({
5861
string,
5962
string
6063
> | null>(null);
61-
const [mapClickStopData, setMapClickStopData] = useState<Record<
62-
string,
63-
string
64-
> | null>(null);
64+
const [mapClickStopData, setMapClickStopData] = useState<Record<string, string> | null>(null);
6565
const mapRef = useRef<MapRef>(null);
6666

6767
// Create a map to store routeId to routeColor mapping

0 commit comments

Comments
 (0)