Skip to content
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

PublicTransport: Select visible routes #681

Merged
merged 8 commits into from
Oct 17, 2024
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
129 changes: 129 additions & 0 deletions src/components/FeaturePanel/PublicTransport/routes/CategoryHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React from 'react';
import { t } from '../../../../services/intl';
import {
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
} from '@mui/material';
import {
MoreHoriz,
Search,
Visibility,
VisibilityOff,
} from '@mui/icons-material';

// TODO: Do this using the id-tagging-presets
const fmtCategory = (category: string) => {
switch (category) {
case 'tourism':
return t('publictransport.tourism');
case 'night':
return t('publictransport.night');
case 'car_shuttle':
return t('publictransport.car_shuttle');
case 'car':
return t('publictransport.car');
case 'commuter':
return t('publictransport.commuter');
case 'regional':
return t('publictransport.regional');
case 'long_distance':
return t('publictransport.long_distance');
case 'high_speed':
return t('publictransport.high_speed');
case 'bus':
return t('publictransport.bus');
case 'subway':
return t('publictransport.subway');
case 'unknown':
return t('publictransport.unknown');
default:
return category;
}
};

const ToggleCategory = ({
shown,
onClick,
}: {
shown: boolean;
onClick?: React.MouseEventHandler<HTMLLIElement>;
}) => (
<MenuItem onClick={onClick}>
<ListItemIcon>
{shown && <VisibilityOff fontSize="small" />}
{!shown && <Visibility fontSize="small" />}
</ListItemIcon>
<ListItemText>
{shown
? t('publictransport.hide_this_category')
: t('publictransport.show_this_category')}
</ListItemText>
</MenuItem>
);

type Props = {
category: string;
shownCategories: string[];
onChange: (categories: string[]) => void;
};

export const CategoryHeading = ({
category,
shownCategories,
onChange,
}: Props) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLButtonElement>(
null,
);
const isShownOnMap = shownCategories.includes(category);

return (
<>
<Stack direction="row" spacing={2} alignItems="center" mb={1}>
<h4>{fmtCategory(category)}</h4>
<IconButton
onClick={({ currentTarget }) => {
setAnchorEl(currentTarget);
}}
>
<MoreHoriz />
</IconButton>
</Stack>
<Menu
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => {
setAnchorEl(null);
}}
MenuListProps={{ 'aria-labelledby': 'basic-button' }}
>
<ToggleCategory
shown={isShownOnMap}
onClick={() => {
const newCategories = isShownOnMap
? shownCategories.filter((cat) => cat !== category)
: shownCategories.concat(category);
onChange(newCategories);
setAnchorEl(null);
}}
/>
<MenuItem
onClick={() => {
onChange([category]);
setAnchorEl(null);
}}
disabled={isShownOnMap && shownCategories.length === 1}
>
<ListItemIcon>
<Search fontSize="small" />
</ListItemIcon>
<ListItemText>{t('publictransport.only_this_category')}</ListItemText>
</MenuItem>
</Menu>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { LineInformation } from './requestRoutes';
import { LineNumber } from './LineNumber';
import { t } from '../../../../services/intl';
import { CategoryHeading } from './CategoryHeading';

const PublicTransportWrapper = ({ children }) => {
const divStyle: React.CSSProperties = {
Expand All @@ -16,49 +16,29 @@ const PublicTransportWrapper = ({ children }) => {
return <div style={divStyle}>{children}</div>;
};

// TODO: Do this using the id-tagging-presets
const fmtCategory = (category: string) => {
switch (category) {
case 'tourism':
return t('publictransport.tourism');
case 'night':
return t('publictransport.night');
case 'car_shuttle':
return t('publictransport.car_shuttle');
case 'car':
return t('publictransport.car');
case 'commuter':
return t('publictransport.commuter');
case 'regional':
return t('publictransport.regional');
case 'long_distance':
return t('publictransport.long_distance');
case 'high_speed':
return t('publictransport.high_speed');
case 'bus':
return t('publictransport.bus');
case 'subway':
return t('publictransport.subway');
case 'unknown':
return t('publictransport.unknown');
default:
return category;
}
};

interface CategoryProps {
category: string;
shownCategories: string[];
lines: LineInformation[];
showHeading: boolean;
onChange: (categories: string[]) => void;
}

export const PublicTransportCategory: React.FC<CategoryProps> = ({
category,
lines,
shownCategories,
showHeading,
onChange,
}) => (
<>
{showHeading && <h4>{fmtCategory(category)}</h4>}
{showHeading && (
<CategoryHeading
category={category}
shownCategories={shownCategories}
onChange={onChange}
/>
)}
<PublicTransportWrapper>
{lines.map((line) => (
<LineNumber key={line.ref} line={line} />
Expand Down
45 changes: 28 additions & 17 deletions src/components/FeaturePanel/PublicTransport/routes/Routes.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import React, { useState } from 'react';
import { LineInformation, requestLines } from './requestRoutes';
import { PublicTransportCategory } from './PublicTransportWrapper';
import { DotLoader } from '../../../helpers';
import { sortByReference } from './helpers';
import { useFeatureContext } from '../../../utils/FeatureContext';
import { getOverpassSource } from '../../../../services/mapStorage';
import groupBy from 'lodash/groupBy';
import { useQuery } from 'react-query';
import { Typography } from '@mui/material';
import { useShowOnMap } from './useShowOnMap';

const categories = [
'tourism',
Expand All @@ -23,22 +23,43 @@ const categories = [
'unknown',
];

const PublicTransportDisplay = ({ routes }) => {
type PublicTransportDisplayProps = {
routes: LineInformation[];
geoJson: GeoJSON.FeatureCollection;
};

const PublicTransportDisplay = ({
routes,
geoJson,
}: PublicTransportDisplayProps) => {
const [shownCategories, setShownCategories] = useState([
'subway',
'commuter',
'regional',
'bus',
]);

const grouped = groupBy(routes, ({ service }) => {
const base = service?.split(';')[0];
return categories.includes(base) ? base : 'unknown';
});
const entries = Object.entries(grouped) as [string, LineInformation[]][];
const sorted = sortByReference(entries, categories, ([category]) => category);

useShowOnMap(geoJson, shownCategories);

return (
<>
{sorted.map(([category, lines]) => (
<PublicTransportCategory
key={category}
category={category}
lines={lines}
showHeading={entries.length > 1}
showHeading={sorted.length > 1}
shownCategories={shownCategories}
onChange={(categories) => {
setShownCategories(categories);
}}
/>
))}
</>
Expand All @@ -53,22 +74,12 @@ export const PublicTransportInner = () => {
requestLines(type, Number(id)),
);

React.useEffect(() => {
if (!data) {
return;
}

const source = getOverpassSource();
source?.setData(data.geoJson);
return () => {
source?.setData({ type: 'FeatureCollection', features: [] });
};
}, [data]);

return (
<div>
{(status === 'loading' || status === 'idle') && <DotLoader />}
{status === 'success' && <PublicTransportDisplay routes={data.routes} />}
{status === 'success' && (
<PublicTransportDisplay routes={data.routes} geoJson={data.geoJson} />
)}
{status === 'error' && (
<Typography color="secondary" paragraph>
Error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ test('conversion', async () => {
osmappType: 'relation',
ref: '89',
route: 'bus',
service: 'bus',
subclass: 'bus',
type: 'route',
},
Expand All @@ -103,6 +104,7 @@ test('conversion', async () => {
osmappType: 'relation',
ref: '89',
route: 'bus',
service: 'bus',
subclass: 'bus',
type: 'route',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export async function requestLines(featureType: string, id: number) {

const geoJsonFeatures = overpassGeomToGeojson({ elements: routes });

const geoJson: GeoJSON.GeoJSON = {
type: 'FeatureCollection',
const geoJson = {
type: 'FeatureCollection' as const,
features: geoJsonFeatures,
};

Expand All @@ -106,7 +106,16 @@ export async function requestLines(featureType: string, id: number) {
.sort((a, b) => a.ref.localeCompare(b.ref, intl.lang, { numeric: true }));

return {
geoJson,
geoJson: {
...geoJson,
features: geoJson.features.map((feature) => ({
...feature,
properties: {
...feature.properties,
service: getService(feature.tags, []),
},
})),
},
routes: allRoutes,
};
}
25 changes: 25 additions & 0 deletions src/components/FeaturePanel/PublicTransport/routes/useShowOnMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import { getOverpassSource } from '../../../../services/mapStorage';

export const useShowOnMap = (
routes: GeoJSON.FeatureCollection,
visiblCategories: string[],
) => {
useEffect(() => {
if (!routes) {
return;
}

const filteredRoutes = routes.features.filter(({ properties }) =>
visiblCategories.includes(properties.service),
);
const source = getOverpassSource();
source?.setData({
type: 'FeatureCollection',
features: filteredRoutes,
});
return () => {
source?.setData({ type: 'FeatureCollection', features: [] });
};
}, [routes, visiblCategories]);
};
4 changes: 4 additions & 0 deletions src/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ export default {
'publictransport.subway': 'U-Bahn',
'publictransport.unknown': 'Unbekannter Typ',

'publictransport.show_this_category': 'Zeige diese Kategorie an',
'publictransport.hide_this_category': 'Blende diese Kategorie aus',
'publictransport.only_this_category': 'Zeige nur diese Kategorie an',

'publictransport.route': 'Streckenverlauf',
'publictransport.hidden_stops': '__amount__ weitere Haltestellen',
'publictransport.visible_stops': 'Blende __amount__ Haltestellen aus',
Expand Down
4 changes: 4 additions & 0 deletions src/locales/vocabulary.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ export default {
'publictransport.subway': 'Subway',
'publictransport.unknown': 'Unknown type',

'publictransport.show_this_category': 'Show this category',
'publictransport.hide_this_category': 'Hide this category',
'publictransport.only_this_category': 'Show only this category',

'publictransport.route': 'Route',
'publictransport.hidden_stops': '__amount__ more stops',
'publictransport.visible_stops': 'Hide __amount__ stops',
Expand Down
6 changes: 5 additions & 1 deletion src/services/overpassSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ const convertOsmIdToMapId = (apiId: OsmId) => {
return parseInt(`${apiId.id}${osmToMapType[apiId.type]}`, 10);
};

type OverpassFeature = Feature & {
tags: Record<string, string>;
};

// TODO use our own implementaion from fetchCrags, which handles recursive geometries
export const overpassGeomToGeojson = (response: {
elements: any[];
}): Feature[] =>
}): OverpassFeature[] =>
response.elements.map((element) => {
const { type, id, tags = {} } = element;
const geometry = GEOMETRY[type]?.(element);
Expand Down
Loading