Skip to content

Commit

Permalink
Add map for learning
Browse files Browse the repository at this point in the history
  • Loading branch information
roshni73 committed Dec 12, 2024
1 parent 3071d1b commit 8ee38d7
Show file tree
Hide file tree
Showing 6 changed files with 398 additions and 15 deletions.
6 changes: 6 additions & 0 deletions app/src/views/OperationalLearning/LearningMap/i18n.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"namespace": "learningMap",
"strings": {
"learningDownloadMapTitle":"Operational learning"
}
}
194 changes: 194 additions & 0 deletions app/src/views/OperationalLearning/LearningMap/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {
useCallback,
useMemo,
useState,
} from 'react';
import {
Container,
TextOutput,
} from '@ifrc-go/ui';
import { useTranslation } from '@ifrc-go/ui/hooks';
import {
_cs,
isDefined,
isNotDefined,
} from '@togglecorp/fujs';
import {
MapLayer,
MapSource,
} from '@togglecorp/re-map';

import BaseMap from '#components/domain/BaseMap';
import Link from '#components/Link';
import MapContainerWithDisclaimer from '#components/MapContainerWithDisclaimer';
import MapPopup from '#components/MapPopup';
import useCountryRaw from '#hooks/domain/useCountryRaw';

import {
adminFillLayerOptions,
basePointLayerOptions,
outerCircleLayerOptionsForPersonnel,
} from './utils';

import i18n from './i18n.json';
import styles from './styles.module.css';

const sourceOptions: mapboxgl.GeoJSONSourceRaw = {
type: 'geojson',
};

interface CountryProperties {
country_id: number;
name: string;
units: number;
}

interface ClickedPoint {
feature: GeoJSON.Feature<GeoJSON.Point, CountryProperties>;
lngLat: mapboxgl.LngLatLike;
}

interface Props {
className?: string;
}

function OperationalLearningMap(props: Props) {
const { className } = props;

const strings = useTranslation(i18n);
const [
clickedPointProperties,
setClickedPointProperties] = useState<ClickedPoint | undefined>();

const learning_by_country = [
{ country_name: 'Afghanistan', country_id: 14, operation_count: 4 },
{ country_name: 'Albania', country_id: 15, operation_count: 1 },
{ country_name: 'Argentina', country_id: 20, operation_count: 1 },
{ country_name: 'Australia', country_id: 22, operation_count: 1 },
{ country_name: 'Belgium', country_id: 30, operation_count: 1 },
{ country_name: 'Canada', country_id: 42, operation_count: 1 },
];

const countryResponse = useCountryRaw();

const countryCentroidGeoJson = useMemo(
(): GeoJSON.FeatureCollection<GeoJSON.Geometry> => ({
type: 'FeatureCollection',
features: countryResponse
?.map((country) => {
if (
(!country.independent && isNotDefined(country.record_type))
|| isNotDefined(country.centroid)
|| isNotDefined(country.iso3)
) {
return undefined;
}

const learningList = learning_by_country.find(
(item) => item.country_id,
);
if (isNotDefined(learningList)) {
return undefined;
}

const units = learningList.operation_count ?? 0;

return {
type: 'Feature' as const,
geometry: country.centroid as {
type: 'Point',
coordinates: [number, number],
},
properties: {
id: country,
name: country.name,
units,
},
};
})
.filter(isDefined) ?? [],
}),
[],
);

const handleCountryClick = useCallback(
(feature: mapboxgl.MapboxGeoJSONFeature, lngLat: mapboxgl.LngLatLike) => {
setClickedPointProperties({
feature: feature as unknown as ClickedPoint['feature'],
lngLat,
});
return false;
},
[],
);

const handlePointClose = useCallback(() => {
setClickedPointProperties(undefined);
}, []);

return (
<Container className={_cs(styles.learningMap, className)}>
<BaseMap
baseLayers={(
<MapLayer
layerKey="admin-0"
hoverable
layerOptions={adminFillLayerOptions}
onClick={handleCountryClick}
/>
)}
>
<MapContainerWithDisclaimer
className={styles.mapContainer}
title={strings.learningDownloadMapTitle}
/>
<MapSource
sourceKey="points"
sourceOptions={sourceOptions}
geoJson={countryCentroidGeoJson}
>
<MapLayer
layerKey="point-circle"
layerOptions={basePointLayerOptions}
/>
<MapLayer
layerKey="outer-circle"
layerOptions={outerCircleLayerOptionsForPersonnel}
/>
</MapSource>
{clickedPointProperties?.lngLat && (
<MapPopup
onCloseButtonClick={handlePointClose}
coordinates={clickedPointProperties.lngLat}
heading={(
<Link
to="countriesLayout"
urlParams={{
countryId: clickedPointProperties.feature.properties.country_id,
}}
>
{clickedPointProperties.feature.properties.name}
</Link>
)}
childrenContainerClassName={styles.popupContent}
>
<Container
className={styles.popupEvent}
childrenContainerClassName={styles.popupEventDetail}
heading=""
headingLevel={5}
>
<TextOutput
value={clickedPointProperties.feature.properties.units}
description=""
valueType="number"
/>
</Container>
</MapPopup>
)}
</BaseMap>
</Container>
);
}

export default OperationalLearningMap;
28 changes: 28 additions & 0 deletions app/src/views/OperationalLearning/LearningMap/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.learning-map {
.map-container {
height: 50rem;
}

.footer {
display: flex;
align-items: flex-end;
flex-wrap: wrap;
background-color: var(--go-ui-color-background);
}
}

.popup-content {
display: flex;
flex-direction: column;
gap: var(--go-ui-spacing-md);

.popup-item {
gap: var(--go-ui-spacing-xs);

.popup-item-detail {
display: flex;
flex-direction: column;
font-size: var(--go-ui-font-size-sm);
}
}
}
161 changes: 161 additions & 0 deletions app/src/views/OperationalLearning/LearningMap/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import type {
CircleLayer,
CirclePaint,
FillLayer,
} from 'mapbox-gl';

import {
COLOR_BLACK,
COLOR_BLUE,
COLOR_DARK_GREY,
COLOR_LIGHT_GREY,
COLOR_RED,
COLOR_YELLOW,
} from '#utils/constants';

type i18nType = typeof import('./i18n.json');

const COLOR_ERU_AND_PERSONNEL = COLOR_BLUE;
const COLOR_ERU_ONLY = COLOR_RED;
const COLOR_PERSONNEL_ONLY = COLOR_YELLOW;
const COLOR_DEFAULT = COLOR_BLACK;

const SURGE_TYPE_ERU = 0;
const SURGE_TYPE_PERSONNEL = 1;
const SURGE_TYPE_ERU_AND_PERSONNEL = 2;

export const adminFillLayerOptions: Omit<FillLayer, 'id'> = {
type: 'fill',
paint: {
'fill-color': [
'case',
['boolean', ['feature-state', 'hovered'], false],
COLOR_DARK_GREY,
COLOR_LIGHT_GREY,
],
},
};

export function getLegendOptions(strings: i18nType['strings']) {
const legendOptions = [
{
value: SURGE_TYPE_ERU_AND_PERSONNEL,
label: strings.eruAndPersonnel,
color: COLOR_ERU_AND_PERSONNEL,
},
{
value: SURGE_TYPE_ERU,
label: strings.surgeEruOnly,
color: COLOR_ERU_ONLY,
},
{
value: SURGE_TYPE_PERSONNEL,
label: strings.surgePersonnelOnly,
color: COLOR_PERSONNEL_ONLY,
},
];

return legendOptions;
}

const circleColor: CirclePaint['circle-color'] = [
'case',
['all', ['>', ['get', 'units'], 0], ['>', ['get', 'personnel'], 0]],
COLOR_ERU_AND_PERSONNEL,
['>', ['get', 'units'], 0],
COLOR_ERU_ONLY,
['>', ['get', 'personnel'], 0],
COLOR_PERSONNEL_ONLY,
COLOR_DEFAULT,
];

const basePointPaint: CirclePaint = {
'circle-radius': 5,
'circle-color': circleColor,
'circle-opacity': 0.8,
};

export const basePointLayerOptions: Omit<CircleLayer, 'id'> = {
type: 'circle',
paint: basePointPaint,
};

const baseOuterCirclePaint: CirclePaint = {
'circle-color': circleColor,
'circle-opacity': 0.4,
};

const outerCirclePaintForEru: CirclePaint = {
...baseOuterCirclePaint,
'circle-radius': [
'interpolate',
['linear', 1],
['get', 'units'],
2,
5,
4,
7,
6,
9,
8,
11,
10,
13,
12,
15,
],
};

const outerCirclePaintForPersonnel: CirclePaint = {
...baseOuterCirclePaint,
'circle-radius': [
'interpolate',
['linear', 1],
['get', 'personnel'],

2,
5,
4,
7,
6,
9,
8,
11,
10,
13,
12,
15,
],
};

export const outerCircleLayerOptionsForEru: Omit<CircleLayer, 'id'> = {
type: 'circle',
paint: outerCirclePaintForEru,
};

export const outerCircleLayerOptionsForPersonnel: Omit<CircleLayer, 'id'> = {
type: 'circle',
paint: outerCirclePaintForPersonnel,
};

export interface ScaleOption {
label: string;
value: 'eru' | 'personnel';
}

export function getScaleOptions(strings: i18nType['strings']) {
const scaleOptions: ScaleOption[] = [
{ value: 'eru', label: strings.eruLabel },
{ value: 'personnel', label: strings.personnelLabel },
];

return scaleOptions;
}

export function optionKeySelector(option: ScaleOption) {
return option.value;
}

export function optionLabelSelector(option: ScaleOption) {
return option.label;
}
Loading

0 comments on commit 8ee38d7

Please sign in to comment.