From 530f64cb8f9d589d18ebc05ddeef9b0e15cb7d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1muel=20Fekete?= Date: Mon, 11 Nov 2024 13:44:54 +0100 Subject: [PATCH 1/5] feat: old version --- package.json | 2 + src/components/Map/ConflictLayer.tsx | 59 ++++++++++++++++++++++ src/components/Map/Map.tsx | 12 ++++- src/domain/entities/alerts/Conflict.ts | 13 +++-- src/domain/entities/common/Feature.ts | 6 ++- src/domain/entities/common/Geometry.ts | 6 +-- src/domain/enums/AlertType.ts | 7 ++- src/operations/ConflictOperations.ts | 36 +++++++++++++ src/operations/GeometryOperations.ts | 15 ++++++ src/operations/charts/SidebarOperations.ts | 39 +++++++++----- yarn.lock | 21 +++++++- 11 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 src/components/Map/ConflictLayer.tsx create mode 100644 src/operations/ConflictOperations.ts create mode 100644 src/operations/GeometryOperations.ts diff --git a/package.json b/package.json index 886dbd79..ba29c264 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,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" }, @@ -61,6 +62,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/Map/ConflictLayer.tsx b/src/components/Map/ConflictLayer.tsx new file mode 100644 index 00000000..d3ef33fa --- /dev/null +++ b/src/components/Map/ConflictLayer.tsx @@ -0,0 +1,59 @@ +import L, { MarkerCluster } from 'leaflet'; +import { useMemo } from 'react'; +import { CircleMarker } from 'react-leaflet'; +import MarkerClusterGroup from 'react-leaflet-cluster'; + +import { ConflictType } from '@/domain/enums/ConflictType'; +import { useConflictQuery } from '@/domain/hooks/alertHooks'; +import ConflictOperations from '@/operations/ConflictOperations'; +import GeometryOperations from '@/operations/GeometryOperations'; + +export function ConflictLayer({ maxZoom }: { maxZoom: number }) { + const { data, isPending } = useConflictQuery(); + const conflictsByType = useMemo(() => ConflictOperations.sortConflictsByType(data), [data]); + + const createClusterCustomIcon = (cluster: MarkerCluster, conflictType: ConflictType) => { + return L.divIcon({ + html: `${cluster.getChildCount()}`, + className: '', + iconSize: L.point(40, 40, true), + }); + }; + if (isPending || !data) return null; + + return ( + <> + {(Object.keys(conflictsByType) as ConflictType[]).map((conflictType) => ( + createClusterCustomIcon(c, conflictType)} + showCoverageOnHover={false} + spiderLegPolylineOptions={{ weight: 0 }} + disableClusteringAtZoom={maxZoom} + zoomToBoundsOnClick={false} + maxClusterRadius={60} + spiderfyOnMaxZoom={false} + > + {conflictsByType[conflictType].map((m) => ( + + ))} + + ))} + + ); +} diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 769f7461..07235cc1 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -1,13 +1,20 @@ import 'leaflet/dist/leaflet.css'; import { Feature, FeatureCollection } from 'geojson'; -import { LeafletMouseEvent } from 'leaflet'; +import L, { LeafletMouseEvent } from 'leaflet'; import { GeoJSON, MapContainer, TileLayer, ZoomControl } from 'react-leaflet'; +import { useSidebar } from '@/domain/contexts/SidebarContext'; import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts'; +import { AlertType } from '@/domain/enums/AlertType'; import { MapProps } from '@/domain/props/MapProps'; +import { ConflictLayer } from './ConflictLayer'; + +const MAX_ZOOM = 8; + export default function Map({ countries }: MapProps) { + const { selectedAlert } = useSidebar(); const countryStyle: L.PathOptions = { fillColor: 'var(--color-active-countries)', weight: 0.5, @@ -75,7 +82,7 @@ export default function Map({ countries }: MapProps) { [90, 180], ]} minZoom={3} - maxZoom={8} + maxZoom={MAX_ZOOM} maxBoundsViscosity={1.0} zoomControl={false} style={{ height: '100%', width: '100%', zIndex: 40 }} @@ -84,6 +91,7 @@ export default function Map({ countries }: MapProps) { attribution='© OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> + {selectedAlert === AlertType.CONFLICTS && } {countries && } 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/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/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/operations/ConflictOperations.ts b/src/operations/ConflictOperations.ts new file mode 100644 index 00000000..0ee882d4 --- /dev/null +++ b/src/operations/ConflictOperations.ts @@ -0,0 +1,36 @@ +import { Conflict } from '@/domain/entities/alerts/Conflict'; +import { ConflictType } from '@/domain/enums/ConflictType'; + +export default class ConflictOperations { + static getMarkerColor(conflictType: ConflictType): string { + switch (conflictType) { + case ConflictType.PROTESTS: + return '#0d657de6'; + case ConflictType.RIOTS: + return '#c95200e6'; + case ConflictType.BATTLES: + return '#7d0631'; + case ConflictType.CIVIL_VIOLENCE: + return '#96badc'; + case ConflictType.EXPLOSIONS: + return '#eaaf75'; + default: + return '#bec0c1'; + } + } + + static sortConflictsByType(data?: Conflict[]) { + const result: { [K in ConflictType]: Conflict[] } = { + [ConflictType.BATTLES]: [], + [ConflictType.PROTESTS]: [], + [ConflictType.RIOTS]: [], + [ConflictType.CIVIL_VIOLENCE]: [], + [ConflictType.EXPLOSIONS]: [], + [ConflictType.STRATEGIC]: [], + }; + data?.forEach((c) => { + result[c.properties.event_type].push(c); + }); + return result; + } +} 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/charts/SidebarOperations.ts b/src/operations/charts/SidebarOperations.ts index 1450186a..c43a9cfb 100644 --- a/src/operations/charts/SidebarOperations.ts +++ b/src/operations/charts/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/yarn.lock b/yarn.lock index 0e34c1e0..a0830dd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2784,7 +2784,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== @@ -4857,6 +4864,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" @@ -5560,6 +5572,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" From 4e28860be5957d911c997bf4bd512f075e1424a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1muel=20Fekete?= Date: Mon, 11 Nov 2024 18:02:20 +0100 Subject: [PATCH 2/5] fix: use predefined colors --- src/components/Map/ConflictLayer.tsx | 6 +++--- src/operations/ConflictOperations.ts | 12 ++++++------ src/utils/tailwind-util.ts | 11 +++++++++++ tailwind.config.js | 18 ++++++++++++++++++ 4 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 src/utils/tailwind-util.ts diff --git a/src/components/Map/ConflictLayer.tsx b/src/components/Map/ConflictLayer.tsx index d3ef33fa..7ef00df9 100644 --- a/src/components/Map/ConflictLayer.tsx +++ b/src/components/Map/ConflictLayer.tsx @@ -7,6 +7,7 @@ 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({ maxZoom }: { maxZoom: number }) { const { data, isPending } = useConflictQuery(); @@ -16,11 +17,10 @@ export function ConflictLayer({ maxZoom }: { maxZoom: number }) { return L.divIcon({ html: `${cluster.getChildCount()}`, className: '', iconSize: L.point(40, 40, true), @@ -45,8 +45,8 @@ export function ConflictLayer({ maxZoom }: { maxZoom: number }) { { + 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', }, }, }, From 38e35b8306d0cac65c1a9e996585f2eb6f0754a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1muel=20Fekete?= Date: Tue, 12 Nov 2024 16:00:00 +0100 Subject: [PATCH 3/5] fix: add type param to generic type --- src/domain/entities/country/CountryMimiData.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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; }[]; From 5237a298d4177752b0a2b302585dd1b101ebbfd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1muel=20Fekete?= Date: Tue, 12 Nov 2024 19:52:38 +0100 Subject: [PATCH 4/5] fix: requested code style changes --- src/components/Map/Alerts/AlertContainer.tsx | 15 +++++++++++ .../Map/{ => Alerts}/ConflictLayer.tsx | 27 +++++-------------- src/components/Map/Map.tsx | 14 ++++------ src/domain/constant/Map.ts | 2 ++ src/domain/entities/alerts/ConflictTypeMap.ts | 5 ++++ src/operations/ConflictOperations.ts | 25 ++++++++++++++--- 6 files changed, 55 insertions(+), 33 deletions(-) create mode 100644 src/components/Map/Alerts/AlertContainer.tsx rename src/components/Map/{ => Alerts}/ConflictLayer.tsx (55%) create mode 100644 src/domain/constant/Map.ts create mode 100644 src/domain/entities/alerts/ConflictTypeMap.ts 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/ConflictLayer.tsx b/src/components/Map/Alerts/ConflictLayer.tsx similarity index 55% rename from src/components/Map/ConflictLayer.tsx rename to src/components/Map/Alerts/ConflictLayer.tsx index 7ef00df9..84a5f530 100644 --- a/src/components/Map/ConflictLayer.tsx +++ b/src/components/Map/Alerts/ConflictLayer.tsx @@ -1,31 +1,18 @@ -import L, { MarkerCluster } from 'leaflet'; import { useMemo } from 'react'; import { CircleMarker } from 'react-leaflet'; import MarkerClusterGroup from 'react-leaflet-cluster'; +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({ maxZoom }: { maxZoom: number }) { +export function ConflictLayer() { const { data, isPending } = useConflictQuery(); const conflictsByType = useMemo(() => ConflictOperations.sortConflictsByType(data), [data]); - const createClusterCustomIcon = (cluster: MarkerCluster, conflictType: ConflictType) => { - return L.divIcon({ - html: `${cluster.getChildCount()}`, - className: '', - iconSize: L.point(40, 40, true), - }); - }; if (isPending || !data) return null; return ( @@ -33,23 +20,23 @@ export function ConflictLayer({ maxZoom }: { maxZoom: number }) { {(Object.keys(conflictsByType) as ConflictType[]).map((conflictType) => ( createClusterCustomIcon(c, conflictType)} + iconCreateFunction={(cluster) => ConflictOperations.createClusterCustomIcon(cluster, conflictType)} showCoverageOnHover={false} spiderLegPolylineOptions={{ weight: 0 }} - disableClusteringAtZoom={maxZoom} + disableClusteringAtZoom={MAP_MAX_ZOOM} zoomToBoundsOnClick={false} maxClusterRadius={60} spiderfyOnMaxZoom={false} > - {conflictsByType[conflictType].map((m) => ( + {conflictsByType[conflictType].map((marker) => ( ))} diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 07235cc1..03add733 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -4,17 +4,13 @@ import { Feature, FeatureCollection } from 'geojson'; import L, { LeafletMouseEvent } from 'leaflet'; import { GeoJSON, MapContainer, TileLayer, ZoomControl } from 'react-leaflet'; -import { useSidebar } from '@/domain/contexts/SidebarContext'; +import { MAP_MAX_ZOOM, MAP_MIN_ZOOM } from '@/domain/constant/Map'; import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts'; -import { AlertType } from '@/domain/enums/AlertType'; import { MapProps } from '@/domain/props/MapProps'; -import { ConflictLayer } from './ConflictLayer'; - -const MAX_ZOOM = 8; +import { AlertContainer } from './Alerts/AlertContainer'; export default function Map({ countries }: MapProps) { - const { selectedAlert } = useSidebar(); const countryStyle: L.PathOptions = { fillColor: 'var(--color-active-countries)', weight: 0.5, @@ -81,8 +77,8 @@ export default function Map({ countries }: MapProps) { [-90, -180], [90, 180], ]} - minZoom={3} - maxZoom={MAX_ZOOM} + minZoom={MAP_MIN_ZOOM} + maxZoom={MAP_MAX_ZOOM} maxBoundsViscosity={1.0} zoomControl={false} style={{ height: '100%', width: '100%', zIndex: 40 }} @@ -91,7 +87,7 @@ export default function Map({ countries }: MapProps) { attribution='© OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - {selectedAlert === AlertType.CONFLICTS && } + {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/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/operations/ConflictOperations.ts b/src/operations/ConflictOperations.ts index 924db31d..1bffa4cd 100644 --- a/src/operations/ConflictOperations.ts +++ b/src/operations/ConflictOperations.ts @@ -1,4 +1,7 @@ +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'; export default class ConflictOperations { @@ -19,8 +22,8 @@ export default class ConflictOperations { } } - static sortConflictsByType(data?: Conflict[]) { - const result: { [K in ConflictType]: Conflict[] } = { + static sortConflictsByType(data?: Conflict[]): ConflictTypeMap { + const result: ConflictTypeMap = { [ConflictType.BATTLES]: [], [ConflictType.PROTESTS]: [], [ConflictType.RIOTS]: [], @@ -28,9 +31,23 @@ export default class ConflictOperations { [ConflictType.EXPLOSIONS]: [], [ConflictType.STRATEGIC]: [], }; - data?.forEach((c) => { - result[c.properties.event_type].push(c); + 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), + }); + } } From 40915996e0e0c63c92b09922c96320dd001a8bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1muel=20Fekete?= Date: Tue, 12 Nov 2024 20:32:11 +0100 Subject: [PATCH 5/5] feat: add legend to conflict layer --- package.json | 1 + src/components/Accordions/Accordion.tsx | 8 ++- src/components/Legend/LegendContainer.tsx | 3 +- src/components/Map/Alerts/ConflictLayer.tsx | 56 +++++++++++---------- src/domain/props/AccordionProps.tsx | 1 + src/domain/props/LegendContainerProps.ts | 1 + src/operations/ConflictOperations.ts | 19 +++++++ yarn.lock | 2 +- 8 files changed, 61 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index a31df6cd..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", 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 && info icon} 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/ConflictLayer.tsx b/src/components/Map/Alerts/ConflictLayer.tsx index 84a5f530..bb56bcd3 100644 --- a/src/components/Map/Alerts/ConflictLayer.tsx +++ b/src/components/Map/Alerts/ConflictLayer.tsx @@ -2,6 +2,7 @@ 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'; @@ -13,34 +14,37 @@ export function ConflictLayer() { const { data, isPending } = useConflictQuery(); const conflictsByType = useMemo(() => ConflictOperations.sortConflictsByType(data), [data]); - if (isPending || !data) return null; - return ( <> - {(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) => ( - - ))} - - ))} +
+ +
+ {!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/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 index 1bffa4cd..63f604af 100644 --- a/src/operations/ConflictOperations.ts +++ b/src/operations/ConflictOperations.ts @@ -3,6 +3,7 @@ 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 { @@ -50,4 +51,22 @@ export default class ConflictOperations { 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/yarn.lock b/yarn.lock index 5c784c85..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==