Skip to content

Commit 7abb594

Browse files
Feat: gtfs visualization (#1270)
1 parent ef2d1f4 commit 7abb594

File tree

26 files changed

+10140
-110
lines changed

26 files changed

+10140
-110
lines changed

functions-python/tasks_executor/src/tasks/pmtiles_builder/build_pmtiles.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,21 @@ def _upload_files_to_gcs(self, file_to_upload):
256256
self.bucket_name,
257257
blob_path,
258258
)
259+
260+
try:
261+
blob.make_public()
262+
self.logger.debug(
263+
"Made object public: https://storage.googleapis.com/%s/%s",
264+
self.bucket_name,
265+
blob_path,
266+
)
267+
except Exception as e:
268+
# Likely due to Uniform bucket-level access; log and continue
269+
self.logger.warning(
270+
"Could not make %s public (uniform bucket-level access enabled?): %s",
271+
blob_path,
272+
e,
273+
)
259274
except Exception as e:
260275
raise Exception(f"Failed to upload files to GCS: {e}") from e
261276

web-app/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@turf/center": "^6.5.0",
1414
"@types/i18next": "^13.0.0",
1515
"@types/leaflet": "^1.9.12",
16+
"@types/react-map-gl": "^6.1.7",
1617
"axios": "^1.7.2",
1718
"countries-list": "^3.1.1",
1819
"date-fns": "^2.30.0",
@@ -24,10 +25,12 @@
2425
"i18next-browser-languagedetector": "^8.0.0",
2526
"i18next-http-backend": "^2.5.2",
2627
"leaflet": "^1.9.4",
28+
"maplibre-gl": "^4.7.1",
2729
"material-react-table": "^2.13.0",
2830
"mui-datatables": "^4.3.0",
2931
"mui-nested-menu": "^3.4.0",
3032
"openapi-fetch": "^0.9.3",
33+
"pmtiles": "^4.2.1",
3134
"react": "^17.0.0 || ^18.0.0",
3235
"react-dom": "^17.0.0 || ^18.0.0",
3336
"react-ga4": "^2.1.0",
@@ -36,6 +39,7 @@
3639
"react-hook-form": "^7.52.1",
3740
"react-i18next": "^14.1.2",
3841
"react-leaflet": "^4.2.1",
42+
"react-map-gl": "^7.1.7",
3943
"react-redux": "^8.1.3",
4044
"react-router-dom": "^6.16.0",
4145
"react-scripts": "5.0.1",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
"coveredAreaTitle": "Covered Area",
6969
"boundingBoxView": "Bounding Box",
7070
"boundingBoxViewTooltip": "View the bounding box of the feed",
71+
"gtfsVisualizationView": "Routes and Stops",
72+
"gtfsVisualizationTooltip": "View the routes and stops of the feed",
7173
"detailedCoveredAreaView": "Detailed",
7274
"detailedCoveredAreaViewTooltip": "View the detailed covered area of the feed",
7375
"unableToGenerateBoundingBox": "Unable to generate bounding box from the feed's stops.txt file.",

web-app/src/app/Theme.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ declare module '@mui/material/styles' {
1212
interface PaletteOptions {
1313
boxShadow?: string;
1414
}
15+
interface Theme {
16+
map: {
17+
basemapTileUrl: string;
18+
routeColor: string;
19+
routeTextColor: string;
20+
};
21+
}
22+
23+
interface ThemeOptions {
24+
map?: {
25+
basemapTileUrl?: string;
26+
routeColor?: string;
27+
routeTextColor?: string;
28+
};
29+
}
1530
}
1631

1732
declare module '@mui/material/Typography' {
@@ -96,6 +111,13 @@ export const getTheme = (mode: ThemeModeEnum): Theme => {
96111
const chosenPalette = !isLightMode ? darkPalette : palette;
97112
return createTheme({
98113
palette: { ...chosenPalette, mode },
114+
map: {
115+
basemapTileUrl: isLightMode
116+
? 'https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
117+
: 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
118+
routeColor: chosenPalette.background.default,
119+
routeTextColor: chosenPalette.text.primary,
120+
},
99121
mixins: {
100122
code: {
101123
contrastText: '#f1fa8c',

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const ContentBox = (
4242
mb: 1,
4343
}}
4444
>
45-
<span>{props.title}</span>
45+
{props.title.trim() !== '' && <span>{props.title}</span>}
4646
{props.action != null && props.action}
4747
</Typography>
4848
)}

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

Lines changed: 126 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
Skeleton,
88
Button,
99
Typography,
10+
Fab,
1011
} from '@mui/material';
12+
import { Link } from 'react-router-dom';
1113
import MapIcon from '@mui/icons-material/Map';
1214
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
1315
import { ContentBox } from './ContentBox';
@@ -28,6 +30,10 @@ import { computeBoundingBox } from '../screens/Feed/Feed.functions';
2830
import { displayFormattedDate } from '../utils/date';
2931
import { useSelector } from 'react-redux';
3032
import { selectAutodiscoveryGbfsVersion } from '../store/feed-selectors';
33+
import ModeOfTravelIcon from '@mui/icons-material/ModeOfTravel';
34+
import { GtfsVisualizationMap } from './GtfsVisualizationMap';
35+
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
36+
import { useRemoteConfig } from '../context/RemoteConfigProvider';
3137

3238
interface CoveredAreaMapProps {
3339
boundingBox?: LatLngExpression[];
@@ -50,22 +56,26 @@ export const fetchGeoJson = async (
5056
return await response.json();
5157
};
5258

59+
type MapViews =
60+
| 'boundingBoxView'
61+
| 'detailedCoveredAreaView'
62+
| 'gtfsVisualizationView';
63+
5364
const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
5465
boundingBox,
5566
latestDataset,
5667
feed,
5768
}) => {
5869
const { t } = useTranslation('feeds');
5970
const theme = useTheme();
71+
const { config } = useRemoteConfig();
6072

6173
const [geoJsonData, setGeoJsonData] = useState<
6274
GeoJSONData | GeoJSONDataGBFS | null
6375
>(null);
6476
const [geoJsonError, setGeoJsonError] = useState(false);
6577
const [geoJsonLoading, setGeoJsonLoading] = useState(false);
66-
const [view, setView] = useState<
67-
'boundingBoxView' | 'detailedCoveredAreaView'
68-
>('detailedCoveredAreaView');
78+
const [view, setView] = useState<MapViews>('detailedCoveredAreaView');
6979
const latestGbfsVersion = useSelector(selectAutodiscoveryGbfsVersion);
7080

7181
const getAndSetGeoJsonData = (urlToExtract: string): void => {
@@ -112,7 +122,7 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
112122

113123
const handleViewChange = (
114124
_: React.MouseEvent<HTMLElement>,
115-
newView: 'boundingBoxView' | 'detailedCoveredAreaView' | null,
125+
newView: MapViews | null,
116126
): void => {
117127
if (newView !== null) setView(newView);
118128
};
@@ -132,29 +142,51 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
132142
const renderMap = (): JSX.Element => {
133143
const displayBoundingBoxMap =
134144
view === 'boundingBoxView' && feed?.data_type === 'gtfs';
145+
146+
const displayGtfsVisualizationView =
147+
view === 'gtfsVisualizationView' &&
148+
feed?.data_type === 'gtfs' &&
149+
config.enableGtfsVisualizationMap;
135150
let gbfsBoundingBox: LatLngExpression[] = [];
136151
if (feed?.data_type === 'gbfs') {
137152
gbfsBoundingBox = computeBoundingBox(geoJsonData) ?? [];
138153
if (gbfsBoundingBox.length === 0) {
139154
setGeoJsonError(true);
140155
}
141156
}
142-
return (
143-
<>
144-
{displayBoundingBoxMap ? (
145-
<Map polygon={boundingBox ?? []} />
146-
) : (
147-
<MapGeoJSON
148-
geoJSONData={geoJsonData}
149-
polygon={boundingBox ?? gbfsBoundingBox}
150-
displayMapDetails={feed?.data_type === 'gtfs'}
157+
158+
if (displayBoundingBoxMap) {
159+
return <Map polygon={boundingBox ?? []} />;
160+
}
161+
162+
if (displayGtfsVisualizationView) {
163+
return (
164+
<>
165+
<Fab
166+
size='small'
167+
sx={{ position: 'absolute', top: 16, right: 16 }}
168+
component={Link}
169+
to='./map'
170+
>
171+
<ZoomOutMapIcon></ZoomOutMapIcon>
172+
</Fab>
173+
<GtfsVisualizationMap
174+
polygon={boundingBox ?? []}
175+
latestDataset={latestDataset}
151176
/>
152-
)}
153-
</>
177+
</>
178+
);
179+
}
180+
181+
return (
182+
<MapGeoJSON
183+
geoJSONData={geoJsonData}
184+
polygon={boundingBox ?? gbfsBoundingBox}
185+
displayMapDetails={feed?.data_type === 'gtfs'}
186+
/>
154187
);
155188
};
156189

157-
const mapDisplayError = boundingBox == undefined && geoJsonError;
158190
const latestAutodiscoveryUrl = getGbfsLatestVersionVisualizationUrl(
159191
feed as GBFSFeedType,
160192
);
@@ -166,73 +198,95 @@ const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
166198
flexDirection: 'column',
167199
maxHeight: {
168200
xs: '100%',
169-
md: '70vh',
201+
md: '70vh', // TODO: optimize this
170202
},
171203
minHeight: '50vh',
172204
}}
173-
title={mapDisplayError ? '' : t('coveredAreaTitle') + ' - ' + t(view)}
205+
title={t('coveredAreaTitle') + ' - ' + t(view)}
174206
width={{ xs: '100%' }}
175207
outlineColor={theme.palette.primary.dark}
176208
padding={2}
177-
action={
178-
<>
179-
{feed?.data_type === 'gbfs' ? (
180-
<Box sx={{ textAlign: 'right' }}>
181-
{latestAutodiscoveryUrl != undefined && (
182-
<Button
183-
href={latestAutodiscoveryUrl}
184-
target='_blank'
185-
rel='noreferrer'
186-
endIcon={<OpenInNew />}
187-
>
188-
{t('viewRealtimeVisualization')}
189-
</Button>
190-
)}
191-
{(geoJsonData as GeoJSONDataGBFS)?.extracted_at != undefined && (
192-
<Typography
193-
variant='caption'
194-
color='text.secondary'
195-
sx={{ display: 'block', px: 1 }}
196-
>
197-
{t('common:updated')}:{' '}
198-
{displayFormattedDate(
199-
(geoJsonData as GeoJSONDataGBFS).extracted_at,
200-
)}
201-
</Typography>
202-
)}
203-
</Box>
204-
) : (
205-
<ToggleButtonGroup
206-
value={view}
207-
color='primary'
208-
exclusive
209-
aria-label='map view selection'
210-
onChange={handleViewChange}
211-
>
212-
<Tooltip title={t('detailedCoveredAreaViewTooltip')}>
213-
<ToggleButton
214-
value='detailedCoveredAreaView'
215-
disabled={
216-
geoJsonLoading || geoJsonError || boundingBox === undefined
217-
}
218-
aria-label='Detailed Covered Area View'
219-
>
220-
<TravelExploreIcon />
221-
</ToggleButton>
222-
</Tooltip>
223-
<Tooltip title={t('boundingBoxViewTooltip')}>
209+
>
210+
<Box
211+
display={'flex'}
212+
justifyContent={
213+
view === 'gtfsVisualizationView' ? 'space-between' : 'flex-end'
214+
}
215+
mb={1}
216+
alignItems={'center'}
217+
>
218+
{view === 'gtfsVisualizationView' &&
219+
config.enableGtfsVisualizationMap && (
220+
<Button component={Link} to='./map'>
221+
Open Full Map with Filters
222+
</Button>
223+
)}
224+
{feed?.data_type === 'gbfs' ? (
225+
<Box sx={{ textAlign: 'right' }}>
226+
{latestAutodiscoveryUrl != undefined && (
227+
<Button
228+
href={latestAutodiscoveryUrl}
229+
target='_blank'
230+
rel='noreferrer'
231+
endIcon={<OpenInNew />}
232+
>
233+
{t('viewRealtimeVisualization')}
234+
</Button>
235+
)}
236+
{(geoJsonData as GeoJSONDataGBFS)?.extracted_at != undefined && (
237+
<Typography
238+
variant='caption'
239+
color='text.secondary'
240+
sx={{ display: 'block', px: 1 }}
241+
>
242+
{t('common:updated')}:{' '}
243+
{displayFormattedDate(
244+
(geoJsonData as GeoJSONDataGBFS).extracted_at,
245+
)}
246+
</Typography>
247+
)}
248+
</Box>
249+
) : (
250+
<ToggleButtonGroup
251+
value={view}
252+
color='primary'
253+
exclusive
254+
aria-label='map view selection'
255+
onChange={handleViewChange}
256+
>
257+
<Tooltip title={t('detailedCoveredAreaViewTooltip')}>
258+
<ToggleButton
259+
value='detailedCoveredAreaView'
260+
disabled={
261+
geoJsonLoading || geoJsonError || boundingBox === undefined
262+
}
263+
aria-label='Detailed Covered Area View'
264+
>
265+
<TravelExploreIcon />
266+
</ToggleButton>
267+
</Tooltip>
268+
<Tooltip title={t('boundingBoxViewTooltip')}>
269+
<ToggleButton
270+
value='boundingBoxView'
271+
aria-label='Bounding Box View'
272+
>
273+
<MapIcon />
274+
</ToggleButton>
275+
</Tooltip>
276+
{config.enableGtfsVisualizationMap && (
277+
<Tooltip title={t('gtfsVisualizationTooltip')}>
224278
<ToggleButton
225-
value='boundingBoxView'
279+
value='gtfsVisualizationView'
280+
disabled={!config.enableGtfsVisualizationMap}
226281
aria-label='Bounding Box View'
227282
>
228-
<MapIcon />
283+
<ModeOfTravelIcon />
229284
</ToggleButton>
230285
</Tooltip>
231-
</ToggleButtonGroup>
232-
)}
233-
</>
234-
}
235-
>
286+
)}
287+
</ToggleButtonGroup>
288+
)}
289+
</Box>
236290
{feed?.data_type === 'gtfs' &&
237291
boundingBox === undefined &&
238292
view === 'boundingBoxView' && (

0 commit comments

Comments
 (0)