Skip to content

Feat: gtfs visualization #1270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions web-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@turf/center": "^6.5.0",
"@types/i18next": "^13.0.0",
"@types/leaflet": "^1.9.12",
"@types/react-map-gl": "^6.1.7",
"axios": "^1.7.2",
"countries-list": "^3.1.1",
"date-fns": "^2.30.0",
Expand All @@ -24,10 +25,12 @@
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"leaflet": "^1.9.4",
"maplibre-gl": "^4.7.1",
"material-react-table": "^2.13.0",
"mui-datatables": "^4.3.0",
"mui-nested-menu": "^3.4.0",
"openapi-fetch": "^0.9.3",
"pmtiles": "^4.2.1",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-ga4": "^2.1.0",
Expand All @@ -36,6 +39,7 @@
"react-hook-form": "^7.52.1",
"react-i18next": "^14.1.2",
"react-leaflet": "^4.2.1",
"react-map-gl": "^7.1.7",
"react-redux": "^8.1.3",
"react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
Expand Down
2 changes: 1 addition & 1 deletion web-app/src/app/components/ContentBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const ContentBox = (
mb: 1,
}}
>
<span>{props.title}</span>
{props.title.trim() !== '' && <span>{props.title}</span>}
{props.action != null && props.action}
</Typography>
)}
Expand Down
196 changes: 120 additions & 76 deletions web-app/src/app/components/CoveredAreaMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
Skeleton,
Button,
Typography,
Fab,
} from '@mui/material';
import { Link } from 'react-router-dom';
import MapIcon from '@mui/icons-material/Map';
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
import { ContentBox } from './ContentBox';
Expand All @@ -28,6 +30,9 @@
import { displayFormattedDate } from '../utils/date';
import { useSelector } from 'react-redux';
import { selectAutodiscoveryGbfsVersion } from '../store/feed-selectors';
import ModeOfTravelIcon from '@mui/icons-material/ModeOfTravel';
import { GtfsVisualizationMap } from './GtfsVisualizationMap';
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';

interface CoveredAreaMapProps {
boundingBox?: LatLngExpression[];
Expand All @@ -50,6 +55,11 @@
return await response.json();
};

type MapViews =
| 'boundingBoxView'
| 'detailedCoveredAreaView'
| 'gtfsVisualizationView';

const CoveredAreaMap: React.FC<CoveredAreaMapProps> = ({
boundingBox,
latestDataset,
Expand All @@ -63,9 +73,7 @@
>(null);
const [geoJsonError, setGeoJsonError] = useState(false);
const [geoJsonLoading, setGeoJsonLoading] = useState(false);
const [view, setView] = useState<
'boundingBoxView' | 'detailedCoveredAreaView'
>('detailedCoveredAreaView');
const [view, setView] = useState<MapViews>('detailedCoveredAreaView');
const latestGbfsVersion = useSelector(selectAutodiscoveryGbfsVersion);

const getAndSetGeoJsonData = (urlToExtract: string): void => {
Expand All @@ -74,7 +82,7 @@
.then((data) => {
setGeoJsonData(data);
setGeoJsonError(false);
setView('detailedCoveredAreaView');
setView('gtfsVisualizationView');
})
.catch(() => {
setGeoJsonError(true);
Expand Down Expand Up @@ -112,7 +120,7 @@

const handleViewChange = (
_: React.MouseEvent<HTMLElement>,
newView: 'boundingBoxView' | 'detailedCoveredAreaView' | null,
newView: MapViews | null,
): void => {
if (newView !== null) setView(newView);
};
Expand All @@ -132,29 +140,47 @@
const renderMap = (): JSX.Element => {
const displayBoundingBoxMap =
view === 'boundingBoxView' && feed?.data_type === 'gtfs';

const displayGtfsVisualizationView =
view === 'gtfsVisualizationView' && feed?.data_type === 'gtfs';
let gbfsBoundingBox: LatLngExpression[] = [];
if (feed?.data_type === 'gbfs') {
gbfsBoundingBox = computeBoundingBox(geoJsonData) ?? [];
if (gbfsBoundingBox.length === 0) {
setGeoJsonError(true);
}
}

if (displayBoundingBoxMap) {
return <Map polygon={boundingBox ?? []} />;
}

if (displayGtfsVisualizationView) {
return (
<>
<Fab
size='small'
sx={{ position: 'absolute', top: 16, right: 16 }}
component={Link}
to='./map'
>
<ZoomOutMapIcon></ZoomOutMapIcon>
</Fab>
<GtfsVisualizationMap polygon={boundingBox ?? []} />
</>
);
}

return (
<>
{displayBoundingBoxMap ? (
<Map polygon={boundingBox ?? []} />
) : (
<MapGeoJSON
geoJSONData={geoJsonData}
polygon={boundingBox ?? gbfsBoundingBox}
displayMapDetails={feed?.data_type === 'gtfs'}
/>
)}
</>
<MapGeoJSON
geoJSONData={geoJsonData}
polygon={boundingBox ?? gbfsBoundingBox}
displayMapDetails={feed?.data_type === 'gtfs'}
/>
);
};

const mapDisplayError = boundingBox == undefined && geoJsonError;

Check failure on line 183 in web-app/src/app/components/CoveredAreaMap.tsx

View workflow job for this annotation

GitHub Actions / deploy-web-app / Test

'mapDisplayError' is assigned a value but never used
const latestAutodiscoveryUrl = getGbfsLatestVersionVisualizationUrl(
feed as GBFSFeedType,
);
Expand All @@ -166,73 +192,91 @@
flexDirection: 'column',
maxHeight: {
xs: '100%',
md: '70vh',
md: '70vh', // TODO: optimize this
},
minHeight: '50vh',
}}
title={mapDisplayError ? '' : t('coveredAreaTitle') + ' - ' + t(view)}
title={''}
width={{ xs: '100%' }}
outlineColor={theme.palette.primary.dark}
padding={2}
action={
<>
{feed?.data_type === 'gbfs' ? (
<Box sx={{ textAlign: 'right' }}>
{latestAutodiscoveryUrl != undefined && (
<Button
href={latestAutodiscoveryUrl}
target='_blank'
rel='noreferrer'
endIcon={<OpenInNew />}
>
{t('viewRealtimeVisualization')}
</Button>
)}
{(geoJsonData as GeoJSONDataGBFS)?.extracted_at != undefined && (
<Typography
variant='caption'
color='text.secondary'
sx={{ display: 'block', px: 1 }}
>
{t('common:updated')}:{' '}
{displayFormattedDate(
(geoJsonData as GeoJSONDataGBFS).extracted_at,
)}
</Typography>
)}
</Box>
) : (
<ToggleButtonGroup
value={view}
color='primary'
exclusive
aria-label='map view selection'
onChange={handleViewChange}
>
<Tooltip title={t('detailedCoveredAreaViewTooltip')}>
<ToggleButton
value='detailedCoveredAreaView'
disabled={
geoJsonLoading || geoJsonError || boundingBox === undefined
}
aria-label='Detailed Covered Area View'
>
<TravelExploreIcon />
</ToggleButton>
</Tooltip>
<Tooltip title={t('boundingBoxViewTooltip')}>
<ToggleButton
value='boundingBoxView'
aria-label='Bounding Box View'
>
<MapIcon />
</ToggleButton>
</Tooltip>
</ToggleButtonGroup>
)}
</>
}
>
<Box
display={'flex'}
justifyContent={
view === 'gtfsVisualizationView' ? 'space-between' : 'flex-end'
}
mb={1}
alignItems={'center'}
>
{view === 'gtfsVisualizationView' && (
<Button component={Link} to='./map'>
Open Full Map with Filters
</Button>
)}
{feed?.data_type === 'gbfs' ? (
<Box sx={{ textAlign: 'right' }}>
{latestAutodiscoveryUrl != undefined && (
<Button
href={latestAutodiscoveryUrl}
target='_blank'
rel='noreferrer'
endIcon={<OpenInNew />}
>
{t('viewRealtimeVisualization')}
</Button>
)}
{(geoJsonData as GeoJSONDataGBFS)?.extracted_at != undefined && (
<Typography
variant='caption'
color='text.secondary'
sx={{ display: 'block', px: 1 }}
>
{t('common:updated')}:{' '}
{displayFormattedDate(
(geoJsonData as GeoJSONDataGBFS).extracted_at,
)}
</Typography>
)}
</Box>
) : (
<ToggleButtonGroup
value={view}
color='primary'
exclusive
aria-label='map view selection'
onChange={handleViewChange}
>
<Tooltip title={'gtfs visualization'}>
<ToggleButton
value='gtfsVisualizationView'
aria-label='Bounding Box View'
>
<ModeOfTravelIcon />
</ToggleButton>
</Tooltip>
<Tooltip title={t('detailedCoveredAreaViewTooltip')}>
<ToggleButton
value='detailedCoveredAreaView'
disabled={
geoJsonLoading || geoJsonError || boundingBox === undefined
}
aria-label='Detailed Covered Area View'
>
<TravelExploreIcon />
</ToggleButton>
</Tooltip>
<Tooltip title={t('boundingBoxViewTooltip')}>
<ToggleButton
value='boundingBoxView'
aria-label='Bounding Box View'
>
<MapIcon />
</ToggleButton>
</Tooltip>
</ToggleButtonGroup>
)}
</Box>
{feed?.data_type === 'gtfs' &&
boundingBox === undefined &&
view === 'boundingBoxView' && (
Expand Down
Loading
Loading