Skip to content

Commit 6be509f

Browse files
authored
Merge pull request #5601 from HSLdevcom/AB#173
AB#173 Filter Traffic now results by vehicle mode
2 parents ebc2109 + 370c96f commit 6be509f

File tree

13 files changed

+238
-46
lines changed

13 files changed

+238
-46
lines changed

app/component/trafficnow/Alerts.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
1+
import React, { useMemo, useRef, useState } from 'react';
22
import cx from 'classnames';
33
import { useLazyLoadQuery } from 'react-relay/hooks';
44
import { FormattedMessage } from 'react-intl';
@@ -7,25 +7,16 @@ import { useBreakpoint } from '../../util/withBreakpoint';
77
import { useConfigContext } from '../../configurations/ConfigContext';
88
import AlertsQuery from './queries/AlertsQuery';
99
import NoAlerts from './NoAlerts';
10-
import useWindowResize from '../../hooks/useWindowSize';
1110
import { useFilterContext } from './filters/FiltersContext';
12-
import { filterAlerts } from './filters/filterUtils';
11+
import { filterAndSortAlerts } from './filters/filterUtils';
1312

1413
export default function Alerts() {
1514
const breakpoint = useBreakpoint();
1615
const { feedIds } = useConfigContext();
1716
const [activeAlertId, setActiveAlertId] = useState();
18-
const { height } = useWindowResize();
1917
const ref = useRef();
20-
const [top, setTop] = useState(0);
2118
const { selectedFilters } = useFilterContext();
2219

23-
useLayoutEffect(() => {
24-
if (ref.current) {
25-
setTop(ref.current.getBoundingClientRect().top);
26-
}
27-
}, [height]);
28-
2920
const handleCardClick = id => {
3021
setActiveAlertId(id);
3122
};
@@ -35,7 +26,7 @@ export default function Alerts() {
3526
});
3627

3728
const filteredAlerts = useMemo(
38-
() => filterAlerts(alerts, selectedFilters),
29+
() => filterAndSortAlerts(alerts, selectedFilters),
3930
[alerts, selectedFilters],
4031
);
4132

@@ -47,9 +38,6 @@ export default function Alerts() {
4738
className={cx('traffic-now__content__alerts', {
4839
'traffic-now__content__alerts--desktop': desktop,
4940
})}
50-
style={{
51-
maxHeight: `calc(100vh - ${top}px)`,
52-
}}
5341
>
5442
{filteredAlerts.length === 0 ? (
5543
<NoAlerts />

app/component/trafficnow/DisruptionCard.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,14 @@ export default function DisruptionCard({ alert, isOpen, onClick }) {
3737
const isValid =
3838
now > effectiveStartDate * 1000 && now < effectiveEndDate * 1000;
3939

40-
const validityPeriod = `${getFormattedTimeDate(
40+
const startDate = `${getFormattedTimeDate(
4141
effectiveStartDate * 1000,
4242
DATE_FORMAT,
43-
)} - ${getFormattedTimeDate(effectiveEndDate * 1000, DATE_FORMAT)}`;
43+
)}`;
44+
const endDate = `${getFormattedTimeDate(
45+
effectiveEndDate * 1000,
46+
DATE_FORMAT,
47+
)}`;
4448

4549
return (
4650
<Card
@@ -95,7 +99,8 @@ export default function DisruptionCard({ alert, isOpen, onClick }) {
9599
{alertSeverityLevel !== AlertSeverityLevelType.Info && (
96100
<>
97101
<div className="separator vertical" />
98-
{validityPeriod}
102+
{startDate}
103+
{startDate !== endDate && ` - ${endDate}`}
99104
</>
100105
)}
101106
</div>

app/component/trafficnow/filters/Filters.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import cx from 'classnames';
55
import ValidityPeriodFilter from './ValidityPeriodFilter';
66
import { useFilterContext } from './FiltersContext';
77
import { useBreakpoint } from '../../../util/withBreakpoint';
8+
import VehicleModesFilter from './VehicleModesFilter';
89

910
const Filters = ({ onApplyClick, onResetClick }) => {
1011
const { selectedFilters, resetFilters, DEFAULT_FILTERS } = useFilterContext();
@@ -17,6 +18,10 @@ const Filters = ({ onApplyClick, onResetClick }) => {
1718
id: 'validityPeriod',
1819
Component: ValidityPeriodFilter,
1920
},
21+
{
22+
id: 'vehicleModes',
23+
Component: VehicleModesFilter,
24+
},
2025
];
2126

2227
const handleResetClick = () => {

app/component/trafficnow/filters/FiltersContext.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const FilterContext = createContext();
55

66
const DEFAULT_FILTERS = {
77
validityPeriod: 'ALL',
8+
vehicleModes: [],
89
};
910

1011
const FilterContextProvider = ({ children }) => {

app/component/trafficnow/filters/ValidityPeriodFilter.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ const ValidityPeriodFilter = ({ filterId }) => {
1414

1515
return (
1616
<fieldset>
17-
<legend>Näytä voimassaolon mukaan</legend>
17+
<FormattedMessage
18+
tagName="legend"
19+
id="traffic-now_filters_validity-period"
20+
defaultMessage="Näytä voimassaolon mukaan"
21+
/>
1822
{FILTER_OPTIONS.map(option => (
19-
<label key={option.value}>
23+
<label key={option.value} htmlFor={`period-${option}`}>
2024
<input
25+
id={`period-${option}`}
2126
type="radio"
22-
name="myRadio"
27+
name="validityPeriodRadio"
2328
checked={selectedFilters[filterId] === option.value}
2429
value={option.value}
2530
onChange={() => setFilter(filterId, option.value)}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react';
2+
import { FormattedMessage } from 'react-intl';
3+
import PropTypes from 'prop-types';
4+
import { useFilterContext } from './FiltersContext';
5+
import { useConfigContext } from '../../../configurations/ConfigContext';
6+
import { getTransportModes } from '../../../util/modeUtils';
7+
import { TrafficNowTransportModes } from '../../../constants';
8+
9+
const VehicleModesFilter = ({ filterId }) => {
10+
const config = useConfigContext();
11+
const { selectedFilters, setFilter } = useFilterContext();
12+
13+
const handleCheck = option => {
14+
const checked = selectedFilters[filterId] || [];
15+
16+
if (checked.includes(option)) {
17+
setFilter(
18+
filterId,
19+
checked.filter(c => c !== option),
20+
);
21+
} else {
22+
setFilter(filterId, [...checked, option]);
23+
}
24+
};
25+
26+
const availableModes = Object.entries(getTransportModes(config)).reduce(
27+
(acc, [k, v]) => {
28+
if (
29+
v.availableForSelection &&
30+
TrafficNowTransportModes.includes(k.toUpperCase())
31+
) {
32+
acc.push(k);
33+
}
34+
return acc;
35+
},
36+
[],
37+
);
38+
39+
return (
40+
<fieldset>
41+
<FormattedMessage
42+
tagName="legend"
43+
id="traffic-now_filters_vehicle-mode"
44+
defaultMessage="Näytä liikennevälineen mukaan"
45+
/>
46+
{availableModes.map(option => (
47+
<label key={option} htmlFor={`vehicleModes-${option}`}>
48+
<input
49+
id={`vehicleModes-${option}`}
50+
type="checkbox"
51+
checked={selectedFilters[filterId]?.includes(option)}
52+
value={option}
53+
onChange={() => handleCheck(option)}
54+
/>
55+
<FormattedMessage id={option.toLowerCase()} />
56+
</label>
57+
))}
58+
</fieldset>
59+
);
60+
};
61+
62+
VehicleModesFilter.propTypes = {
63+
filterId: PropTypes.string.isRequired,
64+
};
65+
66+
export default VehicleModesFilter;
Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const validityPeriodFilter = (alert, selectedFilters) => {
1+
const validityPeriodFilter = (alert, { validityPeriod }) => {
22
const now = Date.now() * 0.001;
3-
switch (selectedFilters.validityPeriod) {
3+
switch (validityPeriod) {
44
case 'VALID':
55
return now >= alert.effectiveStartDate && now <= alert.effectiveEndDate;
66
case 'UPCOMING':
@@ -11,9 +11,33 @@ const validityPeriodFilter = (alert, selectedFilters) => {
1111
}
1212
};
1313

14-
export function filterAlerts(alerts, selectedFilters) {
15-
const filterFns = [validityPeriodFilter];
16-
return alerts.filter(alert =>
17-
filterFns.every(fn => fn(alert, selectedFilters)),
14+
/**
15+
* Filters alerts by selected vehicle modes. If no modes are selected, include all alerts.
16+
* If any entity matches a selected mode, include the alert.
17+
*
18+
* entities may contain objects with different properties:
19+
* - Stop: entity with a vehicleMode property
20+
* - Route: entity with a mode property
21+
* - StopOnRoute: entity with a nested route object that has a mode property
22+
*
23+
*/
24+
const vehicleModesFilter = ({ entities }, { vehicleModes }) => {
25+
const modes = (vehicleModes || []).map(m => m.toLowerCase());
26+
return (
27+
modes.length === 0 ||
28+
entities.some(e => {
29+
const mode =
30+
/* Stop */ e.vehicleMode?.toLowerCase() ||
31+
/* Route */ e.mode?.toLowerCase() ||
32+
/* StopOnRoute */ e.route?.mode?.toLowerCase();
33+
return modes.includes(mode);
34+
})
1835
);
36+
};
37+
38+
export function filterAndSortAlerts(alerts, selectedFilters) {
39+
const filterFns = [validityPeriodFilter, vehicleModesFilter];
40+
return alerts
41+
.filter(alert => filterFns.every(fn => fn(alert, selectedFilters)))
42+
.sort((a, b) => a.effectiveStartDate - b.effectiveStartDate);
1943
}

app/component/trafficnow/queries/AlertsQuery.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default graphql`
2020
gtfsId
2121
locationType
2222
vehicleMode
23+
platformCode
2324
}
2425
... on Route {
2526
gtfsId
@@ -41,6 +42,7 @@ export default graphql`
4142
gtfsId
4243
locationType
4344
vehicleMode
45+
platformCode
4446
}
4547
}
4648
}

app/component/trafficnow/trafficnow.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@
104104
margin-left: unset;
105105
}
106106

107-
input[type='radio'] {
107+
input[type='radio'],
108+
input[type='checkbox'] {
108109
accent-color: $primary-color;
109110
width: 1em;
110111
height: 1em;

app/component/trafficnow/utils.js

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,35 @@ import { getRouteMode } from '../../util/modeUtils';
33
import { AlertEntityType, LocationTypes } from '../../constants';
44
import { stopPagePath, routePagePath } from '../../util/path';
55

6+
const sortAlphaNumeric = (a, b) => {
7+
const first = typeof a === 'string' ? a.toLowerCase() : a.toString();
8+
const second = typeof b === 'string' ? b.toLowerCase() : b.toString();
9+
10+
return first.localeCompare(second);
11+
};
12+
13+
const getMode = (stopOrRoute, config) => {
14+
const routeMode = getRouteMode(stopOrRoute, config);
15+
if (routeMode) {
16+
return routeMode;
17+
}
18+
19+
return stopOrRoute?.vehicleMode?.toLowerCase();
20+
};
21+
622
const addToModeGroup = (
723
acc,
8-
{ mode, id, shortName, name, gtfsId, isStop = false, isStation = false },
24+
{
25+
mode,
26+
id,
27+
shortName,
28+
name,
29+
gtfsId,
30+
platformCode,
31+
locationType,
32+
isStop = false,
33+
isStation = false,
34+
},
935
) => {
1036
const url =
1137
isStop || isStation
@@ -18,25 +44,31 @@ const addToModeGroup = (
1844
mode,
1945
isRoute: !isStop && !isStation,
2046
entities: [],
47+
ids: new Set(),
48+
platformCode,
49+
locationType,
2150
};
2251
}
23-
acc[key].entities.push({
24-
id,
25-
name: shortName || name,
26-
url,
27-
isStop,
28-
isStation,
29-
});
52+
if (!acc[key].ids.has(id)) {
53+
acc[key].entities.push({
54+
id,
55+
name: shortName || name,
56+
url,
57+
isStop,
58+
isStation,
59+
});
60+
acc[key].ids.add(id);
61+
}
3062
};
3163

3264
const groupEntitiesByMode = (entities, config) => {
33-
const group = entities
65+
const grouped = entities
3466
.filter(e => e.__typename !== AlertEntityType.Unknown)
3567
.reduce((acc, e) => {
3668
if (!e.route && !e.stop) {
3769
addToModeGroup(acc, {
3870
...e,
39-
mode: getRouteMode(e, config) || e.vehicleMode?.toLowerCase(),
71+
mode: getMode(e, config),
4072
isStop: !!e.locationType,
4173
isStation: e.locationType === LocationTypes.STATION,
4274
});
@@ -46,20 +78,25 @@ const groupEntitiesByMode = (entities, config) => {
4678
if (e.route) {
4779
addToModeGroup(acc, {
4880
...e.route,
49-
mode: getRouteMode(e.route, config),
81+
mode: getMode(e.route, config),
5082
});
5183
}
5284
if (e.stop) {
5385
addToModeGroup(acc, {
5486
...e.stop,
55-
mode: e.stop.vehicleMode?.toLowerCase(),
87+
mode: getMode(e.stop, config),
5688
isStop: true,
5789
isStation: e.locationType === LocationTypes.STATION,
5890
});
5991
}
6092
return acc;
6193
}, {});
62-
return group;
94+
95+
Object.values(grouped).forEach(group => {
96+
group.entities.sort((a, b) => sortAlphaNumeric(a.name, b.name));
97+
});
98+
99+
return grouped;
63100
};
64101

65102
export { groupEntitiesByMode };

0 commit comments

Comments
 (0)