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}` + - (layer.unit ? ' (' + layer.unit + ')' : ''); + private updateLegend() { + if (!this.legendDiv) { + return; + } - const noDataEntryFound = layer.data?.features.find( - (f) => f.properties?.indicators[layer.name] === null, - ); - if (noDataEntryFound) { - div.innerHTML += `
No data
`; + const layersToShow = this.layers + .filter((l) => l.active && l.group !== IbfLayerGroup.adminRegions) + .filter( + (value, index, self) => + index === self.findIndex((t) => t.name === value.name), + ); // deduplicate based on name (for e.g. waterpoints_internal) + + this.legendDiv.innerHTML = this.mapLegendService.getLegendTitle(); + for (const layer of layersToShow.sort(this.sortLayers)) { + const elements = []; + switch (layer.type) { + case IbfLayerType.point: + if (this.isMultiLinePointLayer(layer.name)) { + for (const exposed of [false, true]) { + const element = this.mapLegendService.getPointLegendString( + layer, + exposed, + ); + elements.push(element); + } + } else if (layer.name === IbfLayerName.glofasStations) { + for (const glofasState of this.getGlofasStationStates()) { + const element = this.mapLegendService.getGlofasPointLegendString( + layer, + `-${glofasState}-trigger`, + ); + elements.push(element); + } + } else { + const element = this.mapLegendService.getPointLegendString( + layer, + false, + ); + elements.push(element); + } + break; + case IbfLayerType.shape: + elements.push(this.mapLegendService.getShapeLegendString(layer)); + break; + case IbfLayerType.wms: + elements.push(this.mapLegendService.getWmsLegendString(layer)); + break; + default: + elements.push(`

${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" > @@ -33,113 +34,56 @@ - - + - - - - - {{ layer.label }} - - - - - - - - - - + + +
+ + + {{ layer.label }} + + + + + +
diff --git a/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.scss b/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.scss index 791de1313..6173d8984 100644 --- a/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.scss +++ b/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.scss @@ -3,7 +3,7 @@ top: 10px; bottom: 0; right: 0; - width: 25%; + width: 28%; ion-menu { --width: 100%; @@ -11,105 +11,90 @@ ion-content { --background: transparent; + } - ion-list { - ion-item { - --min-height: 32px; - --inner-padding-end: 0; - --border-color: var(--ion-color-ibf-no-alert-primary); + ion-content::part(scroll), + ion-content::part(background) { + height: max-content; + } + } - cursor: pointer; + ion-menu::part(backdrop) { + display: none; + background: transparent; + } + ion-menu-toggle { + cursor: pointer; - ion-row { - height: 100%; - width: 100%; + ion-toolbar { + --min-height: 36px; + } + } + ion-row { + min-height: 36px; + border: 1px solid var(--ion-color-ibf-no-alert-primary); + border-top: none; + } - .layer-control-info-col { - display: flex; - } + ion-item { + width: 100%; + --min-height: 36px; + --padding-bottom: 0; + --padding-top: 0; + --padding-start: 4px; + --padding-end: 4px; + --inner-padding-start: 0; + --inner-padding-end: 0; - .point-layer { - background-color: var(--ion-color-ibf-no-alert-primary); - background-repeat: no-repeat; - background-position: center; - background-size: contain; - padding: 4px; - background-origin: content-box; - } + ion-spinner { + width: 18px; + height: 18px; + } - .glofas-layer { - background-image: url('../../../assets/markers/trigger-icon.svg'); - } - .typhoon-track-layer { - background-image: url('../../../assets/markers/typhoon-track.png'); - } - .red-cross-layer { - background-image: url('../../../assets/markers/red-cross-icon.svg'); - } - .dam-layer { - background-image: url('../../../assets/markers/dam-icon.svg'); - } - .waterpoint-layer { - background-image: url('../../../assets/markers/water-point-icon.svg'); - } - .health-layer { - background-image: url('../../../assets/markers/health-center-icon.svg'); - } - .evacuation-center-layer { - background-image: url('../../../assets/markers/evacuation-center-icon.svg'); - } - .school-layer { - background-image: url('../../../assets/markers/school-icon.svg'); - } - .community-notification-layer { - background-image: url('../../../assets/markers/community-notification-icon.svg'); - } - .outline-layer { - border: solid; - border-color: var(--ion-color-ibf-outline-red) !important; - border-width: 3px !important; - background: #2c3136 !important; - } - .aggregate-layer-triggered { - background-image: url('../../../assets/icons/Legend_aggregate_layer-triggered.svg') !important; - background-size: contain !important; - } - .aggregate-layer-non-triggered { - background-image: url('../../../assets/icons/Legend_aggregate_layer-non-triggered.svg') !important; - background-size: contain !important; - } - .raster-layer-raifall-forecast { - background-image: url('../../../assets/icons/Legend_raster_layer_rainfall_forecast.svg') !important; - background-size: contain !important; - } + .matrix-toggler { + width: 18px; + height: 18px; + background-color: #fff; + border: 2px solid var(--ion-color-ibf-no-alert-primary); + cursor: pointer; - .layer-control-label-col { - ion-item { - --background: transparent; + &.matrix-check { + border-radius: 2px; + } - ion-label { - text-transform: capitalize; - line-height: 1.2; - padding: 4px 8px; - font-size: 11px; - } - } - } + &.matrix-radio { + border-radius: 50%; + } - .layer-control-info-icon { - border-radius: 5px; - } - } - } + .matrix-check-mark { + width: 16px; + height: 16px; + background-color: var(--ion-color-ibf-no-alert-primary); + stroke: #fff; + stroke-width: 4px; + } + .matrix-radio-circle { + width: 14px; + height: 14px; + background-color: var(--ion-color-ibf-no-alert-primary); + border: 2px solid #fff; + border-radius: 50%; } } - } - ion-menu-toggle { - cursor: pointer; + ion-label { + width: 80%; + font-size: 12px; + } - ion-toolbar { - --min-height: 36px; + .info-button { + --background-hover: #fff; + --ripple-color: #fff; + } + .info-icon { + font-size: 24px; + + cursor: pointer; } } } diff --git a/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.ts b/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.ts index 3e8c73369..1826a0384 100644 --- a/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.ts +++ b/interfaces/IBF-dashboard/src/app/components/matrix/matrix.component.ts @@ -45,7 +45,11 @@ export class MatrixComponent implements OnDestroy { this.layerSubscription.unsubscribe(); } - private onLayerChange = (newLayer) => { + private onLayerChange = (newLayer: IbfLayer) => { + if (newLayer && newLayer.name === 'alert_threshold') { + return; + } + if (newLayer) { const newLayerIndex = this.layers.findIndex( (layer) => layer.name === newLayer.name, @@ -114,4 +118,14 @@ export class MatrixComponent implements OnDestroy { .filter((layer) => layer.order >= 0) .sort(this.sortLayers); } + + public isCheckBox(layerGroup: IbfLayerGroup): boolean { + return [IbfLayerGroup.point, IbfLayerGroup.wms].includes(layerGroup); + } + + public isRadioButton(layerGroup: IbfLayerGroup): boolean { + return [IbfLayerGroup.aggregates, IbfLayerGroup.outline].includes( + layerGroup, + ); + } } diff --git a/interfaces/IBF-dashboard/src/app/services/api.service.ts b/interfaces/IBF-dashboard/src/app/services/api.service.ts index 85c1036d1..a089d56c8 100644 --- a/interfaces/IBF-dashboard/src/app/services/api.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/api.service.ts @@ -185,11 +185,11 @@ export class ApiService { getPointData( countryCodeISO3: string, layerName: IbfLayerName, - leadTime?: LeadTime, + disasterType: DisasterTypeKey, ): Observable { let params = new HttpParams(); - if (leadTime) { - params = params.append('leadTime', leadTime); + if (disasterType) { + params = params.append('disasterType', disasterType); } return this.get( `point-data/${layerName}/${countryCodeISO3}`, diff --git a/interfaces/IBF-dashboard/src/app/services/map-legend.service.spec.ts b/interfaces/IBF-dashboard/src/app/services/map-legend.service.spec.ts new file mode 100644 index 000000000..c317db5d0 --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/services/map-legend.service.spec.ts @@ -0,0 +1,19 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { MapLegendService } from './map-legend.service'; + +describe('MapLegendService', () => { + let service: MapLegendService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, RouterTestingModule], + }); + service = TestBed.inject(MapLegendService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/interfaces/IBF-dashboard/src/app/services/map-legend.service.ts b/interfaces/IBF-dashboard/src/app/services/map-legend.service.ts new file mode 100644 index 000000000..f24838dd7 --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/services/map-legend.service.ts @@ -0,0 +1,376 @@ +import { Injectable } from '@angular/core'; +import { Country, DisasterType } from '../models/country.model'; +import { breakKey } from '../models/map.model'; +import { EventState } from '../types/event-state'; +import { + IbfLayer, + IbfLayerLabel, + IbfLayerName, + wmsLegendType, +} from '../types/ibf-layer'; +import { NumberFormat } from '../types/indicator-group'; +import { CountryService } from './country.service'; +import { DisasterTypeService } from './disaster-type.service'; +import { EventService } from './event.service'; +import { MapService } from './map.service'; + +enum SingleRowLegendType { + fullSquare = 'full-square', + fullSquareGradient = 'full-square-gradient', + outlineSquare = 'outline-square', + line = 'line', + pin = 'pin', +} +@Injectable({ + providedIn: 'root', +}) +export class MapLegendService { + private legendDivTitle = `
Map Legend
`; + + public eventState: EventState; + private country: Country; + private disasterType: DisasterType; + + private layerIconURLPrefix = 'assets/markers/'; + private layerIcon = { + [IbfLayerName.glofasStations]: 'glofas-station.svg', + [IbfLayerName.typhoonTrack]: 'typhoon-track.png', + [IbfLayerName.redCrossBranches]: 'red-cross-marker.svg', + [IbfLayerName.damSites]: 'dam-marker.svg', + [IbfLayerName.waterpoints]: 'water-point-marker.svg', + [IbfLayerName.waterpointsInternal]: 'water-point-marker.svg', + [IbfLayerName.healthSites]: 'health-center-marker.svg', + [IbfLayerName.evacuationCenters]: 'evacuation-center-marker.svg', + [IbfLayerName.schools]: 'school-marker.svg', + [IbfLayerName.communityNotifications]: 'community-notification-marker.svg', + }; + + constructor( + private mapService: MapService, + private eventService: EventService, + private countryService: CountryService, + private disasterTypeService: DisasterTypeService, + ) { + this.eventService + .getInitialEventStateSubscription() + .subscribe(this.onEventStateChange); + + this.eventService + .getManualEventStateSubscription() + .subscribe(this.onEventStateChange); + + this.countryService + .getCountrySubscription() + .subscribe(this.onCountryChange); + + this.disasterTypeService + .getDisasterTypeSubscription() + .subscribe(this.onDisasterTypeChange); + } + + public getLegendTitle(): string { + return this.legendDivTitle; + } + + public getPointLegendString(layer: IbfLayer, exposed: boolean): string { + const exposedString = exposed ? '-exposed' : ''; + return this.singleRowLegend( + SingleRowLegendType.pin, + this.layerIconURLPrefix + + this.layerIcon[layer.name].slice(0, -4) + + exposedString + + this.layerIcon[layer.name].slice(-4), + `${exposed ? 'Exposed ' : ''}${layer.label}`, + ); + } + + public getGlofasPointLegendString( + layer: IbfLayer, + glofasState: string, + ): string { + return this.singleRowLegend( + SingleRowLegendType.pin, + this.layerIconURLPrefix + + this.layerIcon[layer.name].slice(0, -4) + + glofasState + + this.layerIcon[layer.name].slice(-4), + `${layer.label}${glofasState.replace(new RegExp('-', 'g'), ' ')}`, + ); + } + + public getShapeLegendString(layer: IbfLayer): string { + if (layer.name === IbfLayerName.alertThreshold) { + return this.singleRowLegend( + SingleRowLegendType.outlineSquare, + 'red', + layer.label, + ); + } + + if (!layer.data) { + return ''; + } + + const colorThreshold = this.mapService.getColorThreshold( + layer.data, + layer.colorProperty, + layer.colorBreaks, + ); + + const grades = Object.values(colorThreshold); + let labels; + if (layer.colorBreaks) { + labels = Object.values(layer.colorBreaks).map( + (colorBreak) => colorBreak.label, + ); + } + + const colors = + this.eventState?.activeTrigger && this.eventState?.thresholdReached + ? this.mapService.state.colorGradientTriggered + : this.mapService.state.colorGradient; + + const getColor = this.getFeatureColorByColorsAndColorThresholds( + colors, + colorThreshold, + ); + + const getLabel = this.getLabel(grades, layer, labels); + + let element = '
'; + element += this.layerTitle(layer.label, layer.unit); + + const noDataEntryFound = layer.data?.features.find( + (f) => f.properties?.indicators[layer.name] === null, + ); + element += `
`; + if (noDataEntryFound) { + element += this.singleRowLegend( + SingleRowLegendType.fullSquareGradient, + this.mapService.state.noDataColor, + 'No data', + ); + } + + for (let i = 0; i < grades.length; i++) { + if (grades[i] !== null && (i === 0 || grades[i] > grades[i - 1])) { + element += this.singleRowLegend( + SingleRowLegendType.fullSquareGradient, + getColor(grades[i + 1]), + getLabel(i), + ); + } + } + element += `
`; + + return element; + } + + public getWmsLegendString(layer: IbfLayer): string { + let element = '
'; + const typeKey = 'type'; + const legendColor = + layer.legendColor[this.country.countryCodeISO3]?.[ + this.disasterType.disasterType + ] || + layer.legendColor[this.country.countryCodeISO3] || + layer.legendColor; + const legendType = legendColor[typeKey]; + const valueKey = 'value'; + const value = legendColor[valueKey]; + + switch (legendType) { + case wmsLegendType.exposureLine: + element += this.layerTitle(layer.label); + element += this.singleRowLegend( + SingleRowLegendType.line, + value[0], + 'Exposed ' + layer.label, + ); + element += this.singleRowLegend( + SingleRowLegendType.line, + value[1], + 'Non-exposed ' + layer.label, + ); + break; + case wmsLegendType.exposureSquare: + element += this.layerTitle(layer.label); + element += this.singleRowLegend( + SingleRowLegendType.fullSquare, + value[0], + 'Exposed ' + layer.label, + ); + element += this.singleRowLegend( + SingleRowLegendType.fullSquare, + value[1], + 'Non-exposed ' + layer.label, + ); + break; + case wmsLegendType.gradient: + element += this.layerTitle(layer.label, layer.unit); + + element += ''; + for (const color of value) { + element += `
`; + } + element += '
'; + element += `LowHigh`; + break; + case wmsLegendType.square: + element += this.singleRowLegend( + SingleRowLegendType.fullSquare, + value[0], + layer.label, + ); + break; + default: + break; + } + element += '
'; + return element; + } + + private getWmsGradientCaptionMargin(gradientLength: number): number { + return gradientLength <= 4 + ? (gradientLength - 2) * 14 + : 7 + (gradientLength - 4) * 14; + } + + 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 + '
'; + } else { + return ( + this.numberFormat(valueLow, layer) + + '–' + + this.numberFormat(valueHigh, layer) + + label + + '
' + ); + } + } 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 + + '
' + ); + } + }; + + private numberFormat(d, layer) { + if (d === null) { + return null; + } else if (layer.numberFormatMap === NumberFormat.perc) { + return Math.round(d * 100).toLocaleString() + '%'; + } else if (layer.numberFormatMap === NumberFormat.decimal2) { + return (Math.round(d * 100) / 100).toLocaleString(); + } else if (layer.numberFormatMap === NumberFormat.decimal0) { + 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 Math.round(d).toLocaleString(); + } + } else { + return Math.round(d).toLocaleString(); + } + } + + private onEventStateChange = (eventState: EventState) => { + this.eventState = eventState; + }; + + private onCountryChange = (country: Country) => { + this.country = country; + }; + + private onDisasterTypeChange = (disasterType: DisasterType) => { + this.disasterType = disasterType; + }; + + private singleRowLegend = ( + type: SingleRowLegendType, + identifier: string, + label: string, + ) => { + const rowStyle = + type === SingleRowLegendType.fullSquareGradient + ? 'style="margin-bottom: 0px; margin-top: 0px; height: 14px;"' + : 'style="margin-bottom: 8px; margin-top: 8px;"'; + + const identifierHeight = type === SingleRowLegendType.pin ? 18 : 14; + + const pinImg = + type === SingleRowLegendType.pin ? `` : ''; + + let divStyle = `height: ${identifierHeight}px; width: 14px; margin-right: 4px; `; + switch (type) { + case SingleRowLegendType.fullSquare: + case SingleRowLegendType.fullSquareGradient: + divStyle += `background: ${identifier}`; + break; + case SingleRowLegendType.outlineSquare: + divStyle += `border: 2px solid ${identifier}`; + break; + case SingleRowLegendType.line: + divStyle += `background: ${identifier}; border-color: #fff; border-style: solid; border-width: 6px 0;`; + break; + default: + break; + } + + const labelStyle = + type === SingleRowLegendType.pin && label !== IbfLayerLabel.typhoonTrack + ? 'style="padding-top: 2px"' + : ''; + + return ` + +
${pinImg}
+ ${label} +
+ `; + }; + + private layerTitle(label: string, unit?: string): string { + return ` + ${label}${ + unit ? ' (' + unit + ')' : '' + } + `; + } +} diff --git a/interfaces/IBF-dashboard/src/app/services/map.service.ts b/interfaces/IBF-dashboard/src/app/services/map.service.ts index 925b4d2dd..2a4976c72 100644 --- a/interfaces/IBF-dashboard/src/app/services/map.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/map.service.ts @@ -84,6 +84,18 @@ export class MapService { private disasterType: DisasterType; private placeCode: PlaceCode; + private aggregatesToExclude = { + MWI: { + [DisasterTypeKey.flashFloods]: [ + 'nr_affected_roads', + 'nr_affected_schools', + 'nr_affected_clinics', + 'nr_affected_waterpoints', + 'nr_affected_buildings', + ], + }, + }; + constructor( private countryService: CountryService, private adminLevelService: AdminLevelService, @@ -235,6 +247,7 @@ export class MapService { name: IbfLayerName.glofasStations, label: IbfLayerLabel.glofasStations, type: IbfLayerType.point, + group: IbfLayerGroup.point, description: this.getPopoverText(layer), active: this.adminLevelService.activeLayerNames.length ? this.adminLevelService.activeLayerNames.includes( @@ -273,6 +286,7 @@ export class MapService { name: IbfLayerName.typhoonTrack, label: IbfLayerLabel.typhoonTrack, type: IbfLayerType.point, + group: IbfLayerGroup.point, description: this.getPopoverText(layer), active: this.adminLevelService.activeLayerNames.length ? this.adminLevelService.activeLayerNames.includes( @@ -300,7 +314,7 @@ export class MapService { .getPointData( this.country.countryCodeISO3, layerName, - this.timelineState.activeLeadTime, + this.disasterType.disasterType, ) .subscribe((pointData) => { this.addPointDataLayer(layer, layerName, pointData); @@ -320,6 +334,7 @@ export class MapService { name: layerName, label: layer.label, type: IbfLayerType.point, + group: IbfLayerGroup.point, description: this.getPopoverText(layer), active: this.adminLevelService.activeLayerNames.length ? this.adminLevelService.activeLayerNames.includes(layerName) @@ -352,6 +367,7 @@ export class MapService { name: IbfLayerName.waterpoints, label: IbfLayerLabel.waterpoints, type: IbfLayerType.point, + group: IbfLayerGroup.point, description: this.getPopoverText(layer), active: this.adminLevelService.activeLayerNames.includes( IbfLayerName.waterpoints, @@ -399,7 +415,23 @@ export class MapService { } public loadAggregateLayer(indicator: Indicator) { - if (this.country) { + if (!this.country || !this.disasterType) { + return; + } + + Object.keys(this.aggregatesToExclude).includes( + this.country.countryCodeISO3, + ); + + if ( + !this.aggregatesToExclude[this.country.countryCodeISO3] || + !this.aggregatesToExclude[this.country.countryCodeISO3][ + this.disasterType.disasterType + ] || + !this.aggregatesToExclude[this.country.countryCodeISO3][ + this.disasterType.disasterType + ].includes(indicator.name) + ) { const layerActive = this.adminLevelService.activeLayerNames.length ? this.adminLevelService.activeLayerNames.includes(indicator.name) : this.getActiveState(indicator); @@ -475,21 +507,11 @@ export class MapService { active: boolean, leadTimeDependent?: boolean, ) { - if ( - leadTimeDependent && - !this.eventState.event && - this.eventState.events.length - ) { - // don't show wms-layers in overview-mode of multi-event - return; - } - const leadTime = !leadTimeDependent - ? null - : this.timelineState.activeLeadTime; this.addLayer({ name: layer.name, label: layer.label, type: IbfLayerType.wms, + group: IbfLayerGroup.wms, description: this.getPopoverText(layer), active, show: true, @@ -499,9 +521,7 @@ export class MapService { order: 10, wms: { url: environment.geoserverUrl, - name: `ibf-system:${layer.name}_${leadTime ? `${leadTime}_` : ''}${ - this.country.countryCodeISO3 - }`, + leadTimeDependent, format: 'image/png', version: '1.1.0', attribution: '510 Global', @@ -676,7 +696,7 @@ export class MapService { .getPointData( this.country.countryCodeISO3, layerName, - this.timelineState.activeLeadTime, + this.disasterType.disasterType, ) .pipe(shareReplay(1)); } else if (layer.name === IbfLayerName.adminRegions) { diff --git a/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts b/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts index c47de0398..dad8fb125 100644 --- a/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts +++ b/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts @@ -13,7 +13,7 @@ export class IbfLayerMetadata { label: IbfLayerLabel; type: IbfLayerType; active: string; - legendColor: string; + legendColor: JSON; leadTimeDependent: boolean; description: JSON; } @@ -34,12 +34,19 @@ export class IbfLayer { wms?: IbfLayerWMS; data?: GeoJSON.FeatureCollection; leafletLayer?: Layer | LayerGroup | Marker | GeoJSON | MarkerClusterGroup; - legendColor?: string; + legendColor?: JSON | string; group?: IbfLayerGroup; dynamic?: boolean; isLoading?: boolean; } +export enum wmsLegendType { + exposureLine = 'exposure-line', + exposureSquare = 'exposure-square', + gradient = 'gradient', + square = 'square', +} + export enum IbfLayerThreshold { potentialCasesThreshold = 'potential_cases_threshold', } @@ -148,12 +155,15 @@ export class IbfLayerWMS { crs?: CRS; transparent: boolean; viewparams?: string; + leadTimeDependent: boolean; } export enum IbfLayerGroup { aggregates = 'aggregates', outline = 'outline', adminRegions = 'adminRegions', + point = 'point', + wms = 'wms', } export class ColorBreaks { diff --git a/interfaces/IBF-dashboard/src/assets/markers/community-notification-icon.svg b/interfaces/IBF-dashboard/src/assets/markers/community-notification-icon.svg index 47573de96..dce7f8fdf 100644 --- a/interfaces/IBF-dashboard/src/assets/markers/community-notification-icon.svg +++ b/interfaces/IBF-dashboard/src/assets/markers/community-notification-icon.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/interfaces/IBF-dashboard/src/assets/markers/dam-icon.svg b/interfaces/IBF-dashboard/src/assets/markers/dam-icon.svg index 1a9e7002e..93fe4a98e 100644 --- a/interfaces/IBF-dashboard/src/assets/markers/dam-icon.svg +++ b/interfaces/IBF-dashboard/src/assets/markers/dam-icon.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/interfaces/IBF-dashboard/src/assets/markers/evacuation-center-icon.svg b/interfaces/IBF-dashboard/src/assets/markers/evacuation-center-icon.svg index 6ac346610..3a13a74bf 100644 --- a/interfaces/IBF-dashboard/src/assets/markers/evacuation-center-icon.svg +++ b/interfaces/IBF-dashboard/src/assets/markers/evacuation-center-icon.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/interfaces/IBF-dashboard/src/assets/markers/health-center-icon.svg b/interfaces/IBF-dashboard/src/assets/markers/health-center-icon.svg index 11e5b4f1f..387549315 100644 --- a/interfaces/IBF-dashboard/src/assets/markers/health-center-icon.svg +++ b/interfaces/IBF-dashboard/src/assets/markers/health-center-icon.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/interfaces/IBF-dashboard/src/assets/markers/school-icon.svg b/interfaces/IBF-dashboard/src/assets/markers/school-icon.svg index 840eabf65..92cd79806 100644 --- a/interfaces/IBF-dashboard/src/assets/markers/school-icon.svg +++ b/interfaces/IBF-dashboard/src/assets/markers/school-icon.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/interfaces/IBF-dashboard/src/assets/markers/trigger-icon.svg b/interfaces/IBF-dashboard/src/assets/markers/trigger-icon.svg index 85dd075f8..e78d138cf 100644 --- a/interfaces/IBF-dashboard/src/assets/markers/trigger-icon.svg +++ b/interfaces/IBF-dashboard/src/assets/markers/trigger-icon.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/interfaces/IBF-dashboard/src/assets/markers/water-point-icon.svg b/interfaces/IBF-dashboard/src/assets/markers/water-point-icon.svg index 512bbc9cc..369a24e27 100644 --- a/interfaces/IBF-dashboard/src/assets/markers/water-point-icon.svg +++ b/interfaces/IBF-dashboard/src/assets/markers/water-point-icon.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/interfaces/IBF-dashboard/src/global.scss b/interfaces/IBF-dashboard/src/global.scss index 9ae1e7c4f..1125a1482 100644 --- a/interfaces/IBF-dashboard/src/global.scss +++ b/interfaces/IBF-dashboard/src/global.scss @@ -63,26 +63,12 @@ .info { padding: 6px 8px; - font: 14px/16px Arial, Helvetica, sans-serif; + font: 12px Arial, Helvetica, sans-serif; background: white; - background: rgba(255, 255, 255, 0.8); box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); border-radius: 5px; } -.legend { - line-height: 18px; - color: #555; - - i { - width: 18px; - height: 18px; - float: left; - margin-right: 8px; - opacity: 0.7; - } -} - .leaflet-popup-content-wrapper { border-radius: 0; } @@ -96,11 +82,11 @@ } .leaflet-ibf-wms-pane { - z-index: 566; + z-index: 575; pointer-events: none; } -.leaflet-outline { +.leaflet-outline-pane { z-index: 570; } diff --git a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/admin-area/featuretype.xml b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/admin-area/featuretype.xml deleted file mode 100644 index 7737ba2f6..000000000 --- a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/admin-area/featuretype.xml +++ /dev/null @@ -1,51 +0,0 @@ - - FeatureTypeInfoImpl-4d07a048:1888b7f27fb:33de - admin-area - admin-area - - NamespaceInfoImpl-48cab08a:175ace47603:-7ffa - - admin-area - - features - admin-area - - GEOGCS["WGS 84", - DATUM["World Geodetic System 1984", - SPHEROID["WGS 84", 6378137.0, 298.257223563, AUTHORITY["EPSG","7030"]], - AUTHORITY["EPSG","6326"]], - PRIMEM["Greenwich", 0.0, AUTHORITY["EPSG","8901"]], - UNIT["degree", 0.017453292519943295], - AXIS["Geodetic longitude", EAST], - AXIS["Geodetic latitude", NORTH], - AUTHORITY["EPSG","4326"]] - EPSG:4326 - - 8.80176830291748 - 127.19104766845703 - -22.698894500732422 - 33.4433479309082 - EPSG:4326 - - - 8.80176830291748 - 127.19104766845703 - -22.698894500732422 - 33.4433479309082 - EPSG:4326 - - FORCE_DECLARED - true - - DataStoreInfoImpl--1b3c9ac7:1878f465998:-7e9a - - false - false - 0 - 0 - false - false - false - false - false - \ No newline at end of file diff --git a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/admin-area/layer.xml b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/admin-area/layer.xml deleted file mode 100644 index 052b6e3e2..000000000 --- a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/admin-area/layer.xml +++ /dev/null @@ -1,17 +0,0 @@ - - admin-area - LayerInfoImpl-4d07a048:1888b7f27fb:33df - VECTOR - - StyleInfoImpl-6169d483:1875ba92894:-7ffc - - - FeatureTypeInfoImpl-4d07a048:1888b7f27fb:33de - - - 0 - 0 - - 2023-06-05 12:35:13.175 UTC - 2023-06-05 12:35:25.71 UTC - \ No newline at end of file diff --git a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/buildings_MWI/featuretype.xml b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/buildings_MWI/featuretype.xml index 3e8a19108..cd62def82 100644 --- a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/buildings_MWI/featuretype.xml +++ b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/buildings_MWI/featuretype.xml @@ -30,7 +30,7 @@ buildings_MWI - SELECT * FROM "IBF-app".buildings_exposure_per_lead_time WHERE "leadTime" IS NULL OR "leadTime" = '%leadTime%' + SELECT * FROM "IBF-app".buildings_exposure_per_lead_time false diff --git a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/buildings_MWI/layer.xml b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/buildings_MWI/layer.xml index 54ed7d754..f5849333f 100644 --- a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/buildings_MWI/layer.xml +++ b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/buildings_MWI/layer.xml @@ -13,5 +13,5 @@ 0 2023-06-05 08:28:08.247 UTC - 2023-06-05 12:06:12.75 UTC + 2023-07-10 11:49:36.2 UTC \ No newline at end of file diff --git a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/roads_MWI/featuretype.xml b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/roads_MWI/featuretype.xml index 0fa0c2ff2..dde86f7ce 100644 --- a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/roads_MWI/featuretype.xml +++ b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/roads_MWI/featuretype.xml @@ -30,7 +30,7 @@ roads_MWI - SELECT * FROM "IBF-app".roads_exposure_per_lead_time WHERE "leadTime" IS NULL OR "leadTime" = '%leadTime%' + SELECT * FROM "IBF-app".roads_exposure_per_lead_time false diff --git a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/roads_MWI/layer.xml b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/roads_MWI/layer.xml index ae15e9aa6..af0143e81 100644 --- a/services/API-service/geoserver-volume/geoserver-layers/postgis_db/roads_MWI/layer.xml +++ b/services/API-service/geoserver-volume/geoserver-layers/postgis_db/roads_MWI/layer.xml @@ -13,5 +13,5 @@ 0 2023-06-12 07:37:29.376 UTC - 2023-06-12 07:40:41.905 UTC + 2023-07-10 11:52:25.85 UTC \ No newline at end of file diff --git a/services/API-service/geoserver-volume/geoserver-layers/styles/buildings_exposure.sld b/services/API-service/geoserver-volume/geoserver-layers/styles/buildings_exposure.sld index 4b2ce595c..52d2ef771 100644 --- a/services/API-service/geoserver-volume/geoserver-layers/styles/buildings_exposure.sld +++ b/services/API-service/geoserver-volume/geoserver-layers/styles/buildings_exposure.sld @@ -1,71 +1,71 @@ - - - - - - Default Polygon - - - - Grey Polygon - A sample style that just prints out a grey interior with a black outline - - - - - - - Exposed - Exposed - - - exposed - true - - - - - - - #c21e4d - 0.2 - - - #c21e4d - 0.1 - - - - - - Not Exposed - Not Exposed - - - exposed - false - - - - - - - #3e8262 - 0.2 - - - #3e8262 - 0.1 - - - - - - - - \ No newline at end of file + + + + + + Default Polygon + + + + Grey Polygon + A sample style that just prints out a grey interior with a black outline + + + + + + + Exposed + Exposed + + + exposed + true + + + + + + + #C70000 + 1 + + + #C70000 + 0.1 + + + + + + Not Exposed + Not Exposed + + + exposed + false + + + + + + + #33A02C + 1 + + + #33A02C + 0.1 + + + + + + + + diff --git a/services/API-service/geoserver-volume/geoserver-layers/styles/buildings_exposure.xml b/services/API-service/geoserver-volume/geoserver-layers/styles/buildings_exposure.xml index 5226acaa7..aa56f1791 100644 --- a/services/API-service/geoserver-volume/geoserver-layers/styles/buildings_exposure.xml +++ b/services/API-service/geoserver-volume/geoserver-layers/styles/buildings_exposure.xml @@ -10,5 +10,5 @@ buildings_exposure.sld 2023-04-21 11:36:27.852 UTC - 2023-05-01 14:30:31.514 UTC - \ No newline at end of file + 2023-07-17 13:46:39.59 UTC + diff --git a/services/API-service/geoserver-volume/geoserver-layers/styles/roads_exposure.sld b/services/API-service/geoserver-volume/geoserver-layers/styles/roads_exposure.sld index 4e7e7b2c3..138a5d082 100644 --- a/services/API-service/geoserver-volume/geoserver-layers/styles/roads_exposure.sld +++ b/services/API-service/geoserver-volume/geoserver-layers/styles/roads_exposure.sld @@ -1,16 +1,16 @@ - + xsi:schemaLocation="http://www.opengis.net/sld StyledLayerDescriptor.xsd" + xmlns="http://www.opengis.net/sld" + xmlns:ogc="http://www.opengis.net/ogc" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + Default Polygon - + Grey Polygon A sample style that just prints out a grey interior with a black outline @@ -20,44 +20,129 @@ - Exposed - Exposed + Exposed primary + Exposed primary - - exposed - true - - + + + exposed + true + + + + highway + primary + + + highway + trunk + + + + - #ff0000 + #E80C0C + 3 + + + + + + + Exposed secondary + Exposed secondary + + + + exposed + true + + + highway + primary + + + highway + trunk + + + + + + + + #E80C0C 0.4 - + + + + Not Exposed primary + Not Exposed primary + + + + exposed + false + + + + highway + primary + + + highway + trunk + + + + + + + + + #33A02C + 3 + + + + + - Not Exposed - Not Exposed + Not Exposed secondary + Not Exposed secondary - - exposed - false - - + + + exposed + false + + + highway + primary + + + highway + trunk + + + - #00ff00 + #33A02C 0.4 - + \ No newline at end of file diff --git a/services/API-service/geoserver-volume/geoserver-layers/styles/roads_exposure.xml b/services/API-service/geoserver-volume/geoserver-layers/styles/roads_exposure.xml index 2c7a45dcc..9815c08ec 100644 --- a/services/API-service/geoserver-volume/geoserver-layers/styles/roads_exposure.xml +++ b/services/API-service/geoserver-volume/geoserver-layers/styles/roads_exposure.xml @@ -10,5 +10,5 @@ roads_exposure.sld 2023-04-21 13:53:51.925 UTC - 2023-05-01 12:35:38.323 UTC + 2023-07-17 14:53:56.959 UTC \ No newline at end of file diff --git a/services/API-service/migration/1688740150607-AssetViewsJoinByTimestamp.ts b/services/API-service/migration/1688740150607-AssetViewsJoinByTimestamp.ts new file mode 100644 index 000000000..168af8251 --- /dev/null +++ b/services/API-service/migration/1688740150607-AssetViewsJoinByTimestamp.ts @@ -0,0 +1,93 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AssetViewsJoinByTimestamp1688740150607 + implements MigrationInterface { + name = 'AssetViewsJoinByTimestamp1688740150607'; + + public async up(queryRunner: QueryRunner): Promise { + // delete views first + await queryRunner.query( + `DELETE FROM "IBF-app"."typeorm_metadata" WHERE "type" = 'VIEW' AND "schema" = $1 AND "name" = $2`, + ['IBF-app', 'roads_exposure_per_lead_time'], + ); + await queryRunner.query( + `DROP VIEW "IBF-app"."roads_exposure_per_lead_time"`, + ); + await queryRunner.query( + `DELETE FROM "IBF-app"."typeorm_metadata" WHERE "type" = 'VIEW' AND "schema" = $1 AND "name" = $2`, + ['IBF-app', 'buildings_exposure_per_lead_time'], + ); + await queryRunner.query( + `DROP VIEW "IBF-app"."buildings_exposure_per_lead_time"`, + ); + + // then recreate with new definition + await queryRunner.query( + `CREATE VIEW "IBF-app"."buildings_exposure_per_lead_time" AS SELECT line."referenceId",line.geom, status."leadTime", COALESCE("status"."exposed",FALSE) as "exposed" FROM "IBF-app"."lines-data" "line" LEFT JOIN "IBF-app"."lines-data-dynamic-status" "status" ON line."linesDataId" = status."referenceId" LEFT JOIN (SELECT status."leadTime" as "leadTime", MAX(timestamp) as max_timestamp FROM "IBF-app"."lines-data-dynamic-status" "status" LEFT JOIN "IBF-app"."lines-data" "line" ON line."linesDataId" = status."referenceId" WHERE line."linesDataCategory" = 'buildings' GROUP BY status."leadTime") "max_timestamp" ON status."leadTime" = max_timestamp."leadTime" WHERE line."linesDataCategory" = 'buildings' AND ("status"."timestamp" = max_timestamp.max_timestamp OR "status"."timestamp" IS NULL)`, + ); + await queryRunner.query( + `INSERT INTO "IBF-app"."typeorm_metadata"("type", "schema", "name", "value") VALUES ($1, $2, $3, $4)`, + [ + 'VIEW', + 'IBF-app', + 'buildings_exposure_per_lead_time', + 'SELECT line."referenceId",line.geom, status."leadTime", COALESCE("status"."exposed",FALSE) as "exposed" FROM "IBF-app"."lines-data" "line" LEFT JOIN "IBF-app"."lines-data-dynamic-status" "status" ON line."linesDataId" = status."referenceId" LEFT JOIN (SELECT status."leadTime" as "leadTime", MAX(timestamp) as max_timestamp FROM "IBF-app"."lines-data-dynamic-status" "status" LEFT JOIN "IBF-app"."lines-data" "line" ON line."linesDataId" = status."referenceId" WHERE line."linesDataCategory" = \'buildings\' GROUP BY status."leadTime") "max_timestamp" ON status."leadTime" = max_timestamp."leadTime" WHERE line."linesDataCategory" = \'buildings\' AND ("status"."timestamp" = max_timestamp.max_timestamp OR "status"."timestamp" IS NULL)', + ], + ); + await queryRunner.query( + `CREATE VIEW "IBF-app"."roads_exposure_per_lead_time" AS SELECT line."referenceId",line.geom, line.attributes->>'highway' as "highway", status."leadTime", COALESCE("status"."exposed",FALSE) as "exposed" FROM "IBF-app"."lines-data" "line" LEFT JOIN "IBF-app"."lines-data-dynamic-status" "status" ON line."linesDataId" = status."referenceId" LEFT JOIN (SELECT status."leadTime" as "leadTime", MAX(timestamp) as max_timestamp FROM "IBF-app"."lines-data-dynamic-status" "status" LEFT JOIN "IBF-app"."lines-data" "line" ON line."linesDataId" = status."referenceId" WHERE line."linesDataCategory" = 'roads' GROUP BY status."leadTime") "max_timestamp" ON status."leadTime" = max_timestamp."leadTime" WHERE line."linesDataCategory" = 'roads' AND ("status"."timestamp" = max_timestamp.max_timestamp OR "status"."timestamp" IS NULL)`, + ); + await queryRunner.query( + `INSERT INTO "IBF-app"."typeorm_metadata"("type", "schema", "name", "value") VALUES ($1, $2, $3, $4)`, + [ + 'VIEW', + 'IBF-app', + 'roads_exposure_per_lead_time', + `SELECT line."referenceId",line.geom, line.attributes->>'highway' as "highway", status."leadTime", COALESCE("status"."exposed",FALSE) as "exposed" FROM "IBF-app"."lines-data" "line" LEFT JOIN "IBF-app"."lines-data-dynamic-status" "status" ON line."linesDataId" = status."referenceId" LEFT JOIN (SELECT status."leadTime" as "leadTime", MAX(timestamp) as max_timestamp FROM "IBF-app"."lines-data-dynamic-status" "status" LEFT JOIN "IBF-app"."lines-data" "line" ON line."linesDataId" = status."referenceId" WHERE line."linesDataCategory" = \'roads\' GROUP BY status."leadTime") "max_timestamp" ON status."leadTime" = max_timestamp."leadTime" WHERE line."linesDataCategory" = \'roads\' AND ("status"."timestamp" = max_timestamp.max_timestamp OR "status"."timestamp" IS NULL)`, + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "IBF-app"."typeorm_metadata" WHERE "type" = 'VIEW' AND "schema" = $1 AND "name" = $2`, + ['IBF-app', 'roads_exposure_per_lead_time'], + ); + await queryRunner.query( + `DROP VIEW "IBF-app"."roads_exposure_per_lead_time"`, + ); + await queryRunner.query( + `DELETE FROM "IBF-app"."typeorm_metadata" WHERE "type" = 'VIEW' AND "schema" = $1 AND "name" = $2`, + ['IBF-app', 'buildings_exposure_per_lead_time'], + ); + await queryRunner.query( + `DROP VIEW "IBF-app"."buildings_exposure_per_lead_time"`, + ); + + // recreate with old definition (copied from previous migration file) + await queryRunner.query( + `CREATE VIEW "IBF-app"."buildings_exposure_per_lead_time" AS SELECT line."referenceId",line.geom, status."leadTime", COALESCE("status"."exposed",FALSE) as "exposed" FROM "IBF-app"."lines-data" "line" LEFT JOIN "IBF-app"."lines-data-dynamic-status" "status" ON line."linesDataId" = status."referenceId" WHERE line."linesDataCategory" = 'buildings' ORDER BY "status"."timestamp" DESC`, + ); + await queryRunner.query( + `INSERT INTO "IBF-app"."typeorm_metadata"("type", "schema", "name", "value") VALUES ($1, $2, $3, $4)`, + [ + 'VIEW', + 'IBF-app', + 'buildings_exposure_per_lead_time', + 'SELECT line."referenceId",line.geom, status."leadTime", COALESCE("status"."exposed",FALSE) as "exposed" FROM "IBF-app"."lines-data" "line" LEFT JOIN "IBF-app"."lines-data-dynamic-status" "status" ON line."linesDataId" = status."referenceId" WHERE line."linesDataCategory" = \'buildings\' ORDER BY "status"."timestamp" DESC', + ], + ); + await queryRunner.query( + `CREATE VIEW "IBF-app"."roads_exposure_per_lead_time" AS SELECT line."referenceId",line.geom, status."leadTime", COALESCE("status"."exposed",FALSE) as "exposed" FROM "IBF-app"."lines-data" "line" LEFT JOIN "IBF-app"."lines-data-dynamic-status" "status" ON line."linesDataId" = status."referenceId" WHERE line."linesDataCategory" = 'roads' ORDER BY "status"."timestamp" DESC`, + ); + await queryRunner.query( + `INSERT INTO "IBF-app"."typeorm_metadata"("type", "schema", "name", "value") VALUES ($1, $2, $3, $4)`, + [ + 'VIEW', + 'IBF-app', + 'roads_exposure_per_lead_time', + 'SELECT line."referenceId",line.geom, status."leadTime", COALESCE("status"."exposed",FALSE) as "exposed" FROM "IBF-app"."lines-data" "line" LEFT JOIN "IBF-app"."lines-data-dynamic-status" "status" ON line."linesDataId" = status."referenceId" WHERE line."linesDataCategory" = \'roads\' ORDER BY "status"."timestamp" DESC', + ], + ); + } +} diff --git a/services/API-service/migration/1689252472334-AdvancedLegendColor.ts b/services/API-service/migration/1689252472334-AdvancedLegendColor.ts new file mode 100644 index 000000000..1b892d721 --- /dev/null +++ b/services/API-service/migration/1689252472334-AdvancedLegendColor.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AdvancedLegendColor1689252472334 implements MigrationInterface { + name = 'AdvancedLegendColor1689252472334'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "IBF-app"."layer-metadata" DROP COLUMN "legendColor"`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."layer-metadata" ADD "legendColor" json DEFAULT null`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "IBF-app"."layer-metadata" DROP COLUMN "legendColor"`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."layer-metadata" ADD "legendColor" character varying`, + ); + } +} diff --git a/services/API-service/migration/1689575782230-aggregateUnit.ts b/services/API-service/migration/1689575782230-aggregateUnit.ts new file mode 100644 index 000000000..0a50f07c7 --- /dev/null +++ b/services/API-service/migration/1689575782230-aggregateUnit.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AggregateUnit1689575782230 implements MigrationInterface { + name = 'aggregateUnit1689575782230'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "IBF-app"."indicator-metadata" ADD "aggregateUnit" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "IBF-app"."indicator-metadata" DROP COLUMN "aggregateUnit"`, + ); + } +} diff --git a/services/API-service/src/api/lines-data/lines-data-views.entity.ts b/services/API-service/src/api/lines-data/lines-data-views.entity.ts index 7eb1a627a..d3c4f490a 100644 --- a/services/API-service/src/api/lines-data/lines-data-views.entity.ts +++ b/services/API-service/src/api/lines-data/lines-data-views.entity.ts @@ -6,19 +6,46 @@ const getViewQuery = (type: LinesDataEnum) => { return () => getConnection() .createQueryBuilder() - .select(['line."referenceId",line.geom']) + .select([ + `line."referenceId",line.geom${ + type === LinesDataEnum.roads + ? `line.attributes->>'highway' as "roadType"` + : '' + }`, + ]) .from(LinesDataEntity, 'line') .leftJoin( LinesDataDynamicStatusEntity, 'status', 'line."linesDataId" = status."referenceId"', ) + .leftJoin( + subquery => { + return subquery + .select([ + 'status."leadTime" as "leadTime"', + 'MAX(timestamp) as max_timestamp', + ]) + .from(LinesDataDynamicStatusEntity, 'status') + .leftJoin( + LinesDataEntity, + 'line', + 'line."linesDataId" = status."referenceId"', + ) + .where(`line."linesDataCategory" = '${type}'`) + .groupBy('status."leadTime"'); + }, + 'max_timestamp', + 'status."leadTime" = max_timestamp."leadTime"', + ) .where(`line."linesDataCategory" = '${type}'`) + .andWhere( + '(status.timestamp = max_timestamp.max_timestamp OR status.timestamp IS NULL)', + ) .addSelect([ 'status."leadTime"', 'COALESCE(status.exposed,FALSE) as "exposed"', - ]) - .orderBy('status.timestamp', 'DESC'); + ]); }; @ViewEntity({ expression: getViewQuery(LinesDataEnum.buildings), diff --git a/services/API-service/src/api/lines-data/lines-data.service.ts b/services/API-service/src/api/lines-data/lines-data.service.ts index 2eca02fd3..5ea157d55 100644 --- a/services/API-service/src/api/lines-data/lines-data.service.ts +++ b/services/API-service/src/api/lines-data/lines-data.service.ts @@ -109,6 +109,8 @@ export class LinesDataService { public async uploadAssetExposureStatus( assetFids: UploadLinesExposureStatusDto, ) { + // Make sure all assets within one upload have the same timestamp, to make sure the asset exposure views work correctly + assetFids.date = assetFids.date || new Date(); const assetForecasts: LinesDataDynamicStatusEntity[] = []; for (const fid of assetFids.exposedFids) { const asset = await this.linesDataRepository.findOne({ @@ -129,7 +131,7 @@ export class LinesDataService { timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( assetFids.disasterType, - assetFids.date || new Date(), + assetFids.date, ), ), }); @@ -137,7 +139,7 @@ export class LinesDataService { const assetForecast = new LinesDataDynamicStatusEntity(); assetForecast.linesData = asset; assetForecast.leadTime = assetFids.leadTime; - assetForecast.timestamp = assetFids.date || new Date(); + assetForecast.timestamp = assetFids.date; assetForecast.exposed = true; assetForecasts.push(assetForecast); } diff --git a/services/API-service/src/api/metadata/dto/add-layers.dto.ts b/services/API-service/src/api/metadata/dto/add-layers.dto.ts index 7f035a547..72d048578 100644 --- a/services/API-service/src/api/metadata/dto/add-layers.dto.ts +++ b/services/API-service/src/api/metadata/dto/add-layers.dto.ts @@ -23,9 +23,15 @@ export class LayerDto { @IsIn(['wms', 'poi', 'shape']) public type: string; - @ApiProperty({ example: '#be9600' }) - @IsString() - public legendColor: string; + @ApiProperty({ + example: { + EGY: { + type: 'square', + value: ['#d7301f'], + }, + }, + }) + public legendColor: JSON; @ApiProperty({ example: false }) @IsBoolean() diff --git a/services/API-service/src/api/metadata/indicator-metadata.entity.ts b/services/API-service/src/api/metadata/indicator-metadata.entity.ts index 8ac040329..b53a821dc 100644 --- a/services/API-service/src/api/metadata/indicator-metadata.entity.ts +++ b/services/API-service/src/api/metadata/indicator-metadata.entity.ts @@ -80,6 +80,10 @@ export class IndicatorMetadataEntity { @Column({ nullable: true }) public unit: string; + @ApiProperty({ example: 'km' }) + @Column({ nullable: true }) + public aggregateUnit: string; + @ApiProperty() @Column({ default: false }) public lazyLoad: boolean; diff --git a/services/API-service/src/api/metadata/layer-metadata.entity.ts b/services/API-service/src/api/metadata/layer-metadata.entity.ts index 510caf853..2ec3c5d12 100644 --- a/services/API-service/src/api/metadata/layer-metadata.entity.ts +++ b/services/API-service/src/api/metadata/layer-metadata.entity.ts @@ -34,9 +34,9 @@ export class LayerMetadataEntity { @IsIn(['wms', 'poi', 'shape']) public type: string; - @ApiProperty({ example: '#be9600' }) - @Column({ nullable: true }) - public legendColor: string; + @ApiProperty() + @Column('json', { nullable: true, default: null }) + public legendColor: JSON; @ApiProperty({ example: false }) @Column({ nullable: true }) diff --git a/services/API-service/src/api/metadata/metadata.service.ts b/services/API-service/src/api/metadata/metadata.service.ts index 096934595..8887f28a3 100644 --- a/services/API-service/src/api/metadata/metadata.service.ts +++ b/services/API-service/src/api/metadata/metadata.service.ts @@ -116,7 +116,9 @@ export class MetadataService { }); layerEntity.label = layer.label; layerEntity.type = layer.type; - layerEntity.legendColor = layer.legendColor; + layerEntity.legendColor = layer.legendColor + ? JSON.parse(JSON.stringify(layer.legendColor)) + : null; layerEntity.leadTimeDependent = layer.leadTimeDependent; layerEntity.active = layer.active; layerEntity.description = JSON.parse( diff --git a/services/API-service/src/api/point-data/point-data.controller.ts b/services/API-service/src/api/point-data/point-data.controller.ts index 3e21975eb..ababae4ca 100644 --- a/services/API-service/src/api/point-data/point-data.controller.ts +++ b/services/API-service/src/api/point-data/point-data.controller.ts @@ -41,7 +41,7 @@ export class PointDataController { }) @ApiParam({ name: 'countryCodeISO3', required: true, type: 'string' }) @ApiParam({ name: 'pointDataCategory', required: true, type: 'string' }) - @ApiQuery({ name: 'leadTime', required: false, type: 'string' }) + @ApiQuery({ name: 'disasterType', required: true, type: 'string' }) @ApiResponse({ status: 200, description: @@ -53,7 +53,7 @@ export class PointDataController { return await this.pointDataService.getPointDataByCountry( params.pointDataCategory, params.countryCodeISO3, - query.leadTime, + query.disasterType, ); } diff --git a/services/API-service/src/api/point-data/point-data.service.ts b/services/API-service/src/api/point-data/point-data.service.ts index a5e76b4c7..ca318494d 100644 --- a/services/API-service/src/api/point-data/point-data.service.ts +++ b/services/API-service/src/api/point-data/point-data.service.ts @@ -35,7 +35,7 @@ export class PointDataService { public async getPointDataByCountry( pointDataCategory: PointDataEnum, countryCodeISO3: string, - leadTime?: LeadTime, + disasterType: DisasterType, ): Promise { const attributes = []; const dto = this.getDtoPerPointDataCategory(pointDataCategory); @@ -58,8 +58,8 @@ export class PointDataService { countryCodeISO3: countryCodeISO3, }); - if (leadTime) { - const disasterType = DisasterType.FlashFloods; // TO DO: hard-code for now + // TO DO: hard-code for now + if (disasterType === DisasterType.FlashFloods) { const recentDate = await this.helperService.getRecentDate( countryCodeISO3, disasterType, @@ -70,10 +70,6 @@ export class PointDataService { 'status', 'point."pointDataId" = status."referenceId"', ) - .andWhere( - '(status."leadTime" IS NULL OR status."leadTime" = :leadTime)', - { leadTime: leadTime }, - ) .andWhere( '(status."timestamp" IS NULL OR status.timestamp >= :cutoffTime)', { diff --git a/services/API-service/src/scripts/json/indicator-metadata.json b/services/API-service/src/scripts/json/indicator-metadata.json index 5f844c9b2..1a20e8357 100644 --- a/services/API-service/src/scripts/json/indicator-metadata.json +++ b/services/API-service/src/scripts/json/indicator-metadata.json @@ -14,13 +14,13 @@ "label": "Alert Threshold Reached", "icon": "Person2-white.svg", "weightedAvg": false, - "active": "if-trigger", + "active": "yes", "colorBreaks": null, "numberFormatMap": "decimal0", "aggregateIndicator": "", "numberFormatAggregate": "decimal0", "weightvar": null, - "order": 0, + "order": -100, "dynamic": true, "lazyLoad": true, "description": { @@ -77,7 +77,7 @@ "weightvar": null, "order": 1, "dynamic": true, - "unit": "number of people", + "unit": "no. of people", "description": { "EGY": { "heavy-rain": "Number of people exposed is calculated by the population living in the rainfall extent area within the governorates currently triggered. The number of people and the rainfall extent are derived from the below sources.

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/

Source Rainfall Extent: Global Ensemble Forecast System (GEFS) is a global weather forecast model produced by the NOAA's National Centers for Environmental Prediction (NCEP). Dozens of atmospheric forecast variables up to 16 days in the future, including precipitation, are available through this dataset.
The Rainfall Extent layer shows areas where forecasted GEFS precipitation occurrence exceeds defined thresholds." @@ -129,7 +129,7 @@ "weightvar": null, "order": 1, "dynamic": true, - "unit": "number of people", + "unit": "no. of people", "description": { "PHL": { "typhoon": "

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"] + } + } } ]