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

conflict alerts display #27

Merged
merged 6 commits into from
Nov 13, 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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand All @@ -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",
Expand Down
8 changes: 6 additions & 2 deletions src/components/Accordions/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-full flex justify-start items-start max-w-[600px] overflow-visible overflow-x-auto p-2 rounded-lg">
<Accordion variant="splitted">
Expand All @@ -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={
<div className="flex justify-between items-center w-full">
<span>{item.title}</span>
<div className="flex gap-4">
<span>{item.title}</span>
{loading && <Spinner size="sm" />}
</div>
{item.tooltipInfo ? (
<Tooltip text={item.tooltipInfo}>
{item.iconSrc && <img src={item.iconSrc} alt="info icon" className="w-[37px] h-[37px] p-[5.5px]" />}
Expand Down
3 changes: 2 additions & 1 deletion src/components/Legend/LegendContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="w-[450px]">
<CustomAccordion
loading={loading}
items={items.map((item) => ({
title: item.title,
iconSrc: '/Images/InfoIcon.svg',
Expand Down
15 changes: 15 additions & 0 deletions src/components/Map/Alerts/AlertContainer.tsx
Original file line number Diff line number Diff line change
@@ -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 <ConflictLayer />;
default:
return null; // TODO: hazard layers
}
}
50 changes: 50 additions & 0 deletions src/components/Map/Alerts/ConflictLayer.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="absolute bottom-6 right-8 z-9999">
<LegendContainer loading={isPending || !data} items={ConflictOperations.generateConflictLegend()} />
</div>
{!isPending &&
data &&
(Object.keys(conflictsByType) as ConflictType[]).map((conflictType) => (
<MarkerClusterGroup
key={conflictType}
iconCreateFunction={(cluster) => ConflictOperations.createClusterCustomIcon(cluster, conflictType)}
showCoverageOnHover={false}
spiderLegPolylineOptions={{ weight: 0 }}
disableClusteringAtZoom={MAP_MAX_ZOOM}
zoomToBoundsOnClick={false}
maxClusterRadius={60}
spiderfyOnMaxZoom={false}
>
{conflictsByType[conflictType].map((marker) => (
<CircleMarker
radius={3}
color="white"
fillColor={getTailwindColor(`--nextui-${ConflictOperations.getMarkerColor(conflictType)}`)}
weight={1}
fillOpacity={1}
key={marker.geometry.coordinates.toString()}
center={GeometryOperations.swapCoords(marker.geometry.coordinates)}
/>
))}
</MarkerClusterGroup>
))}
</>
);
}
7 changes: 5 additions & 2 deletions src/components/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 }}
>
<AlertContainer />
{countries && <VectorTileLayer countries={countries} disputedAreas={disputedAreas} />}
<ZoomControl position="bottomright" />
</MapContainer>
Expand Down
2 changes: 2 additions & 0 deletions src/domain/constant/Map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MAP_MAX_ZOOM = 8;
export const MAP_MIN_ZOOM = 3;
13 changes: 9 additions & 4 deletions src/domain/entities/alerts/Conflict.ts
Original file line number Diff line number Diff line change
@@ -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
>;
5 changes: 5 additions & 0 deletions src/domain/entities/alerts/ConflictTypeMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ConflictType } from '@/domain/enums/ConflictType';

import { Conflict } from './Conflict';

export type ConflictTypeMap = { [K in ConflictType]: Conflict[] };
6 changes: 4 additions & 2 deletions src/domain/entities/common/Feature.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { LatLngExpression } from 'leaflet';

import { Geometry } from './Geometry';

export interface Feature<T> {
export interface Feature<T, U = LatLngExpression[][][]> {
type: string;
geometry: Geometry;
geometry: Geometry<U>;
properties: T;
}
6 changes: 2 additions & 4 deletions src/domain/entities/common/Geometry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { LatLngExpression } from 'leaflet';

export interface Geometry {
export interface Geometry<T> {
type: string;
coordinates: LatLngExpression[][][]; // Maybe a common type is not best idea here, the coordinate arrays seem to have different dephts.
coordinates: T;
}
4 changes: 3 additions & 1 deletion src/domain/entities/country/CountryMimiData.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LatLngExpression } from 'leaflet';

import { Geometry } from '../common/Geometry';
import { RegionNutritionProperties } from '../region/RegionNutritionProperties';

Expand All @@ -10,7 +12,7 @@ export interface CountryMimiData {
};
features: {
type: string;
geometry: Geometry;
geometry: Geometry<LatLngExpression[][][]>;
properties: RegionNutritionProperties;
id: string;
}[];
Expand Down
7 changes: 5 additions & 2 deletions src/domain/enums/AlertType.ts
Original file line number Diff line number Diff line change
@@ -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',
}
1 change: 1 addition & 0 deletions src/domain/props/AccordionProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { AccordionItemProps } from '../entities/accordions/Accordions';

export interface AccordionsProps {
items: AccordionItemProps[];
loading?: boolean;
}
1 change: 1 addition & 0 deletions src/domain/props/LegendContainerProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import PointLegendContainerItem from './PointLegendContainerItem';

export default interface LegendContainerProps {
items: (PointLegendContainerItem | GradientLegendContainerItem)[];
loading?: boolean;
}
72 changes: 72 additions & 0 deletions src/operations/ConflictOperations.ts
Original file line number Diff line number Diff line change
@@ -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: `<span
style="
width: ${Math.min(Math.floor(cluster.getChildCount() / 5) + 20, 40)}px;
height: ${Math.min(Math.floor(cluster.getChildCount() / 5) + 20, 40)}px;
"
class="bg-${ConflictOperations.getMarkerColor(conflictType)} flex items-center justify-center rounded-full border-white border-1 text-white font-bold"
>${cluster.getChildCount()}</span>`,
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' },
],
},
];
}
}
15 changes: 15 additions & 0 deletions src/operations/GeometryOperations.ts
Original file line number Diff line number Diff line change
@@ -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';
}
}
39 changes: 27 additions & 12 deletions src/operations/sidebar/SidebarOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
];

Expand Down
Loading
Loading