diff --git a/interfaces/IBF-dashboard/src/app/components/aggregates/aggregates.component.html b/interfaces/IBF-dashboard/src/app/components/aggregates/aggregates.component.html
index f5d12ad83..296d10ec5 100644
--- a/interfaces/IBF-dashboard/src/app/components/aggregates/aggregates.component.html
+++ b/interfaces/IBF-dashboard/src/app/components/aggregates/aggregates.component.html
@@ -85,6 +85,7 @@
) | percent: '.0-0'
}}
+ {{ indicator.aggregateUnit }}
diff --git a/interfaces/IBF-dashboard/src/app/components/map/map.component.ts b/interfaces/IBF-dashboard/src/app/components/map/map.component.ts
index 7eea5f782..a049f1457 100644
--- a/interfaces/IBF-dashboard/src/app/components/map/map.component.ts
+++ b/interfaces/IBF-dashboard/src/app/components/map/map.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnDestroy } from '@angular/core';
+import { AfterViewInit, Component, OnDestroy } from '@angular/core';
import { LeafletControlLayersConfig } from '@asymmetrik/ngx-leaflet';
import bbox from '@turf/bbox';
import { containsNumber } from '@turf/invariant';
@@ -46,7 +46,8 @@ import {
} from 'src/app/models/poi.model';
import { ApiService } from 'src/app/services/api.service';
import { CountryService } from 'src/app/services/country.service';
-import { EventService } from 'src/app/services/event.service';
+import { EventService, EventSummary } from 'src/app/services/event.service';
+import { MapLegendService } from 'src/app/services/map-legend.service';
import { MapService } from 'src/app/services/map.service';
import { PlaceCodeService } from 'src/app/services/place-code.service';
import { TimelineService } from 'src/app/services/timeline.service';
@@ -56,15 +57,14 @@ import {
IbfLayerGroup,
IbfLayerName,
IbfLayerType,
- IbfLayerWMS,
LeafletPane,
} from 'src/app/types/ibf-layer';
import { NumberFormat } from 'src/app/types/indicator-group';
import { LeadTime } from 'src/app/types/lead-time';
-import { breakKey } from '../../models/map.model';
import { PlaceCode } from '../../models/place-code.model';
import { DisasterTypeService } from '../../services/disaster-type.service';
import { PointMarkerService } from '../../services/point-marker.service';
+import { DisasterTypeKey } from '../../types/disaster-type-key';
import { TimelineState } from '../../types/timeline-state';
import { IbfLayerThreshold } from './../../types/ibf-layer';
@@ -73,7 +73,7 @@ import { IbfLayerThreshold } from './../../types/ibf-layer';
templateUrl: './map.component.html',
styleUrls: ['./map.component.scss'],
})
-export class MapComponent implements OnDestroy {
+export class MapComponent implements AfterViewInit, OnDestroy {
private map: Map;
public layers: IbfLayer[] = [];
private placeCode: string;
@@ -84,7 +84,8 @@ export class MapComponent implements OnDestroy {
public timelineState: TimelineState;
private closestPointToTyphoon: number;
- public legends: { [key: string]: Control } = {};
+ public legend: Control;
+ private legendDiv: HTMLElement;
private layerSubscription: Subscription;
private countrySubscription: Subscription;
@@ -118,6 +119,7 @@ export class MapComponent implements OnDestroy {
private analyticsService: AnalyticsService,
private apiService: ApiService,
private pointMarkerService: PointMarkerService,
+ private mapLegendService: MapLegendService,
) {
this.layerSubscription = this.mapService
.getLayerSubscription()
@@ -147,6 +149,11 @@ export class MapComponent implements OnDestroy {
.getTimelineStateSubscription()
.subscribe(this.onTimelineStateChange);
}
+ ngAfterViewInit(): void {
+ if (this.map) {
+ this.initLegend();
+ }
+ }
ngOnDestroy() {
this.layerSubscription.unsubscribe();
@@ -158,23 +165,12 @@ export class MapComponent implements OnDestroy {
this.timelineStateSubscription.unsubscribe();
}
- private filterLayerByLayerName = (newLayer) => (layer) =>
- layer.name === newLayer.name;
-
private onLayerChange = (newLayer) => {
if (newLayer) {
- const newLayerIndex = this.layers.findIndex(
- this.filterLayerByLayerName(newLayer),
- );
newLayer =
newLayer.data || newLayer.type === IbfLayerType.wms
? this.createLayer(newLayer)
: newLayer;
- if (newLayerIndex >= 0) {
- this.layers.splice(newLayerIndex, 1, newLayer);
- } else {
- this.layers.push(newLayer);
- }
if (newLayer.viewCenter) {
this.map.fitBounds(this.mapService.state.bounds);
@@ -185,6 +181,8 @@ export class MapComponent implements OnDestroy {
this.addToLayersControl();
this.triggerWindowResize();
+
+ this.updateLegend();
};
private onCountryChange = (country: Country) => {
@@ -278,110 +276,110 @@ export class MapComponent implements OnDestroy {
} else if (layer.numberFormatMap === NumberFormat.decimal2) {
return (Math.round(d * 100) / 100).toLocaleString();
} else if (layer.numberFormatMap === NumberFormat.decimal0) {
- return Math.round(d).toLocaleString();
- } else {
- return Math.round(d).toLocaleString();
- }
- }
-
- private getFeatureColorByColorsAndColorThresholds = (
- colors,
- colorThreshold,
- ) => (feature) => {
- return feature <= colorThreshold[breakKey.break1] ||
- !colorThreshold[breakKey.break1]
- ? colors[0]
- : feature <= colorThreshold[breakKey.break2] ||
- !colorThreshold[breakKey.break2]
- ? colors[1]
- : feature <= colorThreshold[breakKey.break3] ||
- !colorThreshold[breakKey.break3]
- ? colors[2]
- : feature <= colorThreshold[breakKey.break4] ||
- !colorThreshold[breakKey.break4]
- ? colors[3]
- : colors[4];
- };
-
- private getLabel = (grades, layer, labels) => (i) => {
- const label = labels ? ' - ' + labels[i] : '';
- if (layer.colorBreaks) {
- const valueLow = layer.colorBreaks && layer.colorBreaks[i + 1]?.valueLow;
- const valueHigh =
- layer.colorBreaks && layer.colorBreaks[i + 1]?.valueHigh;
- if (valueLow === valueHigh) {
- return this.numberFormat(valueHigh, layer) + label + '
';
+ if (d > 10000000) {
+ return Math.round(d / 1000000).toLocaleString() + 'M';
+ } else if (d > 1000000) {
+ return (Math.round(d / 100000) / 10).toLocaleString() + 'M';
+ } else if (d > 10000) {
+ return Math.round(d / 1000).toLocaleString() + 'k';
+ } else if (d > 1000) {
+ return (Math.round(d / 100) / 10).toLocaleString() + 'k';
} else {
- return (
- this.numberFormat(valueLow, layer) +
- '–' +
- this.numberFormat(valueHigh, layer) +
- label +
- '
'
- );
+ return Math.round(d).toLocaleString();
}
} else {
- const number1 = this.numberFormat(grades[i], layer);
- const number2 = this.numberFormat(grades[i + 1], layer);
- return (
- number1 +
- (typeof grades[i + 1] !== 'undefined' ? '–' + number2 : '+') +
- label +
- '
'
- );
+ return Math.round(d).toLocaleString();
}
- };
+ }
- public addLegend(colors, colorThreshold, layer: IbfLayer) {
- this.removeLegend();
-
- this.legends[layer.name] = new Control();
- this.legends[layer.name].setPosition('bottomleft');
- this.legends[layer.name].onAdd = () => {
- const div = DomUtil.create('div', 'info legend');
- const grades = Object.values(colorThreshold);
- let labels;
- if (layer.colorBreaks) {
- labels = Object.values(layer.colorBreaks).map(
- (colorBreak) => colorBreak.label,
- );
- }
- const getColor = this.getFeatureColorByColorsAndColorThresholds(
- colors,
- colorThreshold,
- );
+ private initLegend() {
+ this.legend = new Control();
+ this.legend.setPosition('bottomleft');
+ this.legend.onAdd = () => {
+ this.legendDiv = DomUtil.create('div', 'info legend');
+ this.legendDiv.innerHTML += this.mapLegendService.getLegendTitle();
+ return this.legendDiv;
+ };
- const getLabel = this.getLabel(grades, layer, labels);
+ this.legend.addTo(this.map);
+ }
- div.innerHTML +=
- `
${layer.label}
`); + break; } - for (let i = 0; i < grades.length; i++) { - if (grades[i] !== null && (i === 0 || grades[i] > grades[i - 1])) { - div.innerHTML += ` ${getLabel(i)}`; - } + for (const element of elements) { + this.legendDiv.innerHTML += element; } + } + } - return div; - }; - this.legends[layer.name].addTo(this.map); + private isMultiLinePointLayer(layerName: IbfLayerName): boolean { + return ( + [ + IbfLayerName.waterpointsInternal, + IbfLayerName.healthSites, + IbfLayerName.schools, + ].includes(layerName) && + this.disasterType.disasterType === DisasterTypeKey.flashFloods && + this.eventState.event?.activeTrigger + ); } - private removeLegend() { - for (const legend of Object.keys(this.legends)) { - this.map.removeControl(this.legends[legend]); - } - this.legends = {}; + private getGlofasStationStates() { + return Object.keys( + this.country?.countryDisasterSettings.find( + (s) => s.disasterType === this.disasterType?.disasterType, + )?.eapAlertClasses, + ); } onMapReady(map: Map) { @@ -389,18 +387,26 @@ export class MapComponent implements OnDestroy { this.map.createPane(LeafletPane.wmsPane); this.map.createPane(LeafletPane.adminBoundaryPane); this.map.createPane(LeafletPane.outline); - this.map.getPane(LeafletPane.outline).style.zIndex = '570'; this.map.createPane(LeafletPane.aggregatePane); this.triggerWindowResize(); } private createLayer(layer: IbfLayer): IbfLayer { + this.layers = this.layers.filter((l) => l.name !== layer.name); + if (layer.type === IbfLayerType.point) { - layer.leafletLayer = this.createPointLayer(layer); + const pointLayers = this.createPointLayer(layer); + + for (const pointLayer of pointLayers) { + const extraLayer = { ...layer }; + extraLayer.leafletLayer = pointLayer; + this.layers.push(extraLayer); + } } if (layer.type === IbfLayerType.shape) { layer.leafletLayer = this.createAdminRegionsLayer(layer); + this.layers.push(layer); const colors = this.eventState?.activeTrigger && this.eventState?.thresholdReached @@ -411,26 +417,11 @@ export class MapComponent implements OnDestroy { layer.colorProperty, layer.colorBreaks, ); - - if ( - this.layers.filter( - (l) => l.group === IbfLayerGroup.aggregates && l.active, - ).length === 0 - ) { - this.removeLegend(); - } - - if ( - layer.group !== IbfLayerGroup.adminRegions && - layer.group !== IbfLayerGroup.outline && - layer.active - ) { - this.addLegend(colors, colorThreshold, layer); - } } if (layer.type === IbfLayerType.wms) { - layer.leafletLayer = this.createWmsLayer(layer.wms); + layer.leafletLayer = this.createWmsLayer(layer); + this.layers.push(layer); } return layer; @@ -545,7 +536,7 @@ export class MapComponent implements OnDestroy { }); }; - private createPointLayer(layer: IbfLayer): GeoJSON | MarkerClusterGroup { + private createPointLayer(layer: IbfLayer): GeoJSON[] | MarkerClusterGroup[] { if (!layer.data) { return; } @@ -562,12 +553,36 @@ export class MapComponent implements OnDestroy { layer.name, ) ) { - const waterPointClusterLayer = markerClusterGroup({ + // construct exposed marker clusters + const exposedWaterPointClusterLayer = markerClusterGroup({ + iconCreateFunction: this.getIconCreateFunction, + maxClusterRadius: 50, + }); + const exposedLayerData = JSON.parse(JSON.stringify(layer.data)); + exposedLayerData.features = exposedLayerData.features.filter( + (f) => f.properties.exposed, + ); + const mapLayerExposed = geoJSON(exposedLayerData, { + pointToLayer: this.getPointToLayerByLayer(layer.name), + }); + exposedWaterPointClusterLayer.addLayer(mapLayerExposed); + + // construct not-exposed marker clusters + const notExposedWaterPointClusterLayer = markerClusterGroup({ iconCreateFunction: this.getIconCreateFunction, maxClusterRadius: 50, }); - waterPointClusterLayer.addLayer(mapLayer); - return waterPointClusterLayer; + const nonExposedLayerData = JSON.parse(JSON.stringify(layer.data)); + nonExposedLayerData.features = nonExposedLayerData.features.filter( + (f) => !f.properties.exposed, + ); + const mapLayerNotExposed = geoJSON(nonExposedLayerData, { + pointToLayer: this.getPointToLayerByLayer(layer.name), + }); + notExposedWaterPointClusterLayer.addLayer(mapLayerNotExposed); + + // return both + return [exposedWaterPointClusterLayer, notExposedWaterPointClusterLayer]; } if ( @@ -579,9 +594,9 @@ export class MapComponent implements OnDestroy { maxClusterRadius: 10, }); healthSiteClusterLayer.addLayer(mapLayer); - return healthSiteClusterLayer; + return [healthSiteClusterLayer]; } - return mapLayer; + return [mapLayer]; } private onAdminRegionMouseOver = (feature) => (event): void => { @@ -809,21 +824,42 @@ export class MapComponent implements OnDestroy { return adminRegionsLayer; } - private createWmsLayer(layerWMS: IbfLayerWMS): Layer { - if (!layerWMS) { + private createWmsLayer(layer: IbfLayer): Layer { + if (!layer.wms) { return; } + const layerNames = []; + const events = this.eventState.event + ? [this.eventState.event] + : this.eventState.events.length > 0 + ? this.eventState.events + : [new EventSummary()]; + + for (const event of events) { + const leadTime = !layer.wms.leadTimeDependent + ? null + : !this.eventState.event && event.firstLeadTime + ? event.firstLeadTime + : this.timelineState.activeLeadTime; + + const name = `ibf-system:${layer.name}_${leadTime ? `${leadTime}_` : ''}${ + this.country.countryCodeISO3 + }`; + if (!layerNames.includes(name)) { + layerNames.push(name); + } + } const wmsOptions = { pane: LeafletPane.wmsPane, - layers: layerWMS.name, - format: layerWMS.format, - version: layerWMS.version, - attribution: layerWMS.attribution, - crs: layerWMS.crs, - transparent: layerWMS.transparent, - viewparams: layerWMS.viewparams, + layers: layerNames.join(','), + format: layer.wms.format, + version: layer.wms.version, + attribution: layer.wms.attribution, + crs: layer.wms.crs, + transparent: layer.wms.transparent, + viewparams: layer.wms.viewparams, }; - return tileLayer.wms(layerWMS.url, wmsOptions); + return tileLayer.wms(layer.wms.url, wmsOptions); } private calculateClosestPointToTyphoon(layer: IbfLayer) { @@ -837,4 +873,7 @@ export class MapComponent implements OnDestroy { this.closestPointToTyphoon = Math.max.apply(null, dates); } + + private sortLayers = (a: IbfLayer, b: IbfLayer) => + a.order > b.order ? 1 : a.order === b.order ? 0 : -1; } diff --git a/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.html b/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.html index a930c03cb..8cc79e382 100644 --- a/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.html +++ b/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.html @@ -20,6 +20,7 @@ (ionDidOpen)="isLayerControlMenuOpen()" (ionDidClose)="isLayerControlMenuOpen()" data-test="layers-control-menu" + swipeGesture="false" >The number of people affected is calculated based on the predicted number of completely damaged houses, which is derived from the typhoon impact predicting model. To derive a methodology to estimate the number of affected people from a predicted number of completely damaged houses, we performed a log fit between the number of completely damaged houses and the number of affected people for past typhoon events using data derived from DROMIC reports. To estimate potential number of affected population this formula is applied to the predicted number of damaged houses.
" @@ -194,7 +194,7 @@ "weightvar": null, "order": 2, "dynamic": false, - "unit": "number of people", + "unit": "no. of people", "description": { "EGY": { "heavy-rain": "Population data is aggregated per administrative area, from the following original source (Population Data): High Resolution Settlement Layer (HRSL). Source imagery for HRSL © 2016 DigitalGlobe. Accessed 01-01-2020. Facebook Connectivity Lab and Center for International Earth Science Information Network - CIESIN - Columbia University. 2016. https://www.ciesin.columbia.edu/data/hrsl/" @@ -1267,7 +1267,8 @@ "order": 2, "dynamic": true, "lazyLoad": true, - "unit": "$" + "unit": "MWK", + "aggregateUnit": "MWK" }, { "countryCodes": "MWI", @@ -1285,7 +1286,8 @@ "order": 2, "dynamic": true, "lazyLoad": true, - "unit": "nr. of roads" + "unit": "km", + "aggregateUnit": "km" }, { "countryCodes": "MWI", @@ -1303,13 +1305,13 @@ "order": 2, "dynamic": true, "lazyLoad": true, - "unit": "nr. of schools" + "unit": "schools" }, { "countryCodes": "MWI", "disasterTypes": ["flash-floods"], "name": "nr_affected_clinics", - "label": "Affected clinics", + "label": "Affected health sites", "icon": "Clinics_affected.svg", "weightedAvg": false, "active": "no", @@ -1321,7 +1323,7 @@ "order": 2, "dynamic": true, "lazyLoad": true, - "unit": "nr. of clinics" + "unit": "health sites" }, { "countryCodes": "MWI", @@ -1339,7 +1341,7 @@ "order": 2, "dynamic": true, "lazyLoad": true, - "unit": "nr. of waterpoints" + "unit": "waterpoints" }, { "countryCodes": "MWI", @@ -1357,7 +1359,7 @@ "order": 2, "dynamic": true, "lazyLoad": true, - "unit": "nr. of buildings" + "unit": "buildings" }, { "countryCodes": "KEN", diff --git a/services/API-service/src/scripts/json/layer-metadata.json b/services/API-service/src/scripts/json/layer-metadata.json index d1a02c6b9..6b41dcade 100644 --- a/services/API-service/src/scripts/json/layer-metadata.json +++ b/services/API-service/src/scripts/json/layer-metadata.json @@ -5,7 +5,36 @@ "name": "flood_extent", "label": "Flood extent", "type": "wms", - "legendColor": "#d7301f", + "legendColor": { + "ETH": { + "floods": { "type": "square", "value": ["#d7301f"] } + }, + "KEN": { + "floods": { "type": "square", "value": ["#d7301f"] } + }, + "MWI": { + "floods": { "type": "square", "value": ["#d7301f"] }, + "flash-floods": { + "type": "gradient", + "value": ["#ffff00", "#ffa500", "#d7301f"] + } + }, + "PHL": { + "floods": { + "type": "gradient", + "value": ["#ffff00", "#ffa500", "#d7301f"] + } + }, + "SSD": { + "floods": { "type": "square", "value": ["#d7301f"] } + }, + "UGA": { + "floods": { "type": "square", "value": ["#d7301f"] } + }, + "ZMB": { + "floods": { "type": "square", "value": ["#d7301f"] } + } + }, "leadTimeDependent": true, "active": "if-trigger", "description": { @@ -38,7 +67,24 @@ "name": "rainfall_extent", "label": "Rainfall extent", "type": "wms", - "legendColor": "#d7301f", + "legendColor": { + "EGY": { + "type": "square", + "value": ["#d7301f"] + }, + "UGA": { + "type": "gradient", + "value": [ + "#ffff7f", + "#ffe200", + "#ffaa00", + "#ff8d00", + "#ff7100", + "#ff3800", + "#ff0000" + ] + } + }, "leadTimeDependent": true, "active": "yes", "description": { @@ -56,7 +102,18 @@ "name": "rainfall_forecast", "label": "Rainfall forecast", "type": "wms", - "legendColor": "#ff8d00", + "legendColor": { + "type": "gradient", + "value": [ + "#ffff7f", + "#ffe200", + "#ffaa00", + "#ff8d00", + "#ff7100", + "#ff3800", + "#ff0000" + ] + }, "leadTimeDependent": true, "active": "no", "description": { @@ -74,7 +131,7 @@ "name": "population", "label": "Population", "type": "wms", - "legendColor": "#737373", + "legendColor": { "type": "square", "value": ["#737373"] }, "leadTimeDependent": false, "active": "no", "description": { @@ -118,7 +175,7 @@ "name": "cropland", "label": "Cropland", "type": "wms", - "legendColor": "#DCF064", + "legendColor": { "type": "square", "value": ["#dcf064"] }, "leadTimeDependent": false, "active": "no", "description": { @@ -149,7 +206,7 @@ "name": "grassland", "label": "Grassland", "type": "wms", - "legendColor": "#737373", + "legendColor": { "type": "square", "value": ["#be9600"] }, "leadTimeDependent": false, "active": "no", "description": { @@ -180,7 +237,7 @@ "name": "riceland", "label": "Riceland", "type": "wms", - "legendColor": "#DCF064", + "legendColor": { "PHL": { "type": "square", "value": ["#dcf064"] } }, "leadTimeDependent": false, "active": "no", "description": { @@ -237,14 +294,7 @@ }, { "countryCodes": "UGA,ZMB,KEN,ETH,ZWE,PHL,MWI,SSD", - "disasterTypes": [ - "floods", - "heavy-rain", - "dengue", - "malaria", - "drought", - "flash-floods" - ], + "disasterTypes": ["floods", "heavy-rain", "dengue", "malaria", "drought"], "name": "red_cross_branches", "label": "Red Cross branches", "type": "point", @@ -395,7 +445,12 @@ "name": "flood_susceptibility", "label": "Flood susceptibility", "type": "wms", - "legendColor": "#3690c0", + "legendColor": { + "EGY": { + "type": "gradient", + "value": ["#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0"] + } + }, "leadTimeDependent": false, "active": "no", "description": { @@ -482,7 +537,13 @@ "label": "Roads", "type": "wms", "leadTimeDependent": false, - "active": "no" + "active": "no", + "legendColor": { + "MWI": { + "type": "exposure-line", + "value": ["#E80C0C", "#33A02C"] + } + } }, { "countryCodes": "MWI", @@ -491,6 +552,12 @@ "label": "Buildings", "type": "wms", "leadTimeDependent": false, - "active": "no" + "active": "no", + "legendColor": { + "MWI": { + "type": "exposure-square", + "value": ["#C70000", "#33A02C"] + } + } } ]