Skip to content

Commit 5b8ce3d

Browse files
committed
icons: add an activity to icon mapping
Update Transition submodules to get the marker icons. Add a map of activities and activity categories to the path of the corresponding icon. As there is no exhaustive list of possible activities, the activities were taken from the od_nationale_quebec. It maps to the best possible icon representing the activity. Also add a function to retrieve the icon for a specific activity or category, which returns the default icon if the activity mapping does not exist. Another function returns a map marker for the activity and activity category. It implies that for each activity, there is a basic icon for the activity with a `.svg` extension, but also marker icons, ie icons with a pointy end to mark a location, with suffix `-marker_round.svg` and `-marker_square.svg`, to support either a round or square marker.
1 parent b2426e4 commit 5b8ce3d

File tree

7 files changed

+175
-26
lines changed

7 files changed

+175
-26
lines changed

packages/evolution-common/src/services/questionnaire/sections/common/__tests__/widgetPersonVisitedPlacesMap.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,10 @@ describe('personVisitedPlacesMapConfig geojsons', () => {
229229
points: {
230230
type: 'FeatureCollection',
231231
features: [
232-
{ ...visitedPlaces[0].geography!, properties: { ...visitedPlaces[0].geography!.properties, icon: { url: '/dist/images/activities_icons/home_marker.svg', size: [40, 40] }, highlighted: false, label: 'home', sequence: 1 } },
233-
{ ...visitedPlaces[1].geography!, properties: { ...visitedPlaces[1].geography!.properties, icon: { url: '/dist/images/activities_icons/work_marker.svg', size: [40, 40] }, highlighted: false, label: 'place2', sequence: 2 } },
234-
{ ...visitedPlaces[2].geography!, properties: { ...visitedPlaces[2].geography!.properties, icon: { url: '/dist/images/activities_icons/shopping_marker.svg', size: [40, 40] }, highlighted: false, label: 'place3', sequence: 3 } },
235-
{ ...visitedPlaces[3].geography!, properties: { ...visitedPlaces[3].geography!.properties, icon: { url: '/dist/images/activities_icons/home_marker.svg', size: [40, 40] }, highlighted: false, label: 'home', sequence: 3 } }
232+
{ ...visitedPlaces[0].geography!, properties: { ...visitedPlaces[0].geography!.properties, icon: { url: '/dist/icons/activities/home/home-marker_round.svg', size: [40, 40] }, highlighted: false, label: 'home', sequence: 1 } },
233+
{ ...visitedPlaces[1].geography!, properties: { ...visitedPlaces[1].geography!.properties, icon: { url: '/dist/icons/activities/work/briefcase-marker_round.svg', size: [40, 40] }, highlighted: false, label: 'place2', sequence: 2 } },
234+
{ ...visitedPlaces[2].geography!, properties: { ...visitedPlaces[2].geography!.properties, icon: { url: '/dist/icons/activities/shopping/shopping_cart-marker_round.svg', size: [40, 40] }, highlighted: false, label: 'place3', sequence: 3 } },
235+
{ ...visitedPlaces[3].geography!, properties: { ...visitedPlaces[3].geography!.properties, icon: { url: '/dist/icons/activities/home/home-marker_round.svg', size: [40, 40] }, highlighted: false, label: 'home', sequence: 3 } }
236236
]
237237
},
238238
linestrings: {
@@ -297,8 +297,8 @@ describe('personVisitedPlacesMapConfig geojsons', () => {
297297
points: {
298298
type: 'FeatureCollection',
299299
features: [
300-
{ ...visitedPlaces[1].geography!, properties: { ...visitedPlaces[1].geography!.properties, icon: { url: '/dist/images/activities_icons/work_marker.svg', size: [40, 40] }, highlighted: false, label: 'place2', sequence: 2 } },
301-
{ ...visitedPlaces[3].geography!, properties: { ...visitedPlaces[3].geography!.properties, icon: { url: '/dist/images/activities_icons/home_marker.svg', size: [40, 40] }, highlighted: false, label: 'home', sequence: 3 } }
300+
{ ...visitedPlaces[1].geography!, properties: { ...visitedPlaces[1].geography!.properties, icon: { url: '/dist/icons/activities/work/briefcase-marker_round.svg', size: [40, 40] }, highlighted: false, label: 'place2', sequence: 2 } },
301+
{ ...visitedPlaces[3].geography!, properties: { ...visitedPlaces[3].geography!.properties, icon: { url: '/dist/icons/activities/home/home-marker_round.svg', size: [40, 40] }, highlighted: false, label: 'home', sequence: 3 } }
302302
]
303303
},
304304
linestrings: {

packages/evolution-common/src/services/questionnaire/sections/common/widgetPersonVisitedPlacesMap.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { TFunction } from 'i18next';
1212
import projectConfig from '../../../../config/project.config';
1313
import { pointsToBezierCurve } from '../../../geodata/SurveyGeographyUtils';
1414
import { UserInterviewAttributes } from '../../types';
15+
import { getActivityMarkerIcon } from '../visitedPlaces/activityIconMapping';
1516

1617
export const getPersonVisitedPlacesMapConfig = (
1718
// FIXME: Type this when there is a few more widgets implemented
@@ -75,7 +76,7 @@ export const getPersonVisitedPlacesMapConfig = (
7576
if (visitedPlaceGeography) {
7677
const visitedPlaceGeojson = visitedPlaceGeography;
7778
visitedPlaceGeojson.properties!.icon = {
78-
url: `/dist/images/activities_icons/${visitedPlace.activity}_marker.svg`,
79+
url: getActivityMarkerIcon(visitedPlace.activity),
7980
size: [40, 40]
8081
};
8182
visitedPlaceGeojson.properties!.highlighted = false;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2025, Polytechnique Montreal and contributors
3+
*
4+
* This file is licensed under the MIT License.
5+
* License text available at https://opensource.org/licenses/MIT
6+
*/
7+
8+
import { getActivityIcon, getActivityMarkerIcon } from '../activityIconMapping';
9+
import { Activity } from '../../../../baseObjects/attributeTypes/VisitedPlaceAttributes';
10+
11+
describe('activityIconMapping', () => {
12+
describe('getActivityIcon', () => {
13+
test('should return default icon for undefined activity', () => {
14+
expect(getActivityIcon(undefined)).toBe('/dist/icons/activities/other/question_mark.svg');
15+
});
16+
17+
test('should return default icon for null activity', () => {
18+
expect(getActivityIcon(null)).toBe('/dist/icons/activities/other/question_mark.svg');
19+
});
20+
21+
test('should return correct icon for specific activity', () => {
22+
expect(getActivityIcon('home' as Activity)).toBe('/dist/icons/activities/home/home.svg');
23+
});
24+
25+
test('should return default icon for non-existent activity', () => {
26+
expect(getActivityIcon('nonExistentActivity' as any)).toBe('/dist/icons/activities/other/question_mark.svg');
27+
});
28+
});
29+
30+
describe('getActivityMarkerIcon', () => {
31+
test('should return default round marker icon for undefined activity', () => {
32+
expect(getActivityMarkerIcon(undefined)).toBe('/dist/icons/activities/other/question_mark-marker_round.svg');
33+
});
34+
35+
test('should return default round marker icon for null activity', () => {
36+
expect(getActivityMarkerIcon(null)).toBe('/dist/icons/activities/other/question_mark-marker_round.svg');
37+
});
38+
39+
test('should return correct round marker icon for specific activity', () => {
40+
expect(getActivityMarkerIcon('work' as Activity)).toBe('/dist/icons/activities/work/briefcase-marker_round.svg');
41+
});
42+
43+
test('should return correct round marker icon for specific activity, with round specified', () => {
44+
expect(getActivityMarkerIcon('volunteering' as Activity, 'round')).toBe('/dist/icons/activities/other/volunteering-marker_round.svg');
45+
});
46+
47+
test('should return correct square marker icon for specific activity', () => {
48+
expect(getActivityMarkerIcon('school' as Activity, 'square')).toBe('/dist/icons/activities/school/graduation_cap-marker_square.svg');
49+
});
50+
});
51+
52+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2025, Polytechnique Montreal and contributors
3+
*
4+
* This file is licensed under the MIT License.
5+
* License text available at https://opensource.org/licenses/MIT
6+
*/
7+
8+
import { Activity, ActivityCategory } from '../../../baseObjects/attributeTypes/VisitedPlaceAttributes';
9+
10+
/**
11+
* Maps activities and activity categories to their corresponding icon paths
12+
* This mapping associates each activity with an appropriate icon in the /dist/icons/activities/ directory
13+
*/
14+
const activityToIconPathMapping: Record<ActivityCategory | Activity | 'default', string> = {
15+
// FIXME Theses activities are the ones used in the visited places section
16+
// of the od_nationale_quebec survey. This may not be the complete list of
17+
// activities.
18+
19+
// Home activities
20+
home: '/dist/icons/activities/home/home',
21+
otherParentHome: '/dist/icons/activities/home/home',
22+
// FIXME Validate this icon, it's using the 2 persons icon, as in the previous surveys, but its name is accompany_someone
23+
visiting: '/dist/icons/activities/accompany_drop_fetch_someone/accompany_someone',
24+
secondaryHome: '/dist/icons/activities/home/home_secondary',
25+
26+
// Work/school related activities
27+
work: '/dist/icons/activities/work/briefcase',
28+
school: '/dist/icons/activities/school/graduation_cap',
29+
workUsual: '/dist/icons/activities/work/briefcase',
30+
workNotUsual: '/dist/icons/activities/work/building_office',
31+
workOnTheRoad: '/dist/icons/activities/work/truck_with_road',
32+
schoolUsual: '/dist/icons/activities/school/graduation_cap',
33+
schoolNotUsual: '/dist/icons/activities/school/school_building_small',
34+
schoolNotStudent: '/dist/icons/activities/school/graduation_cap',
35+
36+
// Service and leisure activities
37+
shoppingServiceRestaurant: '/dist/icons/activities/shopping/shopping_cart',
38+
leisure: '/dist/icons/activities/leisure/music',
39+
shopping: '/dist/icons/activities/shopping/shopping_cart',
40+
restaurant: '/dist/icons/activities/other/restaurant',
41+
service: '/dist/icons/activities/services/bank',
42+
// FIXME Validate this icon, it's too sportsy
43+
leisureStroll: '/dist/icons/activities/leisure/sports_run',
44+
leisureSports: '/dist/icons/activities/leisure/sports_run',
45+
leisureArtsMusicCulture: '/dist/icons/activities/leisure/music',
46+
leisureTourism: '/dist/icons/activities/leisure/luggages',
47+
medical: '/dist/icons/activities/other/medical_cross',
48+
veterinarian: '/dist/icons/activities/services/cat_dog',
49+
50+
// Various activities
51+
dropFetchSomeone: '/dist/icons/activities/accompany_drop_fetch_someone/drop_fetch_someone',
52+
dropSomeone: '/dist/icons/activities/accompany_drop_fetch_someone/drop_someone',
53+
fetchSomeone: '/dist/icons/activities/accompany_drop_fetch_someone/fetch_someone',
54+
accompanySomeone: '/dist/icons/activities/accompany_drop_fetch_someone/accompany_someone',
55+
worship: '/dist/icons/activities/other/worship',
56+
volunteering: '/dist/icons/activities/other/volunteering',
57+
carElectricChargingStation: '/dist/icons/activities/other/electric_charging_station',
58+
carsharingStation: '/dist/icons/activities/other/carsharing_station',
59+
pickClassifiedPurchase: '/dist/icons/activities/shopping/shopping_basket',
60+
61+
// Other options
62+
other: '/dist/icons/activities/other/question_mark',
63+
dontKnow: '/dist/icons/activities/other/question_mark',
64+
preferNotToAnswer: '/dist/icons/activities/other/question_mark',
65+
default: '/dist/icons/activities/other/question_mark'
66+
};
67+
68+
const resolveIconBasePath = (activity: ActivityCategory | Activity | null | undefined): string => {
69+
const key = activity ?? 'default';
70+
return activityToIconPathMapping[key] ?? activityToIconPathMapping.default;
71+
};
72+
73+
/**
74+
* Returns the appropriate icon path for a given activity or activity category
75+
* @param activity The activity or activity category for which to get the icon
76+
* @returns The path to the icon for the activity
77+
*/
78+
export const getActivityIcon = (activity: ActivityCategory | Activity | null | undefined): string => {
79+
return `${resolveIconBasePath(activity)}.svg`;
80+
};
81+
82+
type MarkerType = 'round' | 'square';
83+
/**
84+
* Returns the appropriate icon path for the map marker corresponding to a given
85+
* activity or activity category
86+
* @param activity The activity or activity category for which to get the icon
87+
* @param markerType The type of marker to use ('round' or 'square')
88+
* @returns The path to the marker icon for the activity
89+
*/
90+
export const getActivityMarkerIcon = (
91+
activity: ActivityCategory | Activity | null | undefined,
92+
markerType: MarkerType = 'round'
93+
): string => {
94+
return `${resolveIconBasePath(activity)}-marker_${markerType}.svg`;
95+
};

packages/evolution-frontend/src/components/survey/sectionTemplates/TripsAndSegmentsSection.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import * as helpers from 'evolution-common/lib/utils/helpers';
2323
import { secondsSinceMidnightToTimeStrWithSuffix } from '../../../services/display/frontendHelper';
2424
import { loopActivities } from 'evolution-common/lib/services/odSurvey/types';
2525
import { VisitedPlace } from 'evolution-common/lib/services/questionnaire/types';
26+
import { getActivityMarkerIcon } from 'evolution-common/lib/services/questionnaire/sections/visitedPlaces/activityIconMapping';
2627

2728
export const SegmentsSection: React.FC<SectionProps & WithTranslation & WithSurveyContextProps> = (
2829
props: SectionProps & WithTranslation & WithSurveyContextProps
@@ -198,7 +199,7 @@ export const SegmentsSection: React.FC<SectionProps & WithTranslation & WithSurv
198199
>
199200
<span className="survey-trip-item-element survey-trip-item-origin-description">
200201
<img
201-
src={`/dist/images/activities_icons/${origin.activity}_marker.svg`}
202+
src={getActivityMarkerIcon(origin.activity)}
202203
style={{ height: '4rem' }}
203204
alt={props.t(`visitedPlaces/activities/${origin.activity}`)}
204205
/>
@@ -217,7 +218,7 @@ export const SegmentsSection: React.FC<SectionProps & WithTranslation & WithSurv
217218
</span>
218219
<span className="survey-trip-item-element survey-trip-item-destination-description">
219220
<img
220-
src={`/dist/images/activities_icons/${destination.activity}_marker.svg`}
221+
src={getActivityMarkerIcon(destination.activity)}
221222
style={{ height: '4rem' }}
222223
alt={props.t(`visitedPlaces/activities/${destination.activity}`)}
223224
/>
@@ -241,7 +242,7 @@ export const SegmentsSection: React.FC<SectionProps & WithTranslation & WithSurv
241242
</span>
242243
<span className="survey-trip-item-element survey-trip-item-destination-description">
243244
<img
244-
src={`/dist/images/activities_icons/${nextDestination.activity}_marker.svg`}
245+
src={getActivityMarkerIcon(nextDestination.activity)}
245246
style={{ height: '4rem' }}
246247
alt={props.t(`visitedPlaces/activities/${nextDestination.activity}`)}
247248
/>

0 commit comments

Comments
 (0)