diff --git a/interfaces/IBF-dashboard/src/app/components/chat/chat.component.html b/interfaces/IBF-dashboard/src/app/components/chat/chat.component.html index e14d6309d..d40ec44d4 100644 --- a/interfaces/IBF-dashboard/src/app/components/chat/chat.component.html +++ b/interfaces/IBF-dashboard/src/app/components/chat/chat.component.html @@ -18,6 +18,7 @@ | translate: { supportEmailAddress: supportEmailAddress } " color="danger" + class="absolute" >

diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component.html b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component.html new file mode 100644 index 000000000..622096c3a --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component.html @@ -0,0 +1,28 @@ +
+ {{ title }} +
+
+ + + + + +
+ diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component.scss b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component.scss new file mode 100644 index 000000000..19a29afcf --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component.scss @@ -0,0 +1,37 @@ +.leaflet--dynamic-point-popup--header { + &.gauges { + background: var(--ion-color-ibf-no-alert-primary); + } + &.typhoon_track { + background: var(--ion-color-ibf-primary); + } + + color: white; + padding: 8px; + border-radius: 8px 8px 0 0; +} + +.leaflet--dynamic-point-popup--content { + padding: 8px; +} + +.leaflet--dynamic-point-popup--footer { + &.gauges { + color: var(--ion-color-ibf-no-alert-primary); + } + &.typhoon_track { + color: var(--ion-color-ibf-primary); + } + &.glofas_stations { + color: red; + } + + padding: 0 8px; + border-radius: 0 0 8px 8px; + + div { + padding: 8px 0; + text-align: center; + border-top: 1px solid var(--ion-color-ibf-grey-light); + } +} diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component.ts b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component.ts new file mode 100644 index 000000000..b19ea2f5e --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component.ts @@ -0,0 +1,144 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { LatLng } from 'leaflet'; +import { EapAlertClass, EapAlertClasses } from '../../../models/country.model'; +import { RiverGauge, Station } from '../../../models/poi.model'; +import { IbfLayerName } from '../../../types/ibf-layer'; +import { LeadTime } from '../../../types/lead-time'; + +@Component({ + selector: 'app-dynamic-point-popup', + templateUrl: './dynamic-point-popup.component.html', + styleUrls: ['./dynamic-point-popup.component.scss'], +}) +export class DynamicPointPopupComponent implements OnInit { + @Input() + public layerName: IbfLayerName; + + @Input() + public riverGauge?: RiverGauge; + + @Input() + public typhoonTrackPoint?: { + timestamp: string; + category: string; + markerLatLng: LatLng; + passed: boolean; + }; + + @Input() + public glofasData?: { + station: Station; + leadTime: LeadTime; + eapAlertClasses: EapAlertClasses; + }; + + public typhoonData: { + timestamp: string; + category: string; + }; + + public ibfLayerName = IbfLayerName; + + public title: string; + public footerContent: string; + + public glofasHeaderStyle: string; + public glofasFooterStyle: string; + + public eapAlertClass: EapAlertClass; + + private allowedLayers = [ + IbfLayerName.gauges, + IbfLayerName.typhoonTrack, + IbfLayerName.glofasStations, + ]; + + constructor(private translate: TranslateService) {} + + ngOnInit(): void { + if (!this.layerName || !this.allowedLayers.includes(this.layerName)) { + return; + } + + if (!this.riverGauge && !this.typhoonTrackPoint && !this.glofasData) { + return; + } + + if (this.layerName === IbfLayerName.typhoonTrack) { + this.typhoonData = { + timestamp: this.typhoonTrackPoint.timestamp, + category: this.typhoonTrackPoint.category, + }; + } + + if ( + this.layerName === IbfLayerName.glofasStations && + this.glofasData.eapAlertClasses + ) { + this.eapAlertClass = this.glofasData.eapAlertClasses[ + this.glofasData.station.dynamicData.eapAlertClass + ]; + } + + this.title = this.getTitle(); + this.footerContent = this.getFooterContent(); + + this.glofasHeaderStyle = + this.layerName === IbfLayerName.glofasStations + ? `background: var(--ion-color-${this.eapAlertClass.color});color: var(--ion-color-${this.eapAlertClass.color}-contrast);` + : ''; + + this.glofasFooterStyle = + this.layerName === IbfLayerName.glofasStations + ? `color: var(--ion-color-${this.eapAlertClass.color}${ + this.eapAlertClass.color === 'ibf-yellow' ? '-contrast' : '' + });` + : ''; + } + + private getTitle(): string { + if (this.layerName === IbfLayerName.gauges) { + return `${this.translate.instant('map-popups.river-gauge.header')} ${ + this.riverGauge.fid + } ${this.riverGauge.name}`; + } + + if (this.layerName === IbfLayerName.typhoonTrack) { + return `Typhoon track${this.typhoonTrackPoint.passed ? ' (passed)' : ''}`; + } + + if (this.layerName === IbfLayerName.glofasStations) { + return `${this.glofasData.station.stationCode} STATION: ${this.glofasData.station.stationName}`; + } + + return ''; + } + + private getFooterContent(): string { + if (this.layerName === IbfLayerName.gauges) { + return !this.riverGauge.dynamicData?.['water-level'] + ? '' + : this.riverGauge.dynamicData?.['water-level'] <= + this.riverGauge.dynamicData?.['water-level-reference'] + ? this.translate.instant('map-popups.river-gauge.below') + : this.translate.instant('map-popups.river-gauge.above'); + } + + if (this.layerName === IbfLayerName.typhoonTrack) { + const lat = `${Math.abs(this.typhoonTrackPoint.markerLatLng.lat).toFixed( + 4, + )}° ${this.typhoonTrackPoint.markerLatLng.lat > 0 ? 'N' : 'S'}`; + const lng = `${Math.abs(this.typhoonTrackPoint.markerLatLng.lng).toFixed( + 4, + )}° ${this.typhoonTrackPoint.markerLatLng.lng > 0 ? 'E' : 'W'}`; + return `${lat}, ${lng}`; + } + + if (this.layerName === IbfLayerName.glofasStations) { + return this.eapAlertClass.label; + } + + return ''; + } +} diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/glofas-station-popup-content/glofas-station-popup-content.component.css b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/glofas-station-popup-content/glofas-station-popup-content.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/glofas-station-popup-content/glofas-station-popup-content.component.html b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/glofas-station-popup-content/glofas-station-popup-content.component.html new file mode 100644 index 000000000..a1f7ca9d9 --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/glofas-station-popup-content/glofas-station-popup-content.component.html @@ -0,0 +1,27 @@ +
+ {{ data?.leadTime }} forecast of + river discharge + in m3/s + +
+ (Corresponding to a return period of + {{ data?.station?.forecastReturnPeriod }} years) +
+
+ diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/glofas-station-popup-content/glofas-station-popup-content.component.ts b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/glofas-station-popup-content/glofas-station-popup-content.component.ts new file mode 100644 index 000000000..233386eea --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/glofas-station-popup-content/glofas-station-popup-content.component.ts @@ -0,0 +1,65 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { EapAlertClass, EapAlertClasses } from '../../../models/country.model'; +import { Station } from '../../../models/poi.model'; +import { LeadTime } from '../../../types/lead-time'; + +@Component({ + selector: 'app-glofas-station-popup-content', + templateUrl: './glofas-station-popup-content.component.html', + styleUrls: ['./glofas-station-popup-content.component.css'], +}) +export class GlofasStationPopupContentComponent implements OnInit { + @Input() + public data: { + station: Station; + leadTime: LeadTime; + eapAlertClasses: EapAlertClasses; + }; + + public barValue: number; + public triggerWidth: number; + public barBackgroundColor: string; + public barTextColor: string; + private eapAlertClass: EapAlertClass; + + ngOnInit(): void { + if (!this.data) { + return; + } + + const difference = + Number(this.data.station.dynamicData.forecastLevel) - + Number(this.data.station.dynamicData.triggerLevel); + const closeMargin = 0.05; + const tooClose = + Math.abs(difference) / this.data.station.triggerLevel < closeMargin; + + this.barValue = + difference === 0 || !tooClose + ? Number(this.data.station.dynamicData.forecastLevel) + : Number(this.data.station.dynamicData.triggerLevel) + + Math.sign(difference) * + Number(this.data.station.dynamicData.triggerLevel) * + closeMargin; + + this.triggerWidth = Math.max( + Math.min( + Math.round( + (this.barValue / Number(this.data.station.dynamicData.triggerLevel)) * + 100, + ), + 115, + ), + 0, + ); + + this.eapAlertClass = this.data.eapAlertClasses[ + this.data.station.dynamicData.eapAlertClass + ]; + + this.barBackgroundColor = `var(--ion-color-${this.eapAlertClass.color})`; + this.barTextColor = `var(--ion-color-${this.eapAlertClass.color}-contrast)`; + } + + public addComma = (n) => Math.round(n).toLocaleString('en-US'); +} diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component.html b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component.html new file mode 100644 index 000000000..1f0f400e6 --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component.html @@ -0,0 +1,54 @@ + +
+
+ + {{ currentString }} + {{ 'map-popups.river-gauge.unit' | translate }} + {{ 'map-popups.river-gauge.sea-level' | translate }} + + + {{ differenceAbsolute }} + {{ 'map-popups.river-gauge.unit' | translate }} +
+ +
+
+ {{ + 'map-popups.river-gauge.current' | translate + }} +
+ +
+ +
+
+ + {{ 'map-popups.river-gauge.no-data' | translate }} + +
+
+
diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component.scss b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component.scss new file mode 100644 index 000000000..36879e670 --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component.scss @@ -0,0 +1,22 @@ +.leaflet--dynamic-point-popup--header { + background: var(--ion-color-ibf-no-alert-primary); + color: white; + padding: 8px; + border-radius: 8px 8px 0 0; +} + +.leaflet--dynamic-point-popup--content { + padding: 8px; +} + +.leaflet--dynamic-point-popup--footer { + color: var(--ion-color-ibf-no-alert-primary); + padding: 0 8px; + border-radius: 0 0 8px 8px; + + div { + padding: 8px 0; + text-align: center; + border-top: 1px solid var(--ion-color-ibf-grey-light); + } +} diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component.ts b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component.ts new file mode 100644 index 000000000..0976695e7 --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component.ts @@ -0,0 +1,43 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { RiverGauge } from '../../../models/poi.model'; + +@Component({ + selector: 'app-river-gauge-popup-content', + templateUrl: './river-gauge-popup-content.component.html', + styleUrls: ['./river-gauge-popup-content.component.scss'], +}) +export class RiverGaugePopupContentComponent implements OnInit { + @Input() + data: RiverGauge; + + public current: number; + public currentString: string; + public previous: number; + public reference: number; + public difference: number; + public differenceAbsolute: number; + public triggerWidth: number; + + constructor(public translate: TranslateService) {} + ngOnInit(): void { + if (!this.data) { + return; + } + + this.current = + Math.round(Number(this.data.dynamicData?.['water-level']) * 100) / 100; // 2 decimals + this.currentString = isNaN(this.current) ? '' : String(this.current); + this.previous = Number(this.data.dynamicData?.['water-level-previous']); + this.reference = Math.round( + Number(this.data.dynamicData?.['water-level-reference']), + ); // 0 decimals + this.difference = Math.round((this.current - this.previous) * 100) / 100; // 2 decimals + this.differenceAbsolute = Math.abs(this.difference); + + this.triggerWidth = Math.max( + Math.min(Math.round((this.current / this.reference) * 100), 115), + 0, + ); + } +} diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/threshold-bar/threshold-bar.component.html b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/threshold-bar/threshold-bar.component.html new file mode 100644 index 000000000..655d5e5d4 --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/threshold-bar/threshold-bar.component.html @@ -0,0 +1,13 @@ +
+
+
+
+ {{ barValue }} +
+
+
+
+
{{ thresholdDescription }}:
+
+ {{ thresholdValue }} +
diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/threshold-bar/threshold-bar.component.scss b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/threshold-bar/threshold-bar.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/threshold-bar/threshold-bar.component.ts b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/threshold-bar/threshold-bar.component.ts new file mode 100644 index 000000000..11c91200a --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/threshold-bar/threshold-bar.component.ts @@ -0,0 +1,103 @@ +import { Component, Input, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-threshold-bar', + templateUrl: './threshold-bar.component.html', + styleUrls: ['./threshold-bar.component.scss'], +}) +export class ThresholdBarComponent implements OnInit { + @Input() public backgroundColor: string; + @Input() public textColor: string; + @Input() public barWidth: number; + @Input() public barValue: string; + @Input() public thresholdDescription: string; + @Input() public thresholdValue: number; + @Input() public thresholdPosition: number; // width percentage to position threshold on bar + + public barBackgroundStyle: string; + public barThresholdStyle: string; + public barLevelStyle: string; + public descriptionStyle: string; + public valueStyle: string; + + ngOnInit(): void { + if ( + !this.backgroundColor || + !this.textColor || + !this.thresholdDescription || + !this.thresholdPosition || + this.barWidth === null || + this.barWidth === undefined || + this.barValue === null || + this.barValue === undefined || + this.thresholdValue === null || + this.thresholdValue === undefined + ) { + return; + } + + this.barBackgroundStyle = this.getBarBackgroundStyle(); + this.barThresholdStyle = this.getBarThresholdStyle(); + this.barLevelStyle = this.getBarLevelStyle(); + this.descriptionStyle = this.getDescriptionStyle(); + this.valueStyle = this.getValueStyle(); + } + + private getBarBackgroundStyle(): string { + return ` + border-radius: 10px; + height: 20px; + background-color: #d4d3d2; + width: 100%; + `; + } + + private getBarThresholdStyle(): string { + return ` + border-radius:10px 0 0 10px; + border-right: dashed; + border-right-width: thin; + height:20px; + width: ${this.thresholdPosition}% + `; + } + + private getBarLevelStyle(): string { + return ` + border-radius:10px; + height:20px; + line-height:20px; + background-color:${this.backgroundColor}; + color:${this.textColor}; + text-align:center; + white-space: nowrap; + min-width: 15%; + width:${this.barWidth}% + `; + } + + private getDescriptionStyle(): string { + return ` + height:20px; + background-color:none; + border-right: dashed; + border-right-width: thin; + float: left; width: ${this.thresholdPosition}%; + padding-top: 5px; + margin-bottom:10px; + text-align: right; + padding-right: 2px`; + } + + private getValueStyle(): string { + return ` + height:20px; + background-color:none; + margin-left: ${this.thresholdPosition + 1}%; + text-align: left; + width: 20%; + padding-top: 5px; + margin-bottom:10px + `; + } +} diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/typhoon-trackpoint-popup-content/typhoon-trackpoint-popup-content.component.html b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/typhoon-trackpoint-popup-content/typhoon-trackpoint-popup-content.component.html new file mode 100644 index 000000000..993392fe0 --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/typhoon-trackpoint-popup-content/typhoon-trackpoint-popup-content.component.html @@ -0,0 +1,27 @@ +
+
+
+ Date and time: {{ dateAndTime }} +
+
+ Category (ECWMF): + {{ + 'map-popups.PHL.typhoon.category.' + category | translate + }} +
+
+
+ + + +
+
diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/typhoon-trackpoint-popup-content/typhoon-trackpoint-popup-content.component.scss b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/typhoon-trackpoint-popup-content/typhoon-trackpoint-popup-content.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/interfaces/IBF-dashboard/src/app/components/leaflet-popup/typhoon-trackpoint-popup-content/typhoon-trackpoint-popup-content.component.ts b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/typhoon-trackpoint-popup-content/typhoon-trackpoint-popup-content.component.ts new file mode 100644 index 000000000..549b504f4 --- /dev/null +++ b/interfaces/IBF-dashboard/src/app/components/leaflet-popup/typhoon-trackpoint-popup-content/typhoon-trackpoint-popup-content.component.ts @@ -0,0 +1,37 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { DateTime } from 'luxon'; + +@Component({ + selector: 'app-typhoon-trackpoint-popup-content', + templateUrl: './typhoon-trackpoint-popup-content.component.html', + styleUrls: ['./typhoon-trackpoint-popup-content.component.scss'], +}) +export class TyphoonTrackpointPopupContentComponent implements OnInit { + @Input() + public data: { + timestamp: string; + category: string; + }; + + public dateAndTime: string; + public category: string; + public iconColor: string; + + constructor(public translate: TranslateService) {} + + ngOnInit(): void { + if (!this.data) { + return; + } + + this.dateAndTime = this.getDateAndTime(); + this.category = this.data.category; + } + + private getDateAndTime() { + return DateTime.fromISO(this.data.timestamp).toFormat( + 'ccc, dd LLLL, HH:mm', + ); + } +} 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 d2ea3158b..62ce7e3cb 100644 --- a/interfaces/IBF-dashboard/src/app/components/map/map.component.ts +++ b/interfaces/IBF-dashboard/src/app/components/map/map.component.ts @@ -42,6 +42,7 @@ import { EvacuationCenter, HealthSite, RedCrossBranch, + RiverGauge, School, Station, TyphoonTrackPoint, @@ -492,6 +493,11 @@ export class MapComponent implements AfterViewInit, OnDestroy { geoJsonPoint.properties as CommunityNotification, latlng, ); + case IbfLayerName.gauges: + return this.pointMarkerService.createMarkerRiverGauges( + geoJsonPoint.properties as RiverGauge, + latlng, + ); default: return this.pointMarkerService.createMarkerDefault(latlng); } @@ -502,7 +508,7 @@ export class MapComponent implements AfterViewInit, OnDestroy { const exposedClass = cluster .getAllChildMarkers() - .some((marker) => marker.feature.properties.exposed) + .some((marker) => marker.feature.properties.dynamicData?.exposure) ? ' exposed' : ''; @@ -557,7 +563,7 @@ export class MapComponent implements AfterViewInit, OnDestroy { }); const exposedLayerData = JSON.parse(JSON.stringify(layer.data)); exposedLayerData.features = exposedLayerData.features.filter( - (f) => f.properties.exposed, + (f) => f.properties.dynamicData?.exposure, ); const mapLayerExposed = geoJSON(exposedLayerData, { pointToLayer: this.getPointToLayerByLayer(layer.name), @@ -571,7 +577,7 @@ export class MapComponent implements AfterViewInit, OnDestroy { }); const nonExposedLayerData = JSON.parse(JSON.stringify(layer.data)); nonExposedLayerData.features = nonExposedLayerData.features.filter( - (f) => !f.properties.exposed, + (f) => !f.properties.dynamicData?.exposure, ); const mapLayerNotExposed = geoJSON(nonExposedLayerData, { pointToLayer: this.getPointToLayerByLayer(layer.name), diff --git a/interfaces/IBF-dashboard/src/app/config.ts b/interfaces/IBF-dashboard/src/app/config.ts index 9d6c2dceb..0a3379ace 100644 --- a/interfaces/IBF-dashboard/src/app/config.ts +++ b/interfaces/IBF-dashboard/src/app/config.ts @@ -79,6 +79,12 @@ export const LEAFLET_MARKER_ICON_OPTIONS_COMMUNITY_NOTIFICATION: IconOptions = { iconRetinaUrl: 'assets/markers/community-notification-marker.svg', }; +export const LEAFLET_MARKER_ICON_OPTIONS_RIVER_GAUGE: IconOptions = { + ...LEAFLET_MARKER_ICON_OPTIONS_BASE, + iconUrl: 'assets/markers/river-gauge-marker.svg', + iconRetinaUrl: 'assets/markers/river-gauge-marker.svg', +}; + export const LEAFLET_MAP_OPTIONS: MapOptions = { zoom: 5, layers: [], diff --git a/interfaces/IBF-dashboard/src/app/models/poi.model.ts b/interfaces/IBF-dashboard/src/app/models/poi.model.ts index 994549921..1e7086b50 100644 --- a/interfaces/IBF-dashboard/src/app/models/poi.model.ts +++ b/interfaces/IBF-dashboard/src/app/models/poi.model.ts @@ -8,6 +8,7 @@ export class Station { forecastLevel: number; eapAlertClass: string; forecastReturnPeriod: number; + dynamicData?: any; } export class TyphoonTrackPoint { @@ -35,7 +36,7 @@ export class Waterpoint { export class HealthSite { name: string; type: string; - exposed: boolean; + dynamicData?: { exposure: string }; } export enum HealthSiteType { @@ -61,13 +62,13 @@ export class EvacuationCenter { export class School { name: string; type: string; - exposed: boolean; + dynamicData?: { exposure: string }; } export class WaterpointInternal { name: string; type: string; - exposed: boolean; + dynamicData?: { exposure: string }; } export class CommunityNotification { public nameVolunteer: string; @@ -79,3 +80,15 @@ export class CommunityNotification { public pointDataId: string; public photoUrl: string; } + +export class RiverGauge { + fid: string; + name: string; + dynamicData: { + 'water-level': string; + 'water-level-previous': string; + 'water-level-reference': string; + }; + exposed: boolean; + pointDataId: string; +} diff --git a/interfaces/IBF-dashboard/src/app/services/api.service.ts b/interfaces/IBF-dashboard/src/app/services/api.service.ts index 90f9358f1..f0c119682 100644 --- a/interfaces/IBF-dashboard/src/app/services/api.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/api.service.ts @@ -166,13 +166,6 @@ export class ApiService { ); } - getStations( - countryCodeISO3: string, - leadTime: LeadTime = LeadTime.day7, - ): Observable { - return this.get(`glofas-stations/${countryCodeISO3}/${leadTime}`, false); - } - getTyphoonTrack( countryCodeISO3: string, eventName: string, diff --git a/interfaces/IBF-dashboard/src/app/services/map-legend.service.ts b/interfaces/IBF-dashboard/src/app/services/map-legend.service.ts index 82daf4e46..f3663a91f 100644 --- a/interfaces/IBF-dashboard/src/app/services/map-legend.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/map-legend.service.ts @@ -43,6 +43,7 @@ export class MapLegendService { [IbfLayerName.evacuationCenters]: 'evacuation-center-marker.svg', [IbfLayerName.schools]: 'school-marker.svg', [IbfLayerName.communityNotifications]: 'community-notification-marker.svg', + [IbfLayerName.gauges]: 'river-gauge-marker.svg', }; constructor( diff --git a/interfaces/IBF-dashboard/src/app/services/map.service.ts b/interfaces/IBF-dashboard/src/app/services/map.service.ts index fd1207643..b2d40b2f0 100644 --- a/interfaces/IBF-dashboard/src/app/services/map.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/map.service.ts @@ -183,8 +183,6 @@ export class MapService { this.loadAdminRegionLayer(layerActive, AdminLevel.adminLevel3); } else if (layer.name === IbfLayerName.adminRegions4) { this.loadAdminRegionLayer(layerActive, AdminLevel.adminLevel4); - } else if (layer.name === IbfLayerName.glofasStations) { - this.loadStationLayer(layer, layerActive); } else if (layer.name === IbfLayerName.typhoonTrack) { this.loadTyphoonTrackLayer(layer, layerActive); } else if (layer.name === IbfLayerName.waterpoints) { @@ -213,43 +211,6 @@ export class MapService { } } - private loadStationLayer(layer: IbfLayerMetadata, layerActive: boolean) { - if (this.country) { - if (layerActive) { - this.apiService - .getStations( - this.country.countryCodeISO3, - this.timelineState.activeLeadTime, - ) - .subscribe((stationData) => this.addStationLayer(layer, stationData)); - } else { - this.addStationLayer(layer, null); - } - } - } - - private addStationLayer = ( - layer: IbfLayerMetadata, - stations: GeoJSON.FeatureCollection, - ) => { - this.addLayer({ - 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( - IbfLayerName.glofasStations, - ) - : true, - show: true, - data: stations, - viewCenter: false, - order: 0, - }); - }; - private loadTyphoonTrackLayer(layer: IbfLayerMetadata, layerActive: boolean) { if (this.country) { if (layerActive) { @@ -660,13 +621,6 @@ export class MapService { layerData = this.apiService .getWaterPoints(this.country.countryCodeISO3) .pipe(shareReplay(1)); - } else if (layer.name === IbfLayerName.glofasStations) { - layerData = this.apiService - .getStations( - this.country.countryCodeISO3, - this.timelineState.activeLeadTime, - ) - .pipe(shareReplay(1)); } else if (layer.name === IbfLayerName.typhoonTrack) { layerData = this.apiService .getTyphoonTrack( diff --git a/interfaces/IBF-dashboard/src/app/services/point-marker.service.ts b/interfaces/IBF-dashboard/src/app/services/point-marker.service.ts index 4c1d87558..5205d2ae7 100644 --- a/interfaces/IBF-dashboard/src/app/services/point-marker.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/point-marker.service.ts @@ -15,6 +15,7 @@ import { LEAFLET_MARKER_ICON_OPTIONS_HEALTH_POINT, LEAFLET_MARKER_ICON_OPTIONS_HEALTH_POINT_EXPOSED, LEAFLET_MARKER_ICON_OPTIONS_RED_CROSS_BRANCH, + LEAFLET_MARKER_ICON_OPTIONS_RIVER_GAUGE, LEAFLET_MARKER_ICON_OPTIONS_SCHOOL, LEAFLET_MARKER_ICON_OPTIONS_SCHOOL_EXPOSED, LEAFLET_MARKER_ICON_OPTIONS_WATER_POINT, @@ -26,6 +27,7 @@ import { EvacuationCenter, HealthSite, RedCrossBranch, + RiverGauge, School, Station, TyphoonTrackPoint, @@ -35,10 +37,12 @@ import { import { AnalyticsEvent, AnalyticsPage } from '../analytics/analytics.enum'; import { AnalyticsService } from '../analytics/analytics.service'; import { CommunityNotificationPopupComponent } from '../components/community-notification-popup/community-notification-popup.component'; +import { DynamicPointPopupComponent } from '../components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component'; import { CountryDisasterSettings, EapAlertClasses, } from '../models/country.model'; +import { IbfLayerName } from '../types/ibf-layer'; import { LeadTime } from '../types/lead-time'; import { EventService } from './event.service'; @@ -121,10 +125,10 @@ export class PointMarkerService { const markerIcon: IconOptions = { ...LEAFLET_MARKER_ICON_OPTIONS_BASE, - iconUrl: `assets/markers/glofas-station-${markerProperties.eapAlertClass}-trigger.svg`, - iconRetinaUrl: `assets/markers/glofas-station-${markerProperties.eapAlertClass}-trigger.svg`, + iconUrl: `assets/markers/glofas-station-${markerProperties.dynamicData?.eapAlertClass}-trigger.svg`, + iconRetinaUrl: `assets/markers/glofas-station-${markerProperties.dynamicData?.eapAlertClass}-trigger.svg`, }; - const className = `trigger-popup-${markerProperties.eapAlertClass}`; + const className = `trigger-popup-${markerProperties.dynamicData?.eapAlertClass}`; const markerInstance = marker(markerLatLng, { title: markerTitle, @@ -165,12 +169,10 @@ export class PointMarkerService { ); let className = 'typhoon-track-icon'; - let passed = ''; if (markerDateTime > modelDateTime) { className += ' typhoon-track-icon-future'; } else { - passed = '(Passed)'; if (isLatest) { className += ' typhoon-track-icon-latest'; } else { @@ -186,12 +188,6 @@ export class PointMarkerService { markerProperties.timestampOfTrackpoint, ).toFormat('ccc, dd LLLL, HH:mm'); - const category = this.translate.instant( - 'map-popups.PHL.typhoon.category.' + markerProperties.category, - ); - - const coordinate = this.formatAsCoordinate(markerLatLng); - const markerInstance = marker(markerLatLng, { title: dateAndTime, icon: divIcon({ @@ -211,10 +207,10 @@ export class PointMarkerService { markerInstance.bindPopup( this.createMarkerTyphoonTrackPopup( - dateAndTime, - category, - coordinate, - passed, + markerProperties.timestampOfTrackpoint, + markerProperties.category, + markerLatLng, + markerDateTime <= modelDateTime, ), { minWidth: 300, @@ -273,7 +269,7 @@ export class PointMarkerService { const markerInstance = marker(markerLatLng, { title: markerTitle, icon: icon( - markerProperties.exposed + markerProperties.dynamicData?.exposure ? LEAFLET_MARKER_ICON_OPTIONS_HEALTH_POINT_EXPOSED : LEAFLET_MARKER_ICON_OPTIONS_HEALTH_POINT, ), @@ -341,7 +337,7 @@ export class PointMarkerService { const markerInstance = marker(markerLatLng, { title: markerTitle, icon: icon( - markerProperties.exposed + markerProperties.dynamicData?.exposure ? LEAFLET_MARKER_ICON_OPTIONS_SCHOOL_EXPOSED : LEAFLET_MARKER_ICON_OPTIONS_SCHOOL, ), @@ -366,7 +362,7 @@ export class PointMarkerService { const markerInstance = marker(markerLatLng, { title: markerTitle, icon: icon( - markerProperties.exposed + markerProperties.dynamicData?.exposure ? LEAFLET_MARKER_ICON_OPTIONS_WATER_POINT_EXPOSED : LEAFLET_MARKER_ICON_OPTIONS_WATER_POINT, ), @@ -382,6 +378,31 @@ export class PointMarkerService { return markerInstance; } + public createMarkerRiverGauges( + markerProperties: RiverGauge, + markerLatLng: LatLng, + ): Marker { + const markerTitle = markerProperties; + + const markerInstance = marker(markerLatLng, { + title: markerTitle.name, + icon: icon(LEAFLET_MARKER_ICON_OPTIONS_RIVER_GAUGE), + }); + markerInstance.bindPopup( + this.createMarkerRiverGaugePopup(markerProperties), + { + minWidth: 300, + className: 'river-gauge-popup', + }, + ); + markerInstance.on( + 'click', + this.onMapMarkerClick(AnalyticsEvent.redCrossBranch), + ); + + return markerInstance; + } + public createThresholdPopup( eapStatusColorText: string, title: string, @@ -408,121 +429,81 @@ export class PointMarkerService { const addComma = (n) => Math.round(n).toLocaleString('en-US'); - const forecastBar = ` -
-
-
${addComma(forecastValue)}
-
+ const headerContent = `${title}`; + + const thresholdBar = this.createThresholdBar( + eapStatusColor, + eapStatusColorText, + triggerWidth, + addComma(forecastValue), + thresholdName, + addComma(thresholdValue), + 80, + ); + + const middleContent = ` +
+ ${subtitle} +
+ ${thresholdBar} + `; + const footerContent = ` +
+ ${eapStatusText}
`; - const infoPopup = ` -
\ - ${title} - \ -
\ -
\ -
\ - ${subtitle} \ -
\ - ${forecastBar} -
\ - ${thresholdName}:
\ - \ -
${addComma( - thresholdValue, - )}
\ -
\ -
\ - ${eapStatusText} \ -
`; - - return infoPopup; + return this.createDynamicPointPopup( + eapStatusColor, + headerContent, + middleContent, + footerContent, + ); } private createMarkerStationPopup( markerProperties: Station, countryDisasterSettings: CountryDisasterSettings, activeLeadTime: LeadTime, - ): string { - const eapAlertClasses = - countryDisasterSettings?.eapAlertClasses || ({} as EapAlertClasses); - const eapAlertClass = eapAlertClasses[markerProperties.eapAlertClass]; - - const eapStatusText = eapAlertClass?.label; - const eapStatusColor = `var(--ion-color-${eapAlertClass?.color})`; - const eapStatusColorText = `var(--ion-color-${eapAlertClass?.color}-contrast)`; - - const title = - markerProperties.stationCode + - ' STATION: ' + - markerProperties.stationName; - + ) { const leadTimes = countryDisasterSettings?.activeLeadTimes; const lastAvailableLeadTime: LeadTime = leadTimes[leadTimes.length - 1]; const leadTime = activeLeadTime || lastAvailableLeadTime; - const subtitle = `${leadTime} forecast of river discharge in m3/s \ - ${ - markerProperties.forecastReturnPeriod - ? `
(Corresponding to a return period of ${markerProperties.forecastReturnPeriod} years)` - : '' - }`; + const eapAlertClasses = + countryDisasterSettings?.eapAlertClasses || ({} as EapAlertClasses); - const thresholdName = 'Trigger activation threshold'; - const stationInfoPopup = this.createThresholdPopup( - eapStatusColorText, - title, - eapStatusColor, - eapStatusText, - markerProperties.forecastLevel, - markerProperties.triggerLevel, - subtitle, - thresholdName, - ); - return stationInfoPopup; + const component = this.componentFactoryResolver + .resolveComponentFactory(DynamicPointPopupComponent) + .create(this.injector); + component.instance.layerName = IbfLayerName.glofasStations; + component.instance.glofasData = { + station: markerProperties, + leadTime, + eapAlertClasses, + }; + component.changeDetectorRef.detectChanges(); + return component.location.nativeElement; } private createMarkerTyphoonTrackPopup( - dateAndTime: string, + timestamp: string, category: string, - coordinate: string, - passed: string, + markerLatLng: LatLng, + passed: boolean, ): string { - const bg = 'var(--ion-color-ibf-primary)'; - const color = 'var(--ion-color-ibf-white)'; - - const trackpointInfoPopup = ` -
-
- TYPHOON TRACK ${passed} -
-
-
-
Date and time: ${dateAndTime}
-
Category (ECWMF): ${category}
-
-
- - - -
-
-
- Coordinate: ${coordinate} -
-
- `; - return trackpointInfoPopup; + const component = this.componentFactoryResolver + .resolveComponentFactory(DynamicPointPopupComponent) + .create(this.injector); + component.instance.layerName = IbfLayerName.typhoonTrack; + component.instance.typhoonTrackPoint = { + timestamp, + category, + markerLatLng, + passed, + }; + component.changeDetectorRef.detectChanges(); + return component.location.nativeElement; } private createMarkerRedCrossPopup(markerProperties: RedCrossBranch): string { @@ -647,4 +628,85 @@ export class PointMarkerService { return markerInstance; } + + private createMarkerRiverGaugePopup(markerProperties: RiverGauge) { + const component = this.componentFactoryResolver + .resolveComponentFactory(DynamicPointPopupComponent) + .create(this.injector); + component.instance.layerName = IbfLayerName.gauges; + component.instance.riverGauge = markerProperties; + component.changeDetectorRef.detectChanges(); + return component.location.nativeElement; + } + + private createDynamicPointPopup( + accentColor: string, + headerContent: string, + middleContent: string, + footerContent: string, + ): string { + const contrastColor = 'var(--ion-color-ibf-white)'; + + return ` +
+ ${headerContent} +
+
+ ${middleContent} +
+
+
+ ${footerContent} +
+
+ + `; + } + + private createThresholdBar( + backgroundColor: string, + textColor: string, + barWidth: number, + barValue: string, + thresholdDescription: string, + thresholdValue: string, + thresholdPosition: number, // width percentage to position threshold on bar + ): string { + return ` +
+
+
+
${barValue} +
+
+
+
+
+ ${thresholdDescription}: +
+
+ ${thresholdValue} +
+ `; + } } diff --git a/interfaces/IBF-dashboard/src/app/shared.module.ts b/interfaces/IBF-dashboard/src/app/shared.module.ts index 550c79a4c..33257293c 100644 --- a/interfaces/IBF-dashboard/src/app/shared.module.ts +++ b/interfaces/IBF-dashboard/src/app/shared.module.ts @@ -30,6 +30,11 @@ import { IbfButtonComponent } from './components/ibf-button/ibf-button.component import { IbfGuideButtonComponent } from './components/ibf-guide-button/ibf-guide-button.component'; import { IbfGuidePopoverComponent } from './components/ibf-guide-popover/ibf-guide-popover.component'; import { LayerControlInfoPopoverComponent } from './components/layer-control-info-popover/layer-control-info-popover.component'; +import { DynamicPointPopupComponent } from './components/leaflet-popup/dynamic-point-popup/dynamic-point-popup.component'; +import { GlofasStationPopupContentComponent } from './components/leaflet-popup/glofas-station-popup-content/glofas-station-popup-content.component'; +import { RiverGaugePopupContentComponent } from './components/leaflet-popup/river-gauge-popup-content/river-gauge-popup-content.component'; +import { ThresholdBarComponent } from './components/leaflet-popup/threshold-bar/threshold-bar.component'; +import { TyphoonTrackpointPopupContentComponent } from './components/leaflet-popup/typhoon-trackpoint-popup-content/typhoon-trackpoint-popup-content.component'; import { LoginFormComponent } from './components/login-form/login-form.component'; import { LogosComponent } from './components/logos/logos.component'; import { MapControlsComponent } from './components/map-controls/map-controls.component'; @@ -91,6 +96,11 @@ import { BackendMockScenarioComponent } from './mocks/backend-mock-scenario-comp TooltipComponent, TooltipPopoverComponent, UserStateComponent, + RiverGaugePopupContentComponent, + ThresholdBarComponent, + DynamicPointPopupComponent, + TyphoonTrackpointPopupContentComponent, + GlofasStationPopupContentComponent, ], exports: [ AboutBtnComponent, @@ -131,6 +141,11 @@ import { BackendMockScenarioComponent } from './mocks/backend-mock-scenario-comp TooltipPopoverComponent, TranslateModule, UserStateComponent, + RiverGaugePopupContentComponent, + ThresholdBarComponent, + DynamicPointPopupComponent, + TyphoonTrackpointPopupContentComponent, + GlofasStationPopupContentComponent, ], }) export class SharedModule {} diff --git a/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts b/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts index d71d9581e..55b63d079 100644 --- a/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts +++ b/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts @@ -123,6 +123,7 @@ export enum IbfLayerName { schools = 'schools', waterpointsInternal = 'waterpoints_internal', roads = 'roads', + gauges = 'gauges', } export enum IbfLayerLabel { diff --git a/interfaces/IBF-dashboard/src/assets/i18n/en.json b/interfaces/IBF-dashboard/src/assets/i18n/en.json index 3d85dad90..8ea3ffa79 100644 --- a/interfaces/IBF-dashboard/src/assets/i18n/en.json +++ b/interfaces/IBF-dashboard/src/assets/i18n/en.json @@ -380,6 +380,17 @@ "photo-popup": { "title": "Community notification - photo" } + }, + "river-gauge": { + "unit": "m", + "sea-level": "MSL", + "header": "River gauge:", + "current": "Current water level", + "normal": "Reference water level", + "below": "Below reference level", + "above": "Above reference level", + "no-data": "No water level data available", + "tooltip": "Current water level in meters above sea level (mMSL) is measured by a gauge during the latest model run. The difference compared to 24 hours ago is shown in green for decrease or red for increase. The reference water level is the typical level for this station during this season." } }, "breadcrumbs": { diff --git a/interfaces/IBF-dashboard/src/assets/markers/river-gauge-marker.svg b/interfaces/IBF-dashboard/src/assets/markers/river-gauge-marker.svg new file mode 100644 index 000000000..175604fba --- /dev/null +++ b/interfaces/IBF-dashboard/src/assets/markers/river-gauge-marker.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/interfaces/IBF-dashboard/src/global.scss b/interfaces/IBF-dashboard/src/global.scss index 7d23bb52e..7a17c23fe 100644 --- a/interfaces/IBF-dashboard/src/global.scss +++ b/interfaces/IBF-dashboard/src/global.scss @@ -70,7 +70,8 @@ } .leaflet-popup-content-wrapper { - border-radius: 0; + border-radius: 8px; + font-size: 12px; } .leaflet-ibf-aggregate-pane { @@ -98,7 +99,8 @@ .trigger-popup-min, .trigger-popup-med, .trigger-popup-max, -.typhoon-track-popup { +.typhoon-track-popup, +.river-gauge-popup { .leaflet-popup-content-wrapper { padding: 0; @@ -107,40 +109,21 @@ } } } -.trigger-popup-no { - .leaflet-popup-tip { - background-color: var(--ion-color-ibf-no-alert-primary); - } -} -.trigger-popup-min { - .leaflet-popup-tip { - background-color: var(--ion-color-ibf-yellow); - } -} -.trigger-popup-med { - .leaflet-popup-tip { - background-color: var(--ion-color-ibf-orange); - } -} -.trigger-popup-max { - .leaflet-popup-tip { - background-color: var(--ion-color-ibf-trigger-alert-primary); - } -} -.typhoon-track-popup { +.trigger-popup-no, +.trigger-popup-min, +.trigger-popup-med, +.trigger-popup-max, +.typhoon-track-popup, +.river-gauge-popup { a { &.leaflet-popup-close-button { color: var(--ion-color-ibf-white); font-size: 20px; - padding-top: 11px; - margin-right: 7px; + padding-top: 5px; + margin-right: 3px; } } - - .leaflet-popup-tip { - background-color: var(--ion-color-ibf-primary); - } } .waterpoint-cluster-10 { @@ -276,9 +259,11 @@ input:-webkit-autofill:focus { } app-tooltip { - position: absolute; - top: 4px; - right: 8px; + &.absolute { + position: absolute; + top: 4px; + right: 8px; + } } .tooltip--container { diff --git a/interfaces/IBF-dashboard/src/theme/ibf-colors/_index.scss b/interfaces/IBF-dashboard/src/theme/ibf-colors/_index.scss index e7be0be4d..859e5b72c 100644 --- a/interfaces/IBF-dashboard/src/theme/ibf-colors/_index.scss +++ b/interfaces/IBF-dashboard/src/theme/ibf-colors/_index.scss @@ -65,22 +65,29 @@ --ion-color-ibf-grey: var(--fiveten-neutral-300); - --ion-color-ibf-yellow: #feff02; + --ion-color-ibf-yellow: #fff500; --ion-color-ibf-yellow-rgb: 254, 255, 2; - --ion-color-ibf-yellow-contrast: #000000; + --ion-color-ibf-yellow-contrast: #7d6906; --ion-color-ibf-yellow-contrast-rgb: 0, 0, 0; - --ion-color-ibf-yellow-shade: #feff02; - --ion-color-ibf-yellow-tint: #feff02; + --ion-color-ibf-yellow-shade: #fff500; + --ion-color-ibf-yellow-tint: #fff500; - --ion-color-ibf-orange: #fcb50c; + --ion-color-ibf-orange: #f0890d; --ion-color-ibf-orange-rgb: 252, 181, 12; - --ion-color-ibf-orange-contrast: #000000; + --ion-color-ibf-orange-contrast: white; --ion-color-ibf-orange-contrast-rgb: 0, 0, 0; - --ion-color-ibf-orange-shade: #fcb50c; - --ion-color-ibf-orange-tint: #fcb50c; + --ion-color-ibf-orange-shade: #f0890d; + --ion-color-ibf-orange-tint: #f0890d; --ion-color-ibf-outline-red: var(--fiveten-error-500); + --ion-color-success: var(--fiveten-success-500); + --ion-color-success-rgb: var(--fiveten-success-500-rgb); + --ion-color-success-contrast: var(--fiveten-neutral-000); + --ion-color-success-contrast-rgb: var(--fiveten-neutral-000); + --ion-color-success-shade: var(--fiveten-success-700); + --ion-color-success-tint: var(--fiveten-success-300); + --ion-color-danger: var(--fiveten-error-500); --ion-color-danger-rgb: var(--fiveten-error-500-rgb); --ion-color-danger-contrast: var(--fiveten-neutral-000); diff --git a/services/API-service/migration/1700396447482-PointReferenceIdString.ts b/services/API-service/migration/1700396447482-PointReferenceIdString.ts new file mode 100644 index 000000000..f06b02586 --- /dev/null +++ b/services/API-service/migration/1700396447482-PointReferenceIdString.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class PointReferenceIdString1700396447482 implements MigrationInterface { + name = 'PointReferenceIdString1700396447482'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "IBF-app"."point-data" RENAME COLUMN "referenceId" TO "referenceIdOld"`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."point-data" ADD "referenceId" character varying`, + ); + await queryRunner.query( + `UPDATE "IBF-app"."point-data" SET "referenceId" = "referenceIdOld"::varchar`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."point-data" DROP COLUMN "referenceIdOld"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "IBF-app"."point-data" DROP COLUMN "referenceId"`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."point-data" ADD "referenceId" integer`, + ); + } +} diff --git a/services/API-service/migration/1701157179286-DynamicPointData.ts b/services/API-service/migration/1701157179286-DynamicPointData.ts new file mode 100644 index 000000000..6bc8fd22f --- /dev/null +++ b/services/API-service/migration/1701157179286-DynamicPointData.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class DynamicPointData1701157179286 implements MigrationInterface { + name = 'DynamicPointData1701157179286'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "IBF-app"."dynamic-point-data" ("dynamicPointDataId" uuid NOT NULL DEFAULT uuid_generate_v4(), "timestamp" TIMESTAMP NOT NULL, "key" character varying NOT NULL, "value" character varying, "pointPointDataId" uuid, "leadTime" character varying, CONSTRAINT "PK_4f51e8c4a1091afa35e3cc51b34" PRIMARY KEY ("dynamicPointDataId"))`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."dynamic-point-data" ADD CONSTRAINT "FK_9aaa9be5dbe82b610209bcb456b" FOREIGN KEY ("pointPointDataId") REFERENCES "IBF-app"."point-data"("pointDataId") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."dynamic-point-data" ADD CONSTRAINT "FK_289a1f52e25e270d9a28bd9d35a" FOREIGN KEY ("leadTime") REFERENCES "IBF-app"."lead-time"("leadTimeName") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_9ca340240072b8ece6e7b8ae61" ON "IBF-app"."dynamic-point-data" ("timestamp") `, + ); + + // Migrate static + dynamic glofas data + await queryRunner.query(`INSERT INTO "IBF-app"."point-data" + ("pointDataId", "countryCodeISO3", "pointDataCategory", "attributes", geom, "referenceId") + select id + ,"countryCodeISO3" + ,'glofas_stations' + ,(cast('{"stationName":"' as varchar) || "stationName" || cast('","stationCode":"' || "stationCode" || '"}' as varchar))::json + ,geom + ,"stationCode" + from "IBF-app"."glofas-station" `); + + await queryRunner.query(`INSERT INTO "IBF-app"."dynamic-point-data" + ("dynamicPointDataId", "timestamp", "key", value, "pointPointDataId", "leadTime") + select uuid_generate_v4() + ,date + ,unnest(array['forecastLevel','forecastReturnPeriod','triggerLevel','eapAlertClass']) as key + ,unnest(array[cast("forecastLevel" as varchar),cast("forecastReturnPeriod" as varchar),cast("triggerLevel" as varchar),"eapAlertClass"]) as value + ,"glofasStationId" + ,"leadTime" + from "IBF-app"."glofas-station-forecast" gsf `); + + await queryRunner.query(`DROP TABLE "IBF-app"."point-data-dynamic-status"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX "IBF-app"."IDX_9ca340240072b8ece6e7b8ae61"`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."dynamic-point-data" DROP CONSTRAINT "FK_289a1f52e25e270d9a28bd9d35a"`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."dynamic-point-data" DROP CONSTRAINT "FK_9aaa9be5dbe82b610209bcb456b"`, + ); + await queryRunner.query(`DROP TABLE "IBF-app"."dynamic-point-data"`); + } +} diff --git a/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts b/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts index 7734b25d5..55f0f244a 100644 --- a/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts +++ b/services/API-service/src/api/glofas-station/dto/upload-trigger-per-station.ts @@ -9,7 +9,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; import { Type } from 'class-transformer'; import { GlofasStationForecastDto } from './station-forecast.dto'; -import stations from './example/glofas-stations-UGA-triggered.json'; +import stations from '../../point-data/dto/example/glofas-stations/glofas-stations-UGA-triggered.json'; export class UploadTriggerPerStationDto { @ApiProperty({ example: 'UGA' }) diff --git a/services/API-service/src/api/glofas-station/glofas-station-forecast.entity.ts b/services/API-service/src/api/glofas-station/glofas-station-forecast.entity.ts deleted file mode 100644 index 8266ec4e0..000000000 --- a/services/API-service/src/api/glofas-station/glofas-station-forecast.entity.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; -import { LeadTimeEntity } from '../lead-time/lead-time.entity'; -import { GlofasStationEntity } from './glofas-station.entity'; - -@Entity('glofas-station-forecast') -export class GlofasStationForecastEntity { - @ApiProperty({ example: '6b9b7669-4839-4fdb-9645-9070a27bda86' }) - @PrimaryGeneratedColumn('uuid') - public id: string; - - @ApiProperty({ example: LeadTime.day7 }) - @ManyToOne((): typeof LeadTimeEntity => LeadTimeEntity) - @JoinColumn({ name: 'leadTime', referencedColumnName: 'leadTimeName' }) - public leadTime: string; - - @ApiProperty({ example: new Date() }) - @Column({ type: 'date' }) - public date: Date; - - @ManyToOne( - (): typeof GlofasStationEntity => GlofasStationEntity, - (station): GlofasStationForecastEntity[] => station.stationForecasts, - ) - public glofasStation: GlofasStationEntity; - - @ApiProperty({ example: 100 }) - @Column({ type: 'double precision' }) - public forecastLevel: string; - - @ApiProperty({ example: 'no' }) - @Column({ nullable: true }) - public eapAlertClass: string; - - @ApiProperty({ example: 10 }) - @Column({ nullable: true }) - public forecastReturnPeriod: number; - - @ApiProperty({ example: 100 }) - @Column({ type: 'double precision', default: 0 }) - public triggerLevel: number; -} diff --git a/services/API-service/src/api/glofas-station/glofas-station.controller.ts b/services/API-service/src/api/glofas-station/glofas-station.controller.ts index 40ec892f7..8519c7cc5 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.controller.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.controller.ts @@ -1,32 +1,15 @@ -import { - Body, - Controller, - Get, - Param, - Post, - UploadedFile, - UseGuards, - UseInterceptors, -} from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, - ApiBody, - ApiConsumes, ApiOperation, - ApiParam, ApiResponse, ApiTags, } from '@nestjs/swagger'; import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; -import { GeoJson } from '../../shared/geo.model'; import { UserRole } from '../user/user-role.enum'; import { UploadTriggerPerStationDto } from './dto/upload-trigger-per-station'; -import { GlofasStationForecastEntity } from './glofas-station-forecast.entity'; -import { GlofasStationEntity } from './glofas-station.entity'; import { GlofasStationService } from './glofas-station.service'; -import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; @ApiBearerAuth() @UseGuards(RolesGuard) @@ -39,44 +22,6 @@ export class GlofasStationController { this.glofasStationService = glofasStationService; } - @ApiOperation({ - summary: - 'Get Glofas station locations and attributes for given country (used by IBF-pipeline)', - }) - @ApiParam({ name: 'countryCodeISO3', required: true, type: 'string' }) - @ApiResponse({ - status: 200, - description: 'Glofas station locations and attributes for given country.', - type: [GlofasStationEntity], - }) - @Get(':countryCodeISO3') - public async getStationsByCountry( - @Param() params, - ): Promise { - return await this.glofasStationService.getStationsByCountry( - params.countryCodeISO3, - ); - } - - @ApiOperation({ - summary: 'Get Glofas station forecast data for given country and leadtime', - }) - @ApiParam({ name: 'countryCodeISO3', required: true, type: 'string' }) - @ApiParam({ name: 'leadTime', required: true, type: 'string' }) - @ApiResponse({ - status: 200, - description: - 'Glofas station forecast data for given country and leadtime in GEOJSON format.', - type: GeoJson, - }) - @Get(':countryCodeISO3/:leadTime') - public async getStationForecastByLeadTime(@Param() params): Promise { - return await this.glofasStationService.getStationForecastByLeadTime( - params.countryCodeISO3, - params.leadTime, - ); - } - @Roles(UserRole.PipelineUser) @ApiOperation({ summary: @@ -85,42 +30,13 @@ export class GlofasStationController { @ApiResponse({ status: 201, description: 'Uploaded Glofas forecast data per station', - type: [GlofasStationForecastEntity], }) @Post('triggers') public async uploadTriggerDataPerStation( @Body() uploadTriggerPerStation: UploadTriggerPerStationDto, - ): Promise { - return await this.glofasStationService.uploadTriggerDataPerStation( - uploadTriggerPerStation, - ); - } - - @Roles(UserRole.Admin) - @ApiOperation({ - summary: - 'Upload (and overwrite) via CSV Glofas station locations and attributes for given country', - }) - @ApiResponse({ - status: 201, - description: 'Uploaded Glofas station locations and attributes', - }) - @ApiResponse({ - status: 400, - description: 'Validation errors in content of CSV', - }) - @ApiParam({ name: 'countryCodeISO3', required: true, type: 'string' }) - @Post('upload-csv/:countryCodeISO3') - @ApiConsumes('multipart/form-data') - @ApiBody(FILE_UPLOAD_API_FORMAT) - @UseInterceptors(FileInterceptor('file')) - public async uploadCsv( - @UploadedFile() glofasStationData, - @Param() params, ): Promise { - await this.glofasStationService.uploadCsv( - glofasStationData, - params.countryCodeISO3, + await this.glofasStationService.uploadTriggerDataPerStation( + uploadTriggerPerStation, ); } } diff --git a/services/API-service/src/api/glofas-station/glofas-station.entity.ts b/services/API-service/src/api/glofas-station/glofas-station.entity.ts deleted file mode 100644 index c7dda7ebd..000000000 --- a/services/API-service/src/api/glofas-station/glofas-station.entity.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - JoinColumn, - OneToMany, -} from 'typeorm'; -import { GeoJson } from '../../shared/geo.model'; -import { CountryEntity } from '../country/country.entity'; -import { GlofasStationForecastEntity } from './glofas-station-forecast.entity'; - -@Entity('glofas-station') -export class GlofasStationEntity { - @ApiProperty({ example: '6b9b7669-4839-4fdb-9645-9070a27bda86' }) - @PrimaryGeneratedColumn('uuid') - public id: string; - - @ApiProperty({ example: 'UGA' }) - @ManyToOne((): typeof CountryEntity => CountryEntity) - @JoinColumn({ - name: 'countryCodeISO3', - referencedColumnName: 'countryCodeISO3', - }) - public countryCodeISO3: string; - - @ApiProperty({ example: 'G1373' }) - @Column() - public stationCode: string; - - @ApiProperty({ example: [GlofasStationForecastEntity] }) - @OneToMany( - (): typeof GlofasStationForecastEntity => GlofasStationForecastEntity, - (station): GlofasStationEntity => station.glofasStation, - ) - public stationForecasts: GlofasStationForecastEntity[]; - - @ApiProperty() - @Column({ nullable: true }) - public stationName: string; - - @ApiProperty({ example: 10 }) - @Column({ nullable: true, type: 'double precision' }) - public lat: string; - - @ApiProperty({ example: 10 }) - @Column({ nullable: true, type: 'double precision' }) - public lon: string; - - @ApiProperty({ example: new GeoJson() }) - @Column('json', { nullable: true }) - public geom: JSON; -} diff --git a/services/API-service/src/api/glofas-station/glofas-station.module.ts b/services/API-service/src/api/glofas-station/glofas-station.module.ts index 59d5e5a54..b4e05210f 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.module.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.module.ts @@ -6,24 +6,22 @@ import { AdminAreaEntity } from '../admin-area/admin-area.entity'; import { CountryEntity } from '../country/country.entity'; import { EventModule } from '../event/event.module'; import { UserModule } from '../user/user.module'; -import { GlofasStationForecastEntity } from './glofas-station-forecast.entity'; import { GlofasStationController } from './glofas-station.controller'; -import { GlofasStationEntity } from './glofas-station.entity'; import { GlofasStationService } from './glofas-station.service'; import { HttpModule } from '@nestjs/axios'; +import { PointDataModule } from '../point-data/point-data.module'; @Module({ imports: [ HttpModule, UserModule, TypeOrmModule.forFeature([ - GlofasStationEntity, - GlofasStationForecastEntity, AdminAreaEntity, AdminAreaDynamicDataEntity, CountryEntity, ]), EventModule, + PointDataModule, ], providers: [GlofasStationService, HelperService], controllers: [GlofasStationController], diff --git a/services/API-service/src/api/glofas-station/glofas-station.service.ts b/services/API-service/src/api/glofas-station/glofas-station.service.ts index fb29fb166..55217db78 100644 --- a/services/API-service/src/api/glofas-station/glofas-station.service.ts +++ b/services/API-service/src/api/glofas-station/glofas-station.service.ts @@ -1,71 +1,18 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; -import { GeoJson } from '../../shared/geo.model'; import { UploadTriggerPerStationDto } from './dto/upload-trigger-per-station'; -import { GlofasStationForecastEntity } from './glofas-station-forecast.entity'; -import { GlofasStationEntity } from './glofas-station.entity'; -import { HelperService } from '../../shared/helper.service'; import { DisasterType } from '../disaster/disaster-type.enum'; import { CountryEntity } from '../country/country.entity'; -import { validate } from 'class-validator'; -import { UploadStationDto } from './dto/upload-station.dto'; +import { PointDataService } from '../point-data/point-data.service'; +import { UploadDynamicPointDataDto } from '../point-data/dto/upload-asset-exposure-status.dto'; @Injectable() export class GlofasStationService { - @InjectRepository(GlofasStationEntity) - private readonly glofasStationRepository: Repository; - @InjectRepository(GlofasStationForecastEntity) - private readonly glofasStationForecastRepository: Repository; @InjectRepository(CountryEntity) private readonly countryRepository: Repository; - public constructor(private readonly helperService: HelperService) {} - - public async getStationsByCountry( - countryCodeISO3: string, - ): Promise { - return await this.glofasStationRepository.find({ - where: { countryCodeISO3: countryCodeISO3 }, - }); - } - - public async getStationForecastByLeadTime( - countryCodeISO3: string, - leadTime: LeadTime, - ): Promise { - const lastTriggeredDate = await this.helperService.getRecentDate( - countryCodeISO3, - DisasterType.Floods, - ); - const stationForecasts = await this.glofasStationRepository - .createQueryBuilder('station') - .select([ - '"countryCodeISO3"', - '"leadTime"', - '"stationCode"', - '"stationName"', - 'geom', - 'forecast."forecastLevel" AS "forecastLevel"', - 'forecast."eapAlertClass" AS "eapAlertClass"', - 'forecast."forecastReturnPeriod" AS "forecastReturnPeriod"', - 'forecast."triggerLevel" AS "triggerLevel"', - ]) - .leftJoin('station.stationForecasts', 'forecast') - .where('"leadTime" = :leadTime', { - leadTime: leadTime, - }) - .andWhere('"countryCodeISO3" = :countryCodeISO3', { - countryCodeISO3: countryCodeISO3, - }) - .andWhere('date = :lastTriggeredDate', { - lastTriggeredDate: lastTriggeredDate.date, - }) - .getRawMany(); - - return this.helperService.toGeojson(stationForecasts); - } + public constructor(private readonly pointDataService: PointDataService) {} private async validateEapAlertClass( uploadTriggerPerStation: UploadTriggerPerStationDto, @@ -95,120 +42,28 @@ export class GlofasStationService { public async uploadTriggerDataPerStation( uploadTriggerPerStation: UploadTriggerPerStationDto, - ): Promise { + ): Promise { await this.validateEapAlertClass(uploadTriggerPerStation); - const stationForecasts: GlofasStationForecastEntity[] = []; - for await (const station of uploadTriggerPerStation.stationForecasts) { - const glofasStation = await this.glofasStationRepository.findOne({ - where: { stationCode: station.stationCode }, - relations: ['stationForecasts'], - }); - - // Delete existing entries with same date, leadTime and countryCodeISO3 and stationCode - await this.glofasStationForecastRepository.delete({ - glofasStation: { id: glofasStation.id }, - leadTime: uploadTriggerPerStation.leadTime, - date: uploadTriggerPerStation.date || new Date(), - }); - - const stationForecast = new GlofasStationForecastEntity(); - stationForecast.glofasStation = glofasStation; - stationForecast.leadTime = uploadTriggerPerStation.leadTime; - stationForecast.date = uploadTriggerPerStation.date || new Date(); - stationForecast.forecastLevel = station.forecastLevel; - stationForecast.eapAlertClass = station.eapAlertClass; - stationForecast.forecastReturnPeriod = station.forecastReturnPeriod; - stationForecast.triggerLevel = station.triggerLevel; - stationForecasts.push(stationForecast); - } - return await this.glofasStationForecastRepository.save(stationForecasts); - } - - public async uploadJson( - countryCodeISO3: string, - newStations: UploadStationDto[], - ) { - for (const station of newStations) { - const existingStation = await this.glofasStationRepository.findOne({ - where: { - countryCodeISO3: countryCodeISO3, - stationCode: station.stationCode, + const keys = [ + 'forecastLevel', + 'forecastReturnPeriod', + 'triggerLevel', + 'eapAlertClass', + ]; + const date = uploadTriggerPerStation.date || new Date(); + for await (const key of keys) { + const payload = new UploadDynamicPointDataDto(); + payload.key = key; + payload.leadTime = uploadTriggerPerStation.leadTime; + payload.date = date; + payload.disasterType = DisasterType.Floods; + payload.dynamicPointData = uploadTriggerPerStation.stationForecasts.map( + (f) => { + return { fid: f.stationCode, value: f[key] }; }, - }); - if (existingStation) { - this.glofasStationRepository - .createQueryBuilder() - .update() - .set({ - stationName: station.stationName, - lat: String(station.lat), - lon: String(station.lon), - geom: (): string => - `st_asgeojson(st_MakePoint(${station.lon}, ${station.lat}))::json`, - }) - .where({ - countryCodeISO3: countryCodeISO3, - stationCode: station.stationCode, - }) - .execute(); - } else { - this.glofasStationRepository - .createQueryBuilder() - .insert() - .values({ - countryCodeISO3: countryCodeISO3, - stationCode: station.stationCode, - stationName: station.stationName, - lat: String(station.lat), - lon: String(station.lon), - geom: (): string => - `st_asgeojson(st_MakePoint(${station.lon}, ${station.lat}))::json`, - }) - .execute(); - } - } - - const existingStations = await this.glofasStationRepository.find({ - where: { countryCodeISO3: countryCodeISO3 }, - }); - const newStationCodes = newStations.map((s) => s.stationCode); - for (const station of existingStations) { - if (!newStationCodes.includes(station.stationCode)) { - await this.glofasStationForecastRepository.delete({ - glofasStation: { id: station.id }, - }); - await this.glofasStationRepository.remove(station); - } - } - } - - public async uploadCsv(data, countryCodeISO3: string): Promise { - const objArray = await this.helperService.csvBufferToArray(data.buffer); - const validatedObjArray = await this.validateArray(objArray); - - await this.uploadJson(countryCodeISO3, validatedObjArray); - } - - public async validateArray(csvArray): Promise { - const errors = []; - const validatatedArray = []; - for (const [i, row] of csvArray.entries()) { - const dto = new UploadStationDto(); - dto.stationCode = row.station_code; - dto.stationName = row.station_name; - dto.lat = row.lat; - dto.lon = row.lon; - const result = await validate(dto); - if (result.length > 0) { - const errorObj = { lineNumber: i + 1, validationError: result }; - errors.push(errorObj); - } - validatatedArray.push(dto); - } - if (errors.length > 0) { - throw new HttpException(errors, HttpStatus.BAD_REQUEST); + ); + await this.pointDataService.uploadDynamicPointData(payload); } - return validatatedArray; } } diff --git a/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/dynamic-point-data_water-level-previous.json b/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/dynamic-point-data_water-level-previous.json new file mode 100644 index 000000000..132b8fa82 --- /dev/null +++ b/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/dynamic-point-data_water-level-previous.json @@ -0,0 +1,14 @@ +[ + { + "fid": 1, + "value": 71 + }, + { + "fid": 2, + "value": 83 + }, + { + "fid": 3, + "value": 114 + } +] diff --git a/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/dynamic-point-data_water-level-reference.json b/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/dynamic-point-data_water-level-reference.json new file mode 100644 index 000000000..4f206b111 --- /dev/null +++ b/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/dynamic-point-data_water-level-reference.json @@ -0,0 +1,14 @@ +[ + { + "fid": 1, + "value": 90 + }, + { + "fid": 2, + "value": 90 + }, + { + "fid": 3, + "value": 110 + } +] diff --git a/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/dynamic-point-data_water-level.json b/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/dynamic-point-data_water-level.json new file mode 100644 index 000000000..ef85b1d9c --- /dev/null +++ b/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/dynamic-point-data_water-level.json @@ -0,0 +1,14 @@ +[ + { + "fid": 1, + "value": 85.24 + }, + { + "fid": 2, + "value": 95.23 + }, + { + "fid": 3, + "value": 100.13 + } +] diff --git a/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/waterpoints_internal.json b/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/waterpoints_internal.json index 6d1226fbf..f3fa50ea3 100644 --- a/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/waterpoints_internal.json +++ b/services/API-service/src/api/point-data/dto/example/MWI/flash-floods/waterpoints_internal.json @@ -1,249 +1,249 @@ { "24-hour": [ - "179", - "273", - "408", - "908", - "1255", - "3413", - "3583", - "3952", - "5402", - "5984", - "6116", - "6142", - "6146", - "7386", - "7889", - "7955", - "11607", - "11647", - "11691", - "13455", - "13953", - "13957", - "14502", - "14511", - "14522", - "14556", - "14835", - "15356", - "15849", - "15871", - "16611", - "16998", - "18130", - "18134", - "18138", - "18143", - "19855", - "20594", - "23129", - "23213", - "23553", - "23833", - "23979", - "23995", - "24286", - "25295", - "25727", - "26497", - "27415", - "27420", - "27468", - "27706", - "27730", - "27949", - "29078", - "29392", - "29553", - "29656", - "29667", - "29811", - "30225", - "30524", - "30720", - "31152", - "31365", - "31442", - "31458", - "31460", - "31607", - "33759", - "34206", - "35588" + { "fid": "179", "value": "true" }, + { "fid": "273", "value": "true" }, + { "fid": "408", "value": "true" }, + { "fid": "908", "value": "true" }, + { "fid": "1255", "value": "true" }, + { "fid": "3413", "value": "true" }, + { "fid": "3583", "value": "true" }, + { "fid": "3952", "value": "true" }, + { "fid": "5402", "value": "true" }, + { "fid": "5984", "value": "true" }, + { "fid": "6116", "value": "true" }, + { "fid": "6142", "value": "true" }, + { "fid": "6146", "value": "true" }, + { "fid": "7386", "value": "true" }, + { "fid": "7889", "value": "true" }, + { "fid": "7955", "value": "true" }, + { "fid": "11607", "value": "true" }, + { "fid": "11647", "value": "true" }, + { "fid": "11691", "value": "true" }, + { "fid": "13455", "value": "true" }, + { "fid": "13953", "value": "true" }, + { "fid": "13957", "value": "true" }, + { "fid": "14502", "value": "true" }, + { "fid": "14511", "value": "true" }, + { "fid": "14522", "value": "true" }, + { "fid": "14556", "value": "true" }, + { "fid": "14835", "value": "true" }, + { "fid": "15356", "value": "true" }, + { "fid": "15849", "value": "true" }, + { "fid": "15871", "value": "true" }, + { "fid": "16611", "value": "true" }, + { "fid": "16998", "value": "true" }, + { "fid": "18130", "value": "true" }, + { "fid": "18134", "value": "true" }, + { "fid": "18138", "value": "true" }, + { "fid": "18143", "value": "true" }, + { "fid": "19855", "value": "true" }, + { "fid": "20594", "value": "true" }, + { "fid": "23129", "value": "true" }, + { "fid": "23213", "value": "true" }, + { "fid": "23553", "value": "true" }, + { "fid": "23833", "value": "true" }, + { "fid": "23979", "value": "true" }, + { "fid": "23995", "value": "true" }, + { "fid": "24286", "value": "true" }, + { "fid": "25295", "value": "true" }, + { "fid": "25727", "value": "true" }, + { "fid": "26497", "value": "true" }, + { "fid": "27415", "value": "true" }, + { "fid": "27420", "value": "true" }, + { "fid": "27468", "value": "true" }, + { "fid": "27706", "value": "true" }, + { "fid": "27730", "value": "true" }, + { "fid": "27949", "value": "true" }, + { "fid": "29078", "value": "true" }, + { "fid": "29392", "value": "true" }, + { "fid": "29553", "value": "true" }, + { "fid": "29656", "value": "true" }, + { "fid": "29667", "value": "true" }, + { "fid": "29811", "value": "true" }, + { "fid": "30225", "value": "true" }, + { "fid": "30524", "value": "true" }, + { "fid": "30720", "value": "true" }, + { "fid": "31152", "value": "true" }, + { "fid": "31365", "value": "true" }, + { "fid": "31442", "value": "true" }, + { "fid": "31458", "value": "true" }, + { "fid": "31460", "value": "true" }, + { "fid": "31607", "value": "true" }, + { "fid": "33759", "value": "true" }, + { "fid": "34206", "value": "true" }, + { "fid": "35588", "value": "true" } ], "6-hour": [ - "48", - "79", - "156", - "620", - "910", - "1070", - "1229", - "1646", - "1751", - "1810", - "2244", - "2587", - "2669", - "3190", - "3257", - "3407", - "3608", - "3684", - "3751", - "3853", - "3920", - "3992", - "4375", - "4447", - "4987", - "5591", - "5704", - "5828", - "5923", - "5923", - "5923", - "5923", - "6098", - "6128", - "6246", - "6253", - "6377", - "6445", - "6872", - "7346", - "7700", - "7873", - "7929", - "8088", - "11181", - "11498", - "12066", - "12191", - "12876", - "13446", - "13592", - "14413", - "14433", - "14528", - "14571", - "14635", - "15071", - "15080", - "15086", - "15174", - "15240", - "15246", - "15267", - "15286", - "15415", - "15701", - "15702", - "16576", - "16991", - "17311", - "17864", - "18066", - "18184", - "18187", - "18190", - "18199", - "18203", - "18205", - "18275", - "18415", - "18483", - "19029", - "19901", - "19977", - "20154", - "20345", - "20406", - "20456", - "20466", - "20531", - "20532", - "20725", - "20881", - "21180", - "21180", - "21180", - "21180", - "21623", - "21624", - "21627", - "21633", - "21634", - "21635", - "21640", - "21642", - "21772", - "22706", - "22720", - "23387", - "23884", - "24333", - "24903", - "25527", - "27789", - "28063", - "28107", - "28130", - "28760", - "28777", - "28915", - "28990", - "29059", - "29062", - "29312", - "29500", - "29931", - "30031", - "30100", - "30395", - "30583", - "31151", - "31817", - "31958", - "31987", - "32158", - "32164", - "32285", - "32309", - "32315", - "32348", - "32354", - "32369", - "32513", - "32547", - "32568", - "32657", - "32735", - "32764", - "32797", - "32801", - "32819", - "32832", - "32936", - "32964", - "33064", - "33163", - "33238", - "33257", - "33688", - "33731", - "33731", - "33731", - "33731", - "33965", - "34116", - "34121", - "34141", - "35825", - "35825", - "35825", - "35825" + { "fid": "48", "value": "true" }, + { "fid": "79", "value": "true" }, + { "fid": "156", "value": "true" }, + { "fid": "620", "value": "true" }, + { "fid": "910", "value": "true" }, + { "fid": "1070", "value": "true" }, + { "fid": "1229", "value": "true" }, + { "fid": "1646", "value": "true" }, + { "fid": "1751", "value": "true" }, + { "fid": "1810", "value": "true" }, + { "fid": "2244", "value": "true" }, + { "fid": "2587", "value": "true" }, + { "fid": "2669", "value": "true" }, + { "fid": "3190", "value": "true" }, + { "fid": "3257", "value": "true" }, + { "fid": "3407", "value": "true" }, + { "fid": "3608", "value": "true" }, + { "fid": "3684", "value": "true" }, + { "fid": "3751", "value": "true" }, + { "fid": "3853", "value": "true" }, + { "fid": "3920", "value": "true" }, + { "fid": "3992", "value": "true" }, + { "fid": "4375", "value": "true" }, + { "fid": "4447", "value": "true" }, + { "fid": "4987", "value": "true" }, + { "fid": "5591", "value": "true" }, + { "fid": "5704", "value": "true" }, + { "fid": "5828", "value": "true" }, + { "fid": "5923", "value": "true" }, + { "fid": "5923", "value": "true" }, + { "fid": "5923", "value": "true" }, + { "fid": "5923", "value": "true" }, + { "fid": "6098", "value": "true" }, + { "fid": "6128", "value": "true" }, + { "fid": "6246", "value": "true" }, + { "fid": "6253", "value": "true" }, + { "fid": "6377", "value": "true" }, + { "fid": "6445", "value": "true" }, + { "fid": "6872", "value": "true" }, + { "fid": "7346", "value": "true" }, + { "fid": "7700", "value": "true" }, + { "fid": "7873", "value": "true" }, + { "fid": "7929", "value": "true" }, + { "fid": "8088", "value": "true" }, + { "fid": "11181", "value": "true" }, + { "fid": "11498", "value": "true" }, + { "fid": "12066", "value": "true" }, + { "fid": "12191", "value": "true" }, + { "fid": "12876", "value": "true" }, + { "fid": "13446", "value": "true" }, + { "fid": "13592", "value": "true" }, + { "fid": "14413", "value": "true" }, + { "fid": "14433", "value": "true" }, + { "fid": "14528", "value": "true" }, + { "fid": "14571", "value": "true" }, + { "fid": "14635", "value": "true" }, + { "fid": "15071", "value": "true" }, + { "fid": "15080", "value": "true" }, + { "fid": "15086", "value": "true" }, + { "fid": "15174", "value": "true" }, + { "fid": "15240", "value": "true" }, + { "fid": "15246", "value": "true" }, + { "fid": "15267", "value": "true" }, + { "fid": "15286", "value": "true" }, + { "fid": "15415", "value": "true" }, + { "fid": "15701", "value": "true" }, + { "fid": "15702", "value": "true" }, + { "fid": "16576", "value": "true" }, + { "fid": "16991", "value": "true" }, + { "fid": "17311", "value": "true" }, + { "fid": "17864", "value": "true" }, + { "fid": "18066", "value": "true" }, + { "fid": "18184", "value": "true" }, + { "fid": "18187", "value": "true" }, + { "fid": "18190", "value": "true" }, + { "fid": "18199", "value": "true" }, + { "fid": "18203", "value": "true" }, + { "fid": "18205", "value": "true" }, + { "fid": "18275", "value": "true" }, + { "fid": "18415", "value": "true" }, + { "fid": "18483", "value": "true" }, + { "fid": "19029", "value": "true" }, + { "fid": "19901", "value": "true" }, + { "fid": "19977", "value": "true" }, + { "fid": "20154", "value": "true" }, + { "fid": "20345", "value": "true" }, + { "fid": "20406", "value": "true" }, + { "fid": "20456", "value": "true" }, + { "fid": "20466", "value": "true" }, + { "fid": "20531", "value": "true" }, + { "fid": "20532", "value": "true" }, + { "fid": "20725", "value": "true" }, + { "fid": "20881", "value": "true" }, + { "fid": "21180", "value": "true" }, + { "fid": "21180", "value": "true" }, + { "fid": "21180", "value": "true" }, + { "fid": "21180", "value": "true" }, + { "fid": "21623", "value": "true" }, + { "fid": "21624", "value": "true" }, + { "fid": "21627", "value": "true" }, + { "fid": "21633", "value": "true" }, + { "fid": "21634", "value": "true" }, + { "fid": "21635", "value": "true" }, + { "fid": "21640", "value": "true" }, + { "fid": "21642", "value": "true" }, + { "fid": "21772", "value": "true" }, + { "fid": "22706", "value": "true" }, + { "fid": "22720", "value": "true" }, + { "fid": "23387", "value": "true" }, + { "fid": "23884", "value": "true" }, + { "fid": "24333", "value": "true" }, + { "fid": "24903", "value": "true" }, + { "fid": "25527", "value": "true" }, + { "fid": "27789", "value": "true" }, + { "fid": "28063", "value": "true" }, + { "fid": "28107", "value": "true" }, + { "fid": "28130", "value": "true" }, + { "fid": "28760", "value": "true" }, + { "fid": "28777", "value": "true" }, + { "fid": "28915", "value": "true" }, + { "fid": "28990", "value": "true" }, + { "fid": "29059", "value": "true" }, + { "fid": "29062", "value": "true" }, + { "fid": "29312", "value": "true" }, + { "fid": "29500", "value": "true" }, + { "fid": "29931", "value": "true" }, + { "fid": "30031", "value": "true" }, + { "fid": "30100", "value": "true" }, + { "fid": "30395", "value": "true" }, + { "fid": "30583", "value": "true" }, + { "fid": "31151", "value": "true" }, + { "fid": "31817", "value": "true" }, + { "fid": "31958", "value": "true" }, + { "fid": "31987", "value": "true" }, + { "fid": "32158", "value": "true" }, + { "fid": "32164", "value": "true" }, + { "fid": "32285", "value": "true" }, + { "fid": "32309", "value": "true" }, + { "fid": "32315", "value": "true" }, + { "fid": "32348", "value": "true" }, + { "fid": "32354", "value": "true" }, + { "fid": "32369", "value": "true" }, + { "fid": "32513", "value": "true" }, + { "fid": "32547", "value": "true" }, + { "fid": "32568", "value": "true" }, + { "fid": "32657", "value": "true" }, + { "fid": "32735", "value": "true" }, + { "fid": "32764", "value": "true" }, + { "fid": "32797", "value": "true" }, + { "fid": "32801", "value": "true" }, + { "fid": "32819", "value": "true" }, + { "fid": "32832", "value": "true" }, + { "fid": "32936", "value": "true" }, + { "fid": "32964", "value": "true" }, + { "fid": "33064", "value": "true" }, + { "fid": "33163", "value": "true" }, + { "fid": "33238", "value": "true" }, + { "fid": "33257", "value": "true" }, + { "fid": "33688", "value": "true" }, + { "fid": "33731", "value": "true" }, + { "fid": "33731", "value": "true" }, + { "fid": "33731", "value": "true" }, + { "fid": "33731", "value": "true" }, + { "fid": "33965", "value": "true" }, + { "fid": "34116", "value": "true" }, + { "fid": "34121", "value": "true" }, + { "fid": "34141", "value": "true" }, + { "fid": "35825", "value": "true" }, + { "fid": "35825", "value": "true" }, + { "fid": "35825", "value": "true" }, + { "fid": "35825", "value": "true" } ] } diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-ETH-triggered.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-ETH-triggered.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-ETH-triggered.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-ETH-triggered.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-ETH.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-ETH.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-ETH.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-ETH.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-KEN-triggered.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-KEN-triggered.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-KEN-triggered.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-KEN-triggered.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-KEN.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-KEN.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-KEN.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-KEN.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-MWI-triggered.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-MWI-triggered.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-MWI-triggered.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-MWI-triggered.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-MWI.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-MWI.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-MWI.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-MWI.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-PHL-triggered.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-PHL-triggered.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-PHL-triggered.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-PHL-triggered.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-PHL.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-PHL.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-PHL.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-PHL.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-SSD-triggered.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-SSD-triggered.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-SSD-triggered.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-SSD-triggered.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-SSD.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-SSD.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-SSD.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-SSD.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-UGA-triggered.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-UGA-triggered.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-UGA-triggered.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-UGA-triggered.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-UGA.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-UGA.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-UGA.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-UGA.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-ZMB-triggered.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-ZMB-triggered.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-ZMB-triggered.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-ZMB-triggered.json diff --git a/services/API-service/src/api/glofas-station/dto/example/glofas-stations-ZMB.json b/services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-ZMB.json similarity index 100% rename from services/API-service/src/api/glofas-station/dto/example/glofas-stations-ZMB.json rename to services/API-service/src/api/point-data/dto/example/glofas-stations/glofas-stations-ZMB.json diff --git a/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts b/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts index 550fe0b3a..031f95b1b 100644 --- a/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-asset-exposure-status.dto.ts @@ -4,11 +4,13 @@ import { IsNotEmpty, IsOptional, IsString, + ValidateNested, } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; import { LeadTime } from '../../admin-area-dynamic-data/enum/lead-time.enum'; import { DisasterType } from '../../disaster/disaster-type.enum'; import { PointDataEnum } from '../point-data.entity'; +import { Type } from 'class-transformer'; export class UploadAssetExposureStatusDto { @ApiProperty({ example: ['123', '234'] }) @@ -39,3 +41,40 @@ export class UploadAssetExposureStatusDto { @IsEnum(PointDataEnum) public pointDataCategory: PointDataEnum; } + +export class UploadDynamicPointDataDto { + @ApiProperty({ example: LeadTime.hour1 }) + @IsOptional() + @IsString() + public leadTime: LeadTime; + + @ApiProperty({ example: new Date() }) + @IsOptional() + public date: Date; + + @ApiProperty({ example: DisasterType.FlashFloods }) + @IsNotEmpty() + @IsEnum(DisasterType) + public disasterType: DisasterType; + + @ApiProperty({ example: PointDataEnum.gauges }) + public pointDataCategory: PointDataEnum; + + @ApiProperty({ example: 'waterLevel' }) + public key: string; + + @ApiProperty({ example: [{ fid: 1, value: 100 }] }) + @IsArray() + @ValidateNested() + @Type(() => DynamicPointData) + public dynamicPointData: DynamicPointData[]; +} + +export class DynamicPointData { + @ApiProperty() + @IsNotEmpty() + public fid: string; + + @ApiProperty() + public value: string; +} diff --git a/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts b/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts new file mode 100644 index 000000000..b64eb1c74 --- /dev/null +++ b/services/API-service/src/api/point-data/dto/upload-gauge.dto.ts @@ -0,0 +1,20 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class GaugeDto { + @ApiProperty({ example: 'name' }) + @IsString() + public name: string = undefined; + + @ApiProperty({ example: '1234' }) + @IsOptional() + public fid: string = undefined; + + @ApiProperty({ example: 0 }) + @IsNotEmpty() + public lat: number; + + @ApiProperty({ example: 0 }) + @IsNotEmpty() + public lon: number; +} diff --git a/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts b/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts new file mode 100644 index 000000000..5df1ce898 --- /dev/null +++ b/services/API-service/src/api/point-data/dto/upload-glofas-station.dto.ts @@ -0,0 +1,24 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class GlofasStationDto { + @ApiProperty({ example: 'G5100' }) + @IsString() + public stationCode: string = undefined; + + @ApiProperty({ example: 'Station name' }) + @IsOptional() + public stationName: string = undefined; + + @ApiProperty({ example: 'G5100' }) + @IsOptional() + public fid: string = undefined; + + @ApiProperty({ example: 0 }) + @IsNotEmpty() + public lat: number; + + @ApiProperty({ example: 0 }) + @IsNotEmpty() + public lon: number; +} diff --git a/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts b/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts index 57729df77..0d109f291 100644 --- a/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-health-sites.dto.ts @@ -12,9 +12,9 @@ export class HealthSiteDto { @IsString() public type: string = undefined; - @ApiProperty({ example: 1234 }) + @ApiProperty({ example: '1234' }) @IsOptional() - public fid: number = undefined; + public fid: string = undefined; @ApiProperty({ example: 0 }) @IsNotEmpty() diff --git a/services/API-service/src/api/point-data/dto/upload-schools.dto.ts b/services/API-service/src/api/point-data/dto/upload-schools.dto.ts index c9c18eebb..e2b1419ce 100644 --- a/services/API-service/src/api/point-data/dto/upload-schools.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-schools.dto.ts @@ -10,8 +10,8 @@ export class SchoolDto { @IsString() public amenity: string = undefined; - @ApiProperty({ example: 1234 }) - public fid: number = undefined; + @ApiProperty({ example: '1234' }) + public fid: string = undefined; @ApiProperty({ example: 0 }) @IsNotEmpty() diff --git a/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts b/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts index 17fae5bbc..a98238e5a 100644 --- a/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts +++ b/services/API-service/src/api/point-data/dto/upload-waterpoint.dto.ts @@ -14,8 +14,8 @@ export class WaterpointDto { @IsString() public type: string = undefined; - @ApiProperty({ example: 1234 }) - public fid: number = undefined; + @ApiProperty({ example: '1234' }) + public fid: string = undefined; @ApiProperty({ example: 0 }) @IsNotEmpty() diff --git a/services/API-service/src/api/point-data/dynamic-point-data.entity.ts b/services/API-service/src/api/point-data/dynamic-point-data.entity.ts new file mode 100644 index 000000000..b8b228b96 --- /dev/null +++ b/services/API-service/src/api/point-data/dynamic-point-data.entity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + Index, +} from 'typeorm'; +import { PointDataEntity } from './point-data.entity'; +import { LeadTimeEntity } from '../lead-time/lead-time.entity'; + +@Entity('dynamic-point-data') +export class DynamicPointDataEntity { + @PrimaryGeneratedColumn('uuid') + public dynamicPointDataId: string; + + @ManyToOne( + (): typeof PointDataEntity => PointDataEntity, + (point): DynamicPointDataEntity[] => point.dynamicData, + ) + public point: PointDataEntity; + + @ManyToOne((): typeof LeadTimeEntity => LeadTimeEntity) + @JoinColumn({ name: 'leadTime', referencedColumnName: 'leadTimeName' }) + public leadTime: string; + + @Column({ type: 'timestamp' }) + @Index() + public timestamp: Date; + + @Column() + public key: string; + + @Column({ nullable: true }) + public value: string; +} diff --git a/services/API-service/src/api/point-data/point-data-dynamic-status.entity.ts b/services/API-service/src/api/point-data/point-data-dynamic-status.entity.ts deleted file mode 100644 index 997877cc1..000000000 --- a/services/API-service/src/api/point-data/point-data-dynamic-status.entity.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - Entity, - PrimaryGeneratedColumn, - Column, - JoinColumn, - ManyToOne, -} from 'typeorm'; -import { LeadTime } from '../admin-area-dynamic-data/enum/lead-time.enum'; -import { LeadTimeEntity } from '../lead-time/lead-time.entity'; -import { PointDataEntity } from './point-data.entity'; - -@Entity('point-data-dynamic-status') -export class PointDataDynamicStatusEntity { - @PrimaryGeneratedColumn('uuid') - public pointDataDynamicStatusId: string; - - @ApiProperty({ example: 12345 }) - @ManyToOne(() => PointDataEntity) - @JoinColumn({ name: 'referenceId' }) - public pointData: PointDataEntity; - @Column() - public referenceId: number; - - @ApiProperty({ example: new Date() }) - @Column({ type: 'timestamp' }) - public timestamp: Date; - - @ApiProperty({ example: LeadTime.hour1 }) - @ManyToOne((): typeof LeadTimeEntity => LeadTimeEntity) - @JoinColumn({ name: 'leadTime', referencedColumnName: 'leadTimeName' }) - public leadTime: string; - - @ApiProperty({ example: true }) - @Column() - public exposed: boolean; -} 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 cdd706dfe..aa7e0b42b 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 @@ -26,7 +26,10 @@ import { RolesGuard } from '../../roles.guard'; import { GeoJson } from '../../shared/geo.model'; import { UserRole } from '../user/user-role.enum'; import { PointDataService } from './point-data.service'; -import { UploadAssetExposureStatusDto } from './dto/upload-asset-exposure-status.dto'; +import { + UploadAssetExposureStatusDto, + UploadDynamicPointDataDto, +} from './dto/upload-asset-exposure-status.dto'; import { FILE_UPLOAD_API_FORMAT } from '../../shared/file-upload-api-format'; @ApiBearerAuth() @@ -132,4 +135,17 @@ export class PointDataController { ): Promise { return await this.pointDataService.uploadAssetExposureStatus(assetFids); } + + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Upload dynamic point data' }) + @ApiResponse({ + status: 201, + description: 'Uploaded dynamic point data.', + }) + @Post('dynamic') + public async uploadDynamicPointData( + @Body() dynamicPointData: UploadDynamicPointDataDto, + ): Promise { + return await this.pointDataService.uploadDynamicPointData(dynamicPointData); + } } diff --git a/services/API-service/src/api/point-data/point-data.entity.ts b/services/API-service/src/api/point-data/point-data.entity.ts index 498e2a1b1..27c87058c 100644 --- a/services/API-service/src/api/point-data/point-data.entity.ts +++ b/services/API-service/src/api/point-data/point-data.entity.ts @@ -1,4 +1,5 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; +import { DynamicPointDataEntity } from './dynamic-point-data.entity'; export enum PointDataEnum { evacuationCenters = 'evacuation_centers', @@ -8,6 +9,8 @@ export enum PointDataEnum { communityNotifications = 'community_notifications', schools = 'schools', waterpointsInternal = 'waterpoints_internal', + gauges = 'gauges', + glofasStations = 'glofas_stations', } @Entity('point-data') @@ -22,11 +25,17 @@ export class PointDataEntity { public pointDataCategory: PointDataEnum; @Column({ nullable: true }) - public referenceId: number; + public referenceId: string; @Column('json', { default: {} }) public attributes: JSON; @Column('json', { nullable: true }) public geom: JSON; + + @OneToMany( + (): typeof DynamicPointDataEntity => DynamicPointDataEntity, + (dynamicData): PointDataEntity => dynamicData.point, + ) + public dynamicData: DynamicPointDataEntity[]; } diff --git a/services/API-service/src/api/point-data/point-data.module.ts b/services/API-service/src/api/point-data/point-data.module.ts index 8a4b86696..d615a9994 100644 --- a/services/API-service/src/api/point-data/point-data.module.ts +++ b/services/API-service/src/api/point-data/point-data.module.ts @@ -6,14 +6,14 @@ import { UserModule } from '../user/user.module'; import { PointDataController } from './point-data.controller'; import { PointDataEntity } from './point-data.entity'; import { PointDataService } from './point-data.service'; -import { PointDataDynamicStatusEntity } from './point-data-dynamic-status.entity'; import { HttpModule } from '@nestjs/axios'; +import { DynamicPointDataEntity } from './dynamic-point-data.entity'; @Module({ imports: [ HttpModule, UserModule, - TypeOrmModule.forFeature([PointDataEntity, PointDataDynamicStatusEntity]), + TypeOrmModule.forFeature([PointDataEntity, DynamicPointDataEntity]), WhatsappModule, ], providers: [PointDataService, HelperService], 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 9f41695bb..ae14ec8a7 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 @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { validate } from 'class-validator'; import { GeoJson } from '../../shared/geo.model'; import { HelperService } from '../../shared/helper.service'; -import { MoreThanOrEqual, Repository } from 'typeorm'; +import { IsNull, MoreThanOrEqual, Repository } from 'typeorm'; import { EvacuationCenterDto } from './dto/upload-evacuation-centers.dto'; import { PointDataEntity, PointDataEnum } from './point-data.entity'; import { DamSiteDto } from './dto/upload-dam-sites.dto'; @@ -13,16 +13,21 @@ import { CommunityNotificationDto } from './dto/upload-community-notifications.d import { WhatsappService } from '../notification/whatsapp/whatsapp.service'; import { SchoolDto } from './dto/upload-schools.dto'; import { WaterpointDto } from './dto/upload-waterpoint.dto'; -import { UploadAssetExposureStatusDto } from './dto/upload-asset-exposure-status.dto'; -import { PointDataDynamicStatusEntity } from './point-data-dynamic-status.entity'; +import { + UploadAssetExposureStatusDto, + UploadDynamicPointDataDto, +} from './dto/upload-asset-exposure-status.dto'; import { DisasterType } from '../disaster/disaster-type.enum'; +import { GaugeDto } from './dto/upload-gauge.dto'; +import { DynamicPointDataEntity } from './dynamic-point-data.entity'; +import { GlofasStationDto } from './dto/upload-glofas-station.dto'; @Injectable() export class PointDataService { @InjectRepository(PointDataEntity) private readonly pointDataRepository: Repository; - @InjectRepository(PointDataDynamicStatusEntity) - private readonly pointDataDynamicStatusRepo: Repository; + @InjectRepository(DynamicPointDataEntity) + private readonly dynamicPointDataRepository: Repository; public constructor( private readonly helperService: HelperService, @@ -47,34 +52,39 @@ export class PointDataService { selectColumns.push('geom'); selectColumns.push('"pointDataId"'); + const recentDate = await this.helperService.getRecentDate( + countryCodeISO3, + disasterType, + ); + const pointDataQuery = this.pointDataRepository .createQueryBuilder('point') .select(selectColumns) .where({ pointDataCategory: pointDataCategory, countryCodeISO3: countryCodeISO3, - }); + }) + .leftJoin( + (subquery) => { + return subquery + .select([ + 'dynamic."pointPointDataId"', + 'json_object_agg("key",value) as "dynamicData"', + ]) + .from(DynamicPointDataEntity, 'dynamic') + .where('dynamic.timestamp >= :cutoffMoment', { + cutoffMoment: this.helperService.getUploadCutoffMoment( + disasterType, + recentDate.timestamp, + ), + }) + .groupBy('dynamic."pointPointDataId"'); + }, + 'dynamic', + 'dynamic."pointPointDataId" = point."pointDataId"', + ) + .addSelect('dynamic."dynamicData" as "dynamicData"'); - // TO DO: hard-code for now - if (disasterType === DisasterType.FlashFloods) { - const recentDate = await this.helperService.getRecentDate( - countryCodeISO3, - disasterType, - ); - pointDataQuery - .leftJoin( - PointDataDynamicStatusEntity, - 'status', - 'point."pointDataId" = status."referenceId"', - ) - .andWhere( - '(status."timestamp" IS NULL OR status.timestamp = :modelTimestamp)', - { - modelTimestamp: recentDate.timestamp, - }, - ) - .addSelect('COALESCE(status.exposed,FALSE) as "exposed"'); - } const pointData = await pointDataQuery.getRawMany(); return this.helperService.toGeojson(pointData); } @@ -95,6 +105,10 @@ export class PointDataService { return new SchoolDto(); case PointDataEnum.waterpointsInternal: return new WaterpointDto(); + case PointDataEnum.gauges: + return new GaugeDto(); + case PointDataEnum.glofasStations: + return new GlofasStationDto(); default: throw new HttpException( 'Not a known point layer', @@ -222,41 +236,56 @@ export class PointDataService { await this.whatsappService.sendCommunityNotification(countryCodeISO3); } + // The old endpoint is left in for a grace period, and here the input is transformed to the required input for the new endpoint public async uploadAssetExposureStatus( assetFids: UploadAssetExposureStatusDto, ) { - const assetForecasts: PointDataDynamicStatusEntity[] = []; - for (const fid of assetFids.exposedFids) { + const dynamicPointData = new UploadDynamicPointDataDto(); + dynamicPointData.date = assetFids.date; + dynamicPointData.leadTime = assetFids.leadTime; + dynamicPointData.disasterType = assetFids.disasterType; + dynamicPointData.key = 'exposure'; + dynamicPointData.dynamicPointData = assetFids.exposedFids.map((fid) => { + return { fid: fid, value: 'true' }; + }); + await this.uploadDynamicPointData(dynamicPointData); + } + + async uploadDynamicPointData(dynamicPointData: UploadDynamicPointDataDto) { + const dynamicPointDataArray: DynamicPointDataEntity[] = []; + + for (const point of dynamicPointData.dynamicPointData) { const asset = await this.pointDataRepository.findOne({ where: { - referenceId: Number(fid), - pointDataCategory: assetFids.pointDataCategory, - countryCodeISO3: assetFids.countryCodeISO3, + referenceId: point.fid, + pointDataCategory: dynamicPointData.pointDataCategory, }, }); if (!asset) { continue; } - // Delete existing entries with same date, leadTime and countryCodeISO3 and stationCode - await this.pointDataDynamicStatusRepo.delete({ - pointData: { pointDataId: asset.pointDataId }, - leadTime: assetFids.leadTime, + // Delete existing entries + await this.dynamicPointDataRepository.delete({ + point: { pointDataId: asset.pointDataId }, + leadTime: dynamicPointData.leadTime || IsNull(), + key: dynamicPointData.key, timestamp: MoreThanOrEqual( this.helperService.getUploadCutoffMoment( - assetFids.disasterType, - assetFids.date || new Date(), + dynamicPointData.disasterType, + dynamicPointData.date || new Date(), ), ), }); - const assetForecast = new PointDataDynamicStatusEntity(); - assetForecast.pointData = asset; - assetForecast.leadTime = assetFids.leadTime; - assetForecast.timestamp = assetFids.date || new Date(); - assetForecast.exposed = true; - assetForecasts.push(assetForecast); + const dynamicPoint = new DynamicPointDataEntity(); + dynamicPoint.key = dynamicPointData.key; + dynamicPoint.leadTime = dynamicPointData.leadTime; + dynamicPoint.timestamp = dynamicPointData.date || new Date(); + dynamicPoint.value = point.value; + dynamicPoint.point = asset; + dynamicPointDataArray.push(dynamicPoint); } - await this.pointDataDynamicStatusRepo.save(assetForecasts); + await this.dynamicPointDataRepository.save(dynamicPointDataArray); } } diff --git a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_ETH.csv b/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_ETH.csv deleted file mode 100644 index 5aa416945..000000000 --- a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_ETH.csv +++ /dev/null @@ -1,29 +0,0 @@ -station_code,station_name,lat,lon -G1045,Tendaho,11.75,40.95000076 -G1053,Adaitu,11.35000038,40.95000076 -G1067,Melka Sedi,9.149999619,40.15000153 -G1074,Metahara,8.850000381,39.95000076 -G1603,Gambella,8.25,34.95000076 -G1901,Omo River Delta,4.849999905,36.04999924 -G1902,Na,4.75,42.04999924 -G1904,Na,5.050000191,44.84999847 -G1907,Na,6.050000191,43.04999924 -G4951,Na,11.14999962,41.65000153 -G4953,Bahir Dar,11.55000019,37.34999847 -G4954,Na,11.05000019,41.65000153 -G4957,Nr. BAHIR DAR,11.75,37.65000153 -G4965,Nr. ADDIS ZEMEN,12.05000019,37.75 -G4977,Nr. MARAWI,11.35000038,37.04999924 -G4979,Na,10.05000019,35.75 -G5056,Na,8.050000191,33.15000153 -G5068,Na,7.650000095,42.04999924 -G5076,Na,7.550000191,41.95000076 -G5081,Na,7.349999905,42.15000153 -G5115,Na,5.75,40.84999847 -G5141,Na,4.349999905,41.95000076 -G5173,Na,2.849999905,37.45000076 -G6107,Abetlti,8.25,37.54999924 -G6108,Woltkite,8.25,37.75 -G6109,Asetndabo,7.75,37.25 -G6110,Woltkite,8.350000381,37.54999924 -G6111,Shetbe,7.449999809,36.34999847 diff --git a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_KEN.csv b/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_KEN.csv deleted file mode 100644 index 2579921bf..000000000 --- a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_KEN.csv +++ /dev/null @@ -1,4 +0,0 @@ -station_code,station_name,lat,lon -G5142,ATHI MUNYU (3DA02),-1.149999976,37.15000153 -G5195,NZOIA AT RUAMBWA FERRY (1EF01),0.150000006,34.04999924 -G5305,TANA HOLA (4G04),-1.450000048,40.04999924 diff --git a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_MWI.csv b/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_MWI.csv deleted file mode 100644 index a5e65db3c..000000000 --- a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_MWI.csv +++ /dev/null @@ -1,5 +0,0 @@ -lat,lon,station_code,station_name --16.45,35.05,G1724,Na --16.25,34.95,G2001,Na --16.25,35.25,G5670,Sinoya South --16.05,34.85,G5694,Chikwawa \ No newline at end of file diff --git a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_PHL.csv b/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_PHL.csv deleted file mode 100644 index cf8ba293f..000000000 --- a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_PHL.csv +++ /dev/null @@ -1,11 +0,0 @@ -station_code,station_name,lat,lon -G1966,Na,14.89,120.74 -G1967,Na,15.34,120.47 -G1969,Na,7.15,124.34 -G1970,Na,7.15,124.79 -G4608,Pototan Jalaur Bridge,10.91,122.69 -G4611,Nabua,13.40,123.33 -G4630,Tuguegarao Buntun Bridge,17.61,121.69 -G4945,Talacogon Municipal Hall,8.45,125.78 -G5368,Nia Pumping Station,8.89,125.54 -G5369,Dao Bridge,11.39,122.69 diff --git a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_SSD.csv b/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_SSD.csv deleted file mode 100644 index 17d2c3288..000000000 --- a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_SSD.csv +++ /dev/null @@ -1,2 +0,0 @@ -lat,lon,station_code,station_name -6.22,31.59,G5100,Na diff --git a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_UGA.csv b/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_UGA.csv deleted file mode 100644 index f8e988604..000000000 --- a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_UGA.csv +++ /dev/null @@ -1,9 +0,0 @@ -station_code,station_name,lat,lon -G5075,Mpologoma at Budumba (82217),0.75,33.75 -G5160,Mayanja (83218),0.649999976,32.15000153 -G5189,Muzizi (85211),0.850000024,30.75 -G5196,Akokorio at Uganda Gauge,1.75,33.84999847 -G5220,Manafwa at Butaleja (82212),0.949999988,34.04999924 -G5227,Nkusi (85212),1.049999952,30.75 -G5230,SIO RIVER (1AH01),0.349999994,34.04999924 -G5317,Mitano (84267),-0.850000024,29.85000038 diff --git a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_ZMB.csv b/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_ZMB.csv deleted file mode 100644 index 72a5e2c5c..000000000 --- a/services/API-service/src/scripts/git-lfs/glofas-stations/Glofas_station_locations_ZMB.csv +++ /dev/null @@ -1,21 +0,0 @@ -station_code,station_name,lat,lon -G1264,NearChibote,-9.866600037,29.54999924 -G1265,ChungaRanch,-9.930000305,32.13000107 -G1269,MbesumaPontoon60773245,-10,32.16669846 -G1271,PumpHouse,-10.16660023,30.96660042 -G1294,ChembeFerry,-11.96660042,28.75 -G1312,Mpatamato,-13.25,28.13330078 -G1319,KabompoPontoon60380051,-13.60000038,24.21660042 -G1320,Kafironda,-13.63329983,27.59000015 -G1323,KelongwaSchool60334550,-13.69999981,26.33329964 -G1328,Lukulu60370030,-14.38329983,23.23329926 -G1329,Mswebi,-14.41660023,27 -G1338,KafueHookBridge60334669,-14.93330002,25.91659927 -G1340,LuangwaRoadBridge60332040,-14.9666996,30.2166996 -G1344,NgwerereConfluence,-15.2166996,28.5 -G1351,Nyimba,-15.75,27.03000069 -G1352,ItezhiTezhi,-15.76669979,26.01659966 -G1353,Kasaka60334977,-15.81659985,28.21660042 -G1361,Senanga60370001,-16.11669922,23.25 -G1373,KatimaMulilo64370001,-17.48329926,24.29999924 -G5696,Na,-16.35000038,23.45000076 diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/dams_ZWE.csv b/services/API-service/src/scripts/git-lfs/point-layers/dams_ZWE.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/dams_ZWE.csv rename to services/API-service/src/scripts/git-lfs/point-layers/dams_ZWE.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/evacuation_centers_SSD.csv b/services/API-service/src/scripts/git-lfs/point-layers/evacuation_centers_SSD.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/evacuation_centers_SSD.csv rename to services/API-service/src/scripts/git-lfs/point-layers/evacuation_centers_SSD.csv diff --git a/services/API-service/src/scripts/git-lfs/point-layers/gauges_MWI.csv b/services/API-service/src/scripts/git-lfs/point-layers/gauges_MWI.csv new file mode 100644 index 000000000..916d6b187 --- /dev/null +++ b/services/API-service/src/scripts/git-lfs/point-layers/gauges_MWI.csv @@ -0,0 +1,4 @@ +id,lon,lat,fid,name,Country +1,33.65612777,-11.13575247,1,Lake Kazuni,MWI +2,33.88461925,-11.10706701,2,Kasitu River,MWI +3,33.82902969,-9.943700455,3,Karonga,MWI diff --git a/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_ETH.csv b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_ETH.csv new file mode 100644 index 000000000..e59c30a5f --- /dev/null +++ b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_ETH.csv @@ -0,0 +1,29 @@ +stationCode,stationName,lat,lon,fid +G1045,Tendaho,11.75,40.95000076,G1045 +G1053,Adaitu,11.35000038,40.95000076,G1053 +G1067,Melka Sedi,9.149999619,40.15000153,G1067 +G1074,Metahara,8.850000381,39.95000076,G1074 +G1603,Gambella,8.25,34.95000076,G1603 +G1901,Omo River Delta,4.849999905,36.04999924,G1901 +G1902,Na,4.75,42.04999924,G1902 +G1904,Na,5.050000191,44.84999847,G1904 +G1907,Na,6.050000191,43.04999924,G1907 +G4951,Na,11.14999962,41.65000153,G4951 +G4953,Bahir Dar,11.55000019,37.34999847,G4953 +G4954,Na,11.05000019,41.65000153,G4954 +G4957,Nr. BAHIR DAR,11.75,37.65000153,G4957 +G4965,Nr. ADDIS ZEMEN,12.05000019,37.75,G4965 +G4977,Nr. MARAWI,11.35000038,37.04999924,G4977 +G4979,Na,10.05000019,35.75,G4979 +G5056,Na,8.050000191,33.15000153,G5056 +G5068,Na,7.650000095,42.04999924,G5068 +G5076,Na,7.550000191,41.95000076,G5076 +G5081,Na,7.349999905,42.15000153,G5081 +G5115,Na,5.75,40.84999847,G5115 +G5141,Na,4.349999905,41.95000076,G5141 +G5173,Na,2.849999905,37.45000076,G5173 +G6107,Abetlti,8.25,37.54999924,G6107 +G6108,Woltkite,8.25,37.75,G6108 +G6109,Asetndabo,7.75,37.25,G6109 +G6110,Woltkite,8.350000381,37.54999924,G6110 +G6111,Shetbe,7.449999809,36.34999847,G6111 diff --git a/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_KEN.csv b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_KEN.csv new file mode 100644 index 000000000..a82b5a58b --- /dev/null +++ b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_KEN.csv @@ -0,0 +1,4 @@ +stationCode,stationName,lat,lon,fid +G5142,ATHI MUNYU (3DA02),-1.149999976,37.15000153,G5142 +G5195,NZOIA AT RUAMBWA FERRY (1EF01),0.150000006,34.04999924,G5195 +G5305,TANA HOLA (4G04),-1.450000048,40.04999924,G5305 diff --git a/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_MWI.csv b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_MWI.csv new file mode 100644 index 000000000..fa3931b58 --- /dev/null +++ b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_MWI.csv @@ -0,0 +1,5 @@ +lat,lon,stationCode,stationName,fid +-16.45,35.05,G1724,Na,G1724 +-16.25,34.95,G2001,Na,G2001 +-16.25,35.25,G5670,Sinoya South,G5670 +-16.05,34.85,G5694,Chikwawa,G5694 diff --git a/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_PHL.csv b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_PHL.csv new file mode 100644 index 000000000..0d05ac7c0 --- /dev/null +++ b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_PHL.csv @@ -0,0 +1,11 @@ +stationCode,stationName,lat,lon,fid +G1966,Na,14.89,120.74,G1966 +G1967,Na,15.34,120.47,G1967 +G1969,Na,7.15,124.34,G1969 +G1970,Na,7.15,124.79,G1970 +G4608,Pototan Jalaur Bridge,10.91,122.69,G4608 +G4611,Nabua,13.4,123.33,G4611 +G4630,Tuguegarao Buntun Bridge,17.61,121.69,G4630 +G4945,Talacogon Municipal Hall,8.45,125.78,G4945 +G5368,Nia Pumping Station,8.89,125.54,G5368 +G5369,Dao Bridge,11.39,122.69,G5369 diff --git a/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_SSD.csv b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_SSD.csv new file mode 100644 index 000000000..817c5140d --- /dev/null +++ b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_SSD.csv @@ -0,0 +1,2 @@ +lat,lon,stationCode,stationName,fid +6.22,31.59,G5100,Na,G5100 diff --git a/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_UGA.csv b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_UGA.csv new file mode 100644 index 000000000..9994301a5 --- /dev/null +++ b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_UGA.csv @@ -0,0 +1,9 @@ +stationCode,stationName,lat,lon,fid +G5075,Mpologoma at Budumba (82217),0.75,33.75,G5075 +G5160,Mayanja (83218),0.649999976,32.15000153,G5160 +G5189,Muzizi (85211),0.850000024,30.75,G5189 +G5196,Akokorio at Uganda Gauge,1.75,33.84999847,G5196 +G5220,Manafwa at Butaleja (82212),0.949999988,34.04999924,G5220 +G5227,Nkusi (85212),1.049999952,30.75,G5227 +G5230,SIO RIVER (1AH01),0.349999994,34.04999924,G5230 +G5317,Mitano (84267),-0.850000024,29.85000038,G5317 diff --git a/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_ZMB.csv b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_ZMB.csv new file mode 100644 index 000000000..d2f0f834f --- /dev/null +++ b/services/API-service/src/scripts/git-lfs/point-layers/glofas_stations_ZMB.csv @@ -0,0 +1,21 @@ +stationCode,stationName,lat,lon,fid +G1264,NearChibote,-9.866600037,29.54999924,G1264 +G1265,ChungaRanch,-9.930000305,32.13000107,G1265 +G1269,MbesumaPontoon60773245,-10,32.16669846,G1269 +G1271,PumpHouse,-10.16660023,30.96660042,G1271 +G1294,ChembeFerry,-11.96660042,28.75,G1294 +G1312,Mpatamato,-13.25,28.13330078,G1312 +G1319,KabompoPontoon60380051,-13.60000038,24.21660042,G1319 +G1320,Kafironda,-13.63329983,27.59000015,G1320 +G1323,KelongwaSchool60334550,-13.69999981,26.33329964,G1323 +G1328,Lukulu60370030,-14.38329983,23.23329926,G1328 +G1329,Mswebi,-14.41660023,27,G1329 +G1338,KafueHookBridge60334669,-14.93330002,25.91659927,G1338 +G1340,LuangwaRoadBridge60332040,-14.9666996,30.2166996,G1340 +G1344,NgwerereConfluence,-15.2166996,28.5,G1344 +G1351,Nyimba,-15.75,27.03000069,G1351 +G1352,ItezhiTezhi,-15.76669979,26.01659966,G1352 +G1353,Kasaka60334977,-15.81659985,28.21660042,G1353 +G1361,Senanga60370001,-16.11669922,23.25,G1361 +G1373,KatimaMulilo64370001,-17.48329926,24.29999924,G1373 +G5696,Na,-16.35000038,23.45000076,G5696 diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/health_sites_KEN.csv b/services/API-service/src/scripts/git-lfs/point-layers/health_sites_KEN.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/health_sites_KEN.csv rename to services/API-service/src/scripts/git-lfs/point-layers/health_sites_KEN.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/health_sites_MWI.csv b/services/API-service/src/scripts/git-lfs/point-layers/health_sites_MWI.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/health_sites_MWI.csv rename to services/API-service/src/scripts/git-lfs/point-layers/health_sites_MWI.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/health_sites_PHL.csv b/services/API-service/src/scripts/git-lfs/point-layers/health_sites_PHL.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/health_sites_PHL.csv rename to services/API-service/src/scripts/git-lfs/point-layers/health_sites_PHL.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_EGY.csv b/services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_EGY.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_EGY.csv rename to services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_EGY.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_ETH.csv b/services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_ETH.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_ETH.csv rename to services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_ETH.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_KEN.csv b/services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_KEN.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_KEN.csv rename to services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_KEN.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_UGA.csv b/services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_UGA.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_UGA.csv rename to services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_UGA.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_ZMB.csv b/services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_ZMB.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_ZMB.csv rename to services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_ZMB.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_ZWE.csv b/services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_ZWE.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/red_cross_branches_ZWE.csv rename to services/API-service/src/scripts/git-lfs/point-layers/red_cross_branches_ZWE.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/schools_MWI.csv b/services/API-service/src/scripts/git-lfs/point-layers/schools_MWI.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/schools_MWI.csv rename to services/API-service/src/scripts/git-lfs/point-layers/schools_MWI.csv diff --git a/services/API-service/src/scripts/git-lfs/standard-point-layers/waterpoints_internal_MWI.csv b/services/API-service/src/scripts/git-lfs/point-layers/waterpoints_internal_MWI.csv similarity index 100% rename from services/API-service/src/scripts/git-lfs/standard-point-layers/waterpoints_internal_MWI.csv rename to services/API-service/src/scripts/git-lfs/point-layers/waterpoints_internal_MWI.csv diff --git a/services/API-service/src/scripts/json/layer-metadata.json b/services/API-service/src/scripts/json/layer-metadata.json index 8ec4b6783..497753e3c 100644 --- a/services/API-service/src/scripts/json/layer-metadata.json +++ b/services/API-service/src/scripts/json/layer-metadata.json @@ -469,6 +469,18 @@ "active": "no", "description": { "MWI": { "flash-floods": "TBD" } } }, + { + "name": "gauges", + "label": "River gauges", + "type": "point", + "leadTimeDependent": false, + "active": "no", + "description": { + "MWI": { + "flash-floods": "The river gauge layer shows you the location of river gauges on the map, they are visualised as blue drop pins with a river icon. Clicking on a drop pin opens a pop up containing information about the current water level in meters above sea level (mMSL) this is measured daily with the latest model run. The difference compared to 24 hours ago is shown in green for decrease or red for increase. The reference water level is the typical level for this station during this season." + } + } + }, { "name": "roads", "label": "Roads", diff --git a/services/API-service/src/scripts/json/layer-popup-info.xlsx b/services/API-service/src/scripts/json/layer-popup-info.xlsx index 9d0fdc958..d02c4eecb 100644 Binary files a/services/API-service/src/scripts/json/layer-popup-info.xlsx and b/services/API-service/src/scripts/json/layer-popup-info.xlsx differ diff --git a/services/API-service/src/scripts/scripts.module.ts b/services/API-service/src/scripts/scripts.module.ts index 308c02f76..940d56203 100644 --- a/services/API-service/src/scripts/scripts.module.ts +++ b/services/API-service/src/scripts/scripts.module.ts @@ -21,7 +21,6 @@ import { AdminAreaModule } from '../api/admin-area/admin-area.module'; import { CountryModule } from '../api/country/country.module'; import SeedAdminAreaData from './seed-admin-area-data'; import SeedPointData from './seed-point-data'; -import SeedGlofasStation from './seed-glofas-station'; import SeedRainfallData from './seed-rainfall-data'; import { PointDataModule } from '../api/point-data/point-data.module'; import { AdminAreaDataModule } from '../api/admin-area-data/admin-area-data.module'; @@ -63,7 +62,6 @@ import { ORMConfig } from '../../ormconfig'; SeedAdminAreaData, SeedPointData, SeedLineData, - SeedGlofasStation, SeedRainfallData, ], controllers: [ScriptsController], diff --git a/services/API-service/src/scripts/scripts.service.ts b/services/API-service/src/scripts/scripts.service.ts index 8c0fc56f9..42d350f89 100644 --- a/services/API-service/src/scripts/scripts.service.ts +++ b/services/API-service/src/scripts/scripts.service.ts @@ -28,7 +28,7 @@ import { LinesDataService } from '../api/lines-data/lines-data.service'; import { UploadLinesExposureStatusDto } from '../api/lines-data/dto/upload-asset-exposure-status.dto'; import { LinesDataEnum } from '../api/lines-data/lines-data.entity'; import { PointDataEnum } from '../api/point-data/point-data.entity'; -import { UploadAssetExposureStatusDto } from '../api/point-data/dto/upload-asset-exposure-status.dto'; +import { UploadDynamicPointDataDto } from '../api/point-data/dto/upload-asset-exposure-status.dto'; import { PointDataService } from '../api/point-data/point-data.service'; @Injectable() @@ -187,11 +187,16 @@ export class ScriptsService { } if (mockInput.disasterType === DisasterType.FlashFloods) { - await this.mockEsposedAssets( + await this.mockExposedAssets( selectedCountry.countryCodeISO3, mockInput.triggered, date, ); + await this.mockDynamicPointData( + selectedCountry.countryCodeISO3, + mockInput.disasterType, + date, + ); } if (mockInput.disasterType === DisasterType.Typhoon) { @@ -901,7 +906,8 @@ export class ScriptsService { triggered: boolean, date: Date, ) { - const stationsFileName = `./src/api/glofas-station/dto/example/glofas-stations-${ + // TODO: this is still the old glofas input-format, needs to be updated + const stationsFileName = `./src/api/point-data/dto/example/glofas-stations/glofas-stations-${ selectedCountry.countryCodeISO3 }${triggered ? '-triggered' : ''}.json`; const stationsRaw = fs.readFileSync(stationsFileName, 'utf-8'); @@ -944,7 +950,7 @@ export class ScriptsService { }); } - private async mockEsposedAssets( + private async mockExposedAssets( countryCodeISO3: string, triggered: boolean, date: Date, @@ -986,38 +992,65 @@ export class ScriptsService { } for (const pointAssetType of pointDataCategories) { - const payload = new UploadAssetExposureStatusDto(); - payload.countryCodeISO3 = countryCodeISO3; + const payload = new UploadDynamicPointDataDto(); payload.disasterType = DisasterType.FlashFloods; - payload.pointDataCategory = pointAssetType; + payload.key = 'exposure'; payload.leadTime = leadTime; payload.date = date || new Date(); if (pointAssetType === PointDataEnum.healthSites) { leadTime === LeadTime.hour24 - ? (payload.exposedFids = []) + ? (payload.dynamicPointData = []) : leadTime === LeadTime.hour6 - ? (payload.exposedFids = ['124']) + ? (payload.dynamicPointData = [{ fid: '124', value: 'true' }]) : []; } else if (pointAssetType === PointDataEnum.schools) { leadTime === LeadTime.hour24 - ? (payload.exposedFids = ['167']) + ? (payload.dynamicPointData = [{ fid: '167', value: 'true' }]) : leadTime === LeadTime.hour6 - ? (payload.exposedFids = []) + ? (payload.dynamicPointData = []) : []; } else if (pointAssetType === PointDataEnum.waterpointsInternal) { const filename = `./src/api/point-data/dto/example/${countryCodeISO3}/${DisasterType.FlashFloods}/${pointAssetType}.json`; const assets = JSON.parse(fs.readFileSync(filename, 'utf-8')); leadTime === LeadTime.hour24 - ? (payload.exposedFids = assets[LeadTime.hour24]) + ? (payload.dynamicPointData = assets[LeadTime.hour24]) : leadTime === LeadTime.hour6 - ? (payload.exposedFids = assets[LeadTime.hour6]) + ? (payload.dynamicPointData = assets[LeadTime.hour6]) : []; } - await this.pointDataService.uploadAssetExposureStatus(payload); + await this.pointDataService.uploadDynamicPointData(payload); } } } + private async mockDynamicPointData( + countryCodeISO3: string, + disasterType: DisasterType, + date: Date, + ) { + if (countryCodeISO3 !== 'MWI') { + return; + } + + const keys = [ + 'water-level', + 'water-level-reference', + 'water-level-previous', + ]; + for (const key of keys) { + const payload = new UploadDynamicPointDataDto(); + payload.key = key; + payload.leadTime = null; + payload.date = date || new Date(); + payload.disasterType = disasterType; + const filename = `./src/api/point-data/dto/example/${countryCodeISO3}/${DisasterType.FlashFloods}/dynamic-point-data_${key}.json`; + const dynamicPointData = JSON.parse(fs.readFileSync(filename, 'utf-8')); + payload.dynamicPointData = dynamicPointData; + + await this.pointDataService.uploadDynamicPointData(payload); + } + } + private async mockRasterFile( selectedCountry, disasterType: DisasterType, diff --git a/services/API-service/src/scripts/seed-glofas-station.ts b/services/API-service/src/scripts/seed-glofas-station.ts deleted file mode 100644 index 2ca8b5292..000000000 --- a/services/API-service/src/scripts/seed-glofas-station.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { DisasterType } from './../api/disaster/disaster-type.enum'; -import { Injectable } from '@nestjs/common'; -import { InterfaceScript } from './scripts.module'; -import { DataSource } from 'typeorm'; -import { SeedHelper } from './seed-helper'; -import countries from './json/countries.json'; -import { GlofasStationService } from '../api/glofas-station/glofas-station.service'; - -@Injectable() -export class SeedGlofasStation implements InterfaceScript { - private readonly seedHelper: SeedHelper; - - public constructor( - private glofasStationService: GlofasStationService, - dataSource: DataSource, - ) { - this.seedHelper = new SeedHelper(dataSource); - } - - public async run(): Promise { - const envCountries = process.env.COUNTRIES.split(','); - - await Promise.all( - countries.map((country): Promise => { - if ( - envCountries.includes(country.countryCodeISO3) && - country.disasterTypes.includes(DisasterType.Floods) - ) { - return this.seedCountryGlofasStations(country); - } else { - return Promise.resolve(); - } - }), - ); - } - - private async seedCountryGlofasStations(country): Promise { - const glofasStationDataFileName = `./src/scripts/git-lfs/glofas-stations/Glofas_station_locations_${country.countryCodeISO3}.csv`; - const glofasStationData = await this.seedHelper.getCsvData( - glofasStationDataFileName, - ); - - const validatedData = await this.glofasStationService.validateArray( - glofasStationData, - ); - - await this.glofasStationService.uploadJson( - country.countryCodeISO3, - validatedData, - ); - } -} - -export default SeedGlofasStation; diff --git a/services/API-service/src/scripts/seed-init.ts b/services/API-service/src/scripts/seed-init.ts index c0e18a23e..35ce840b6 100644 --- a/services/API-service/src/scripts/seed-init.ts +++ b/services/API-service/src/scripts/seed-init.ts @@ -25,7 +25,6 @@ import layerMetadata from './json/layer-metadata.json'; import disasters from './json/disasters.json'; import SeedAdminArea from './seed-admin-area'; -import SeedGlofasStation from './seed-glofas-station'; import { SeedHelper } from './seed-helper'; import SeedAdminAreaData from './seed-admin-area-data'; import SeedRainfallData from './seed-rainfall-data'; @@ -46,7 +45,6 @@ export class SeedInit implements InterfaceScript { private dataSource: DataSource, private seedAdminArea: SeedAdminArea, private seedAdminAreaData: SeedAdminAreaData, - private seedGlofasStation: SeedGlofasStation, private seedPointData: SeedPointData, private seedLineData: SeedLineData, private seedRainfallData: SeedRainfallData, @@ -242,10 +240,6 @@ export class SeedInit implements InterfaceScript { // ***** SEED RAINFALL DATA ***** console.log('Seed rainfall data...'); await this.seedRainfallData.run(); - - // ***** SEED GLOFAS-STATION DATA ***** - console.log('Seed Glofas Stations...'); - await this.seedGlofasStation.run(); } } diff --git a/services/API-service/src/scripts/seed-point-data.ts b/services/API-service/src/scripts/seed-point-data.ts index 236efe678..c5e462c41 100644 --- a/services/API-service/src/scripts/seed-point-data.ts +++ b/services/API-service/src/scripts/seed-point-data.ts @@ -29,6 +29,8 @@ export class SeedPointData implements InterfaceScript { this.seedPointData(PointDataEnum.dams, country); this.seedPointData(PointDataEnum.schools, country); this.seedPointData(PointDataEnum.waterpointsInternal, country); + this.seedPointData(PointDataEnum.gauges, country); + this.seedPointData(PointDataEnum.glofasStations, country); return; } else { return Promise.resolve(); @@ -41,7 +43,7 @@ export class SeedPointData implements InterfaceScript { pointDataCategory: PointDataEnum, country, ): Promise { - const filename = `./src/scripts/git-lfs/standard-point-layers/${pointDataCategory}_${country.countryCodeISO3}.csv`; + const filename = `./src/scripts/git-lfs/point-layers/${pointDataCategory}_${country.countryCodeISO3}.csv`; try { const data = await this.seedHelper.getCsvData(filename);