diff --git a/package.json b/package.json
index 7a5bb551..573b4b2e 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"@nextui-org/react": "^2.4.8",
"@nextui-org/skeleton": "^2.0.32",
"@nextui-org/snippet": "2.0.43",
+ "@nextui-org/spinner": "^2.0.34",
"@nextui-org/switch": "^2.0.34",
"@nextui-org/system": "2.2.6",
"@nextui-org/table": "^2.0.40",
@@ -58,6 +59,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-leaflet": "^4.2.1",
+ "react-leaflet-cluster": "^2.1.0",
"react-pdf": "^9.1.1",
"tailwind-variants": "0.1.20",
"uuid": "^11.0.3"
@@ -67,6 +69,7 @@
"@commitlint/config-conventional": "^19.5.0",
"@types/geojson": "^7946.0.14",
"@types/leaflet": "^1.9.14",
+ "@types/leaflet.markercluster": "^1.5.5",
"@types/node": "20.5.7",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
diff --git a/src/components/Accordions/Accordion.tsx b/src/components/Accordions/Accordion.tsx
index e3f3d6cb..41bbb59d 100644
--- a/src/components/Accordions/Accordion.tsx
+++ b/src/components/Accordions/Accordion.tsx
@@ -1,12 +1,13 @@
'use client';
import { Accordion, AccordionItem } from '@nextui-org/accordion';
+import { Spinner } from '@nextui-org/spinner';
import { AccordionsProps } from '@/domain/props/AccordionProps';
import { Tooltip } from '../Tooltip/Tooltip';
-export default function CustomAccordion({ items }: AccordionsProps) {
+export default function CustomAccordion({ items, loading = false }: AccordionsProps) {
return (
@@ -17,7 +18,10 @@ export default function CustomAccordion({ items }: AccordionsProps) {
className="last:border-b-[none] dark:bg-black white:bg-white overflow-x-auto"
title={
-
{item.title}
+
+ {item.title}
+ {loading && }
+
{item.tooltipInfo ? (
{item.iconSrc && }
diff --git a/src/components/Legend/LegendContainer.tsx b/src/components/Legend/LegendContainer.tsx
index fe162546..5333b755 100644
--- a/src/components/Legend/LegendContainer.tsx
+++ b/src/components/Legend/LegendContainer.tsx
@@ -5,10 +5,11 @@ import CustomAccordion from '../Accordions/Accordion';
import GradientLegend from './GradientLegend';
import PointLegend from './PointLegend';
-export default function LegendContainer({ items }: LegendContainerProps) {
+export default function LegendContainer({ items, loading = false }: LegendContainerProps) {
return (
({
title: item.title,
iconSrc: '/Images/InfoIcon.svg',
diff --git a/src/components/Map/Alerts/AlertContainer.tsx b/src/components/Map/Alerts/AlertContainer.tsx
new file mode 100644
index 00000000..257e398d
--- /dev/null
+++ b/src/components/Map/Alerts/AlertContainer.tsx
@@ -0,0 +1,15 @@
+import { useSidebar } from '@/domain/contexts/SidebarContext';
+import { AlertType } from '@/domain/enums/AlertType';
+
+import { ConflictLayer } from './ConflictLayer';
+
+export function AlertContainer() {
+ const { selectedAlert } = useSidebar();
+
+ switch (selectedAlert) {
+ case AlertType.CONFLICTS:
+ return ;
+ default:
+ return null; // TODO: hazard layers
+ }
+}
diff --git a/src/components/Map/Alerts/ConflictLayer.tsx b/src/components/Map/Alerts/ConflictLayer.tsx
new file mode 100644
index 00000000..bb56bcd3
--- /dev/null
+++ b/src/components/Map/Alerts/ConflictLayer.tsx
@@ -0,0 +1,50 @@
+import { useMemo } from 'react';
+import { CircleMarker } from 'react-leaflet';
+import MarkerClusterGroup from 'react-leaflet-cluster';
+
+import LegendContainer from '@/components/Legend/LegendContainer';
+import { MAP_MAX_ZOOM } from '@/domain/constant/Map';
+import { ConflictType } from '@/domain/enums/ConflictType';
+import { useConflictQuery } from '@/domain/hooks/alertHooks';
+import ConflictOperations from '@/operations/ConflictOperations';
+import GeometryOperations from '@/operations/GeometryOperations';
+import { getTailwindColor } from '@/utils/tailwind-util';
+
+export function ConflictLayer() {
+ const { data, isPending } = useConflictQuery();
+ const conflictsByType = useMemo(() => ConflictOperations.sortConflictsByType(data), [data]);
+
+ return (
+ <>
+
+
+
+ {!isPending &&
+ data &&
+ (Object.keys(conflictsByType) as ConflictType[]).map((conflictType) => (
+ ConflictOperations.createClusterCustomIcon(cluster, conflictType)}
+ showCoverageOnHover={false}
+ spiderLegPolylineOptions={{ weight: 0 }}
+ disableClusteringAtZoom={MAP_MAX_ZOOM}
+ zoomToBoundsOnClick={false}
+ maxClusterRadius={60}
+ spiderfyOnMaxZoom={false}
+ >
+ {conflictsByType[conflictType].map((marker) => (
+
+ ))}
+
+ ))}
+ >
+ );
+}
diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx
index 0fd45f99..dfb1eb65 100644
--- a/src/components/Map/Map.tsx
+++ b/src/components/Map/Map.tsx
@@ -2,8 +2,10 @@ import 'leaflet/dist/leaflet.css';
import { MapContainer, ZoomControl } from 'react-leaflet';
+import { MAP_MAX_ZOOM, MAP_MIN_ZOOM } from '@/domain/constant/Map';
import { MapProps } from '@/domain/props/MapProps';
+import { AlertContainer } from './Alerts/AlertContainer';
import VectorTileLayer from './VectorTileLayer';
export default function Map({ countries, disputedAreas }: MapProps) {
@@ -15,12 +17,13 @@ export default function Map({ countries, disputedAreas }: MapProps) {
[-90, -180],
[90, 180],
]}
- minZoom={3}
- maxZoom={8}
+ minZoom={MAP_MIN_ZOOM}
+ maxZoom={MAP_MAX_ZOOM}
maxBoundsViscosity={1.0}
zoomControl={false}
style={{ height: '100%', width: '100%', zIndex: 1 }}
>
+
{countries && }
diff --git a/src/domain/constant/Map.ts b/src/domain/constant/Map.ts
new file mode 100644
index 00000000..e74a01ad
--- /dev/null
+++ b/src/domain/constant/Map.ts
@@ -0,0 +1,2 @@
+export const MAP_MAX_ZOOM = 8;
+export const MAP_MIN_ZOOM = 3;
diff --git a/src/domain/entities/alerts/Conflict.ts b/src/domain/entities/alerts/Conflict.ts
index 1d153bbd..e9578f45 100644
--- a/src/domain/entities/alerts/Conflict.ts
+++ b/src/domain/entities/alerts/Conflict.ts
@@ -1,8 +1,13 @@
+import { LatLngExpression } from 'leaflet';
+
import { ConflictType } from '@/domain/enums/ConflictType';
import { Feature } from '../common/Feature';
-export type Conflict = Feature<{
- count: number;
- event_type: ConflictType;
-}>;
+export type Conflict = Feature<
+ {
+ count: number;
+ event_type: ConflictType;
+ },
+ LatLngExpression
+>;
diff --git a/src/domain/entities/alerts/ConflictTypeMap.ts b/src/domain/entities/alerts/ConflictTypeMap.ts
new file mode 100644
index 00000000..c900fee6
--- /dev/null
+++ b/src/domain/entities/alerts/ConflictTypeMap.ts
@@ -0,0 +1,5 @@
+import { ConflictType } from '@/domain/enums/ConflictType';
+
+import { Conflict } from './Conflict';
+
+export type ConflictTypeMap = { [K in ConflictType]: Conflict[] };
diff --git a/src/domain/entities/common/Feature.ts b/src/domain/entities/common/Feature.ts
index 496772ff..a2ccc48c 100644
--- a/src/domain/entities/common/Feature.ts
+++ b/src/domain/entities/common/Feature.ts
@@ -1,7 +1,9 @@
+import { LatLngExpression } from 'leaflet';
+
import { Geometry } from './Geometry';
-export interface Feature {
+export interface Feature {
type: string;
- geometry: Geometry;
+ geometry: Geometry;
properties: T;
}
diff --git a/src/domain/entities/common/Geometry.ts b/src/domain/entities/common/Geometry.ts
index 82e604d6..1079d5ba 100644
--- a/src/domain/entities/common/Geometry.ts
+++ b/src/domain/entities/common/Geometry.ts
@@ -1,6 +1,4 @@
-import { LatLngExpression } from 'leaflet';
-
-export interface Geometry {
+export interface Geometry {
type: string;
- coordinates: LatLngExpression[][][]; // Maybe a common type is not best idea here, the coordinate arrays seem to have different dephts.
+ coordinates: T;
}
diff --git a/src/domain/entities/country/CountryMimiData.ts b/src/domain/entities/country/CountryMimiData.ts
index 23f2ced8..36ff1444 100644
--- a/src/domain/entities/country/CountryMimiData.ts
+++ b/src/domain/entities/country/CountryMimiData.ts
@@ -1,3 +1,5 @@
+import { LatLngExpression } from 'leaflet';
+
import { Geometry } from '../common/Geometry';
import { RegionNutritionProperties } from '../region/RegionNutritionProperties';
@@ -10,7 +12,7 @@ export interface CountryMimiData {
};
features: {
type: string;
- geometry: Geometry;
+ geometry: Geometry;
properties: RegionNutritionProperties;
id: string;
}[];
diff --git a/src/domain/enums/AlertType.ts b/src/domain/enums/AlertType.ts
index 30f3396e..7057eb51 100644
--- a/src/domain/enums/AlertType.ts
+++ b/src/domain/enums/AlertType.ts
@@ -1,7 +1,10 @@
export enum AlertType {
HUNGER = 'hunger',
CONFLICTS = 'conflicts',
- CONFLICT1 = 'conflict1',
- CONFLICT2 = 'conflict2',
HAZARDS = 'hazards',
+ COVID19 = 'covid19',
+ FLOODS = 'floods',
+ DROUGHTS = 'droughts',
+ EARTHQUAKES = 'earthquakes',
+ CYCLONES = 'cyclones',
}
diff --git a/src/domain/props/AccordionProps.tsx b/src/domain/props/AccordionProps.tsx
index 13ed209b..a491a07e 100644
--- a/src/domain/props/AccordionProps.tsx
+++ b/src/domain/props/AccordionProps.tsx
@@ -2,4 +2,5 @@ import { AccordionItemProps } from '../entities/accordions/Accordions';
export interface AccordionsProps {
items: AccordionItemProps[];
+ loading?: boolean;
}
diff --git a/src/domain/props/LegendContainerProps.ts b/src/domain/props/LegendContainerProps.ts
index aa186d6e..2df6ae03 100644
--- a/src/domain/props/LegendContainerProps.ts
+++ b/src/domain/props/LegendContainerProps.ts
@@ -3,4 +3,5 @@ import PointLegendContainerItem from './PointLegendContainerItem';
export default interface LegendContainerProps {
items: (PointLegendContainerItem | GradientLegendContainerItem)[];
+ loading?: boolean;
}
diff --git a/src/operations/ConflictOperations.ts b/src/operations/ConflictOperations.ts
new file mode 100644
index 00000000..63f604af
--- /dev/null
+++ b/src/operations/ConflictOperations.ts
@@ -0,0 +1,72 @@
+import L, { MarkerCluster } from 'leaflet';
+
+import { Conflict } from '@/domain/entities/alerts/Conflict';
+import { ConflictTypeMap } from '@/domain/entities/alerts/ConflictTypeMap';
+import { ConflictType } from '@/domain/enums/ConflictType';
+import PointLegendContainerItem from '@/domain/props/PointLegendContainerItem';
+
+export default class ConflictOperations {
+ static getMarkerColor(conflictType: ConflictType): string {
+ switch (conflictType) {
+ case ConflictType.PROTESTS:
+ return 'conflictProtest';
+ case ConflictType.RIOTS:
+ return 'conflictRiot';
+ case ConflictType.BATTLES:
+ return 'conflictBattle';
+ case ConflictType.CIVIL_VIOLENCE:
+ return 'conflictCivil';
+ case ConflictType.EXPLOSIONS:
+ return 'conflictExplosion';
+ default:
+ return 'conflictStrategic';
+ }
+ }
+
+ static sortConflictsByType(data?: Conflict[]): ConflictTypeMap {
+ const result: ConflictTypeMap = {
+ [ConflictType.BATTLES]: [],
+ [ConflictType.PROTESTS]: [],
+ [ConflictType.RIOTS]: [],
+ [ConflictType.CIVIL_VIOLENCE]: [],
+ [ConflictType.EXPLOSIONS]: [],
+ [ConflictType.STRATEGIC]: [],
+ };
+ data?.forEach((conflict) => {
+ result[conflict.properties.event_type].push(conflict);
+ });
+ return result;
+ }
+
+ static createClusterCustomIcon(cluster: MarkerCluster, conflictType: ConflictType): L.DivIcon {
+ return L.divIcon({
+ html: `${cluster.getChildCount()}`,
+ className: '',
+ iconSize: L.point(40, 40, true),
+ });
+ }
+
+ static generateConflictLegend(): PointLegendContainerItem[] {
+ return [
+ {
+ title: 'Types of conflict',
+ tooltipInfo:
+ 'All reported violence and conflicts across Africa, the Middle East, South and South East Asia, Eastern and Southeastern Europe and the Balkans.',
+ records: [
+ { label: ConflictType.BATTLES, color: 'conflictBattle' },
+ { label: ConflictType.CIVIL_VIOLENCE, color: 'conflictCivil' },
+ { label: ConflictType.EXPLOSIONS, color: 'conflictExplosion' },
+ { label: ConflictType.RIOTS, color: 'conflictRiot' },
+ { label: ConflictType.PROTESTS, color: 'conflictProtest' },
+ { label: ConflictType.STRATEGIC, color: 'conflictStrategic' },
+ ],
+ },
+ ];
+ }
+}
diff --git a/src/operations/GeometryOperations.ts b/src/operations/GeometryOperations.ts
new file mode 100644
index 00000000..5344d544
--- /dev/null
+++ b/src/operations/GeometryOperations.ts
@@ -0,0 +1,15 @@
+import { LatLngExpression, LatLngTuple } from 'leaflet';
+
+export default class GeometryOperations {
+ static swapCoords(coords: LatLngExpression): LatLngExpression {
+ if (!GeometryOperations.isLatLngTuple(coords)) {
+ throw Error('Invlaid coordinate array');
+ }
+
+ return [coords[1], coords[0]];
+ }
+
+ static isLatLngTuple(value: LatLngExpression): value is LatLngTuple {
+ return Array.isArray(value) && value.length === 2 && typeof value[0] === 'number' && typeof value[1] === 'number';
+ }
+}
diff --git a/src/operations/sidebar/SidebarOperations.ts b/src/operations/sidebar/SidebarOperations.ts
index 1450186a..c43a9cfb 100644
--- a/src/operations/sidebar/SidebarOperations.ts
+++ b/src/operations/sidebar/SidebarOperations.ts
@@ -41,26 +41,41 @@ export class SidebarOperations {
icon: '/menu_fcs.png',
},
{
- key: AlertType.CONFLICTS,
- label: 'Conflicts',
- icon: '/menu_conflicts.png',
+ key: AlertType.HAZARDS,
+ label: 'Hazards',
+ icon: '/menu_hazards.png',
subalerts: [
{
- key: AlertType.CONFLICT1,
- label: 'Conflicts 1',
- icon: '/menu_conflicts.png',
+ key: AlertType.COVID19,
+ label: 'COVID-19',
+ icon: '/menu_hazards.png',
+ },
+ {
+ key: AlertType.FLOODS,
+ label: 'Floods',
+ icon: '/menu_hazards.png',
+ },
+ {
+ key: AlertType.DROUGHTS,
+ label: 'Droughts',
+ icon: '/menu_hazards.png',
+ },
+ {
+ key: AlertType.EARTHQUAKES,
+ label: 'Earthquakes',
+ icon: '/menu_hazards.png',
},
{
- key: AlertType.CONFLICT2,
- label: 'Conflicts 2',
- icon: '/menu_conflicts.png',
+ key: AlertType.CYCLONES,
+ label: 'Cyclones',
+ icon: '/menu_hazards.png',
},
],
},
{
- key: AlertType.HAZARDS,
- label: 'Hazards',
- icon: '/menu_hazards.png',
+ key: AlertType.CONFLICTS,
+ label: 'Conflicts',
+ icon: '/menu_conflicts.png',
},
];
diff --git a/src/utils/tailwind-util.ts b/src/utils/tailwind-util.ts
new file mode 100644
index 00000000..2ee0a864
--- /dev/null
+++ b/src/utils/tailwind-util.ts
@@ -0,0 +1,11 @@
+/**
+ * Converts a tailwind color variable to its hex format
+ * @param colorVariable a TailwindCSS color parameter
+ * @returns the color in hex format, as a string
+ */
+export const getTailwindColor = (colorVariable: string) => {
+ const [hue, saturation, lightness] = getComputedStyle(document.documentElement)
+ .getPropertyValue(colorVariable)
+ .split(' ');
+ return `hsl(${hue}, ${saturation}, ${lightness})`;
+};
diff --git a/tailwind.config.js b/tailwind.config.js
index 5bb214e4..3abfbee6 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -16,6 +16,12 @@ module.exports = {
'pl-[215px]',
'pl-[179px]',
'z-[9999]',
+ 'bg-conflictProtest',
+ 'bg-conflictRiot',
+ 'bg-conflictBattle',
+ 'bg-conflictCivil',
+ 'bg-conflictExplosion',
+ 'bg-conflictStrategic',
],
content: [
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
@@ -111,6 +117,12 @@ module.exports = {
surfaceGrey: '#B0B0B0',
activeCountries: '#82bce0',
inactiveCountries: '#a7b3ba',
+ conflictProtest: '#0d657de6',
+ conflictRiot: '#c95200e6',
+ conflictBattle: '#7d0631',
+ conflictCivil: '#96badc',
+ conflictExplosion: '#eaaf75',
+ conflictStrategic: '#bec0c1',
},
},
dark: {
@@ -140,6 +152,12 @@ module.exports = {
surfaceGrey: '#444444',
activeCountries: '#115884',
inactiveCountries: '#85929b',
+ conflictProtest: '#0d657de6',
+ conflictRiot: '#c95200e6',
+ conflictBattle: '#7d0631',
+ conflictCivil: '#96badc',
+ conflictExplosion: '#eaaf75',
+ conflictStrategic: '#bec0c1',
},
},
},
diff --git a/yarn.lock b/yarn.lock
index 240b595f..8508778d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1099,7 +1099,7 @@
"@nextui-org/shared-utils" "2.0.8"
"@nextui-org/system-rsc" "2.1.6"
-"@nextui-org/spinner@2.0.34":
+"@nextui-org/spinner@2.0.34", "@nextui-org/spinner@^2.0.34":
version "2.0.34"
resolved "https://registry.yarnpkg.com/@nextui-org/spinner/-/spinner-2.0.34.tgz#91f1d3db33fa4ceedbd88e00e7b1a10c7833c9b5"
integrity sha512-YKw/6xSLhsXU1k22OvYKyWhtJCHzW2bRAiieVSVG5xak3gYwknTds5H9s5uur+oAZVK9AkyAObD19QuZND32Jg==
@@ -2828,7 +2828,14 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
-"@types/leaflet@^1.9.14":
+"@types/leaflet.markercluster@^1.5.5":
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.5.tgz#e420caf5b9f6300ab4869fc877d08a9eaf200eec"
+ integrity sha512-TkWOhSHDM1ANxmLi+uK0PjsVcjIKBr8CLV2WoF16dIdeFmC0Cj5P5axkI3C1Xsi4+ht6EU8+BfEbbqEF9icPrg==
+ dependencies:
+ "@types/leaflet" "*"
+
+"@types/leaflet@*", "@types/leaflet@^1.9.14":
version "1.9.14"
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.14.tgz#f008fef281a05e457bbae9f00a82c836af1b9b03"
integrity sha512-sx2q6MDJaajwhKeVgPSvqXd8rhNJSTA3tMidQGduZn9S6WBYxDkCpSpV5xXEmSg7Cgdk/5vJGhVF1kMYLzauBg==
@@ -4967,6 +4974,11 @@ leaflet-geosearch@^4.0.0:
"@googlemaps/js-api-loader" "^1.16.6"
leaflet "^1.6.0"
+leaflet.markercluster@^1.5.3:
+ version "1.5.3"
+ resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz#9cdb52a4eab92671832e1ef9899669e80efc4056"
+ integrity sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==
+
leaflet@^1.6.0, leaflet@^1.9.4:
version "1.9.4"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
@@ -5737,6 +5749,13 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+react-leaflet-cluster@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/react-leaflet-cluster/-/react-leaflet-cluster-2.1.0.tgz#9e5299efb7b16eff75511a47ed4a5d763dcf55b5"
+ integrity sha512-16X7XQpRThQFC4PH4OpXHimGg19ouWmjxjtpxOeBKpvERSvIRqTx7fvhTwkEPNMFTQ8zTfddz6fRTUmUEQul7g==
+ dependencies:
+ leaflet.markercluster "^1.5.3"
+
react-leaflet@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-4.2.1.tgz#c300e9eccaf15cb40757552e181200aa10b94780"