Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(dashboard)!: id handling and mansa data #323

Merged
merged 6 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { SupabaseService } from '@picsa/shared/services/core/supabase';
import { SupabaseStorageService } from '@picsa/shared/services/core/supabase/services/supabase-storage.service';

import { IDeploymentRow } from '../deployment/types';
import { ClimateService } from './climate.service';
import type { ClimateApiService } from './climate-api.service';
import {
IClimateProductInsert,
Expand All @@ -28,20 +29,23 @@ export type IAPICountryCode = ApiComponents['schemas']['StationAndDefintionRespo
*/
export const ApiMapping = (
api: ClimateApiService,
service: ClimateService,
db: SupabaseService['db'],
storage: SupabaseStorageService,
deployment: IDeploymentRow
) => {
return {
rainfallSummaries: async (station: IStationRow) => {
const { country_code, station_id, id } = station;
const { country_code, station_id, station_name, id } = station;
// TODO - add model type definitions for server rainfall summary response body
const { data, error } = await api
.getObservableClient(`rainfallSummary_${id}`)
.POST('/v1/annual_rainfall_summaries/', {
body: {
country: `${country_code}` as any,
station_id: `${station_id}`,
// HACK - API uses the value stored as station_name (instead of sanitized id)
// TODO - Push for api to use safer ID values
station_id: `${station_name}`,
summaries: ['annual_rain', 'start_rains', 'end_rains', 'end_season', 'seasonal_rain', 'seasonal_length'],
},
});
Expand All @@ -67,15 +71,22 @@ export const ApiMapping = (
.GET(`/v1/station/{country}`, { params: { path: { country: country_code as any } } });
if (error) throw error;
console.log('station data', data);
const dbData = data.data.map(
const update = data.data.map(
(d): IStationInsert => ({
...d,
station_id: `${d.station_id}`,
// HACK - clean IDs as currently just free text input
// TODO - Push for api to use safer ID values
station_id: `${d.station_id.toLowerCase().replace(/[^a-z]/gi, '_')}`,
})
);
const { error: dbError } = await db.table('climate_stations').upsert<IStationInsert>(dbData);
const { error: dbError, data: dbData } = await db
.table('climate_stations')
.upsert<IStationInsert>(update)
.select();
if (dbError) throw dbError;
return dbData;
if (dbData?.length > 0) {
service.stations.set(dbData);
}
},
//
forecasts: async (country_code: IAPICountryCode) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { effect, Injectable, signal } from '@angular/core';
import { computed, effect, Injectable, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router';
import { PicsaAsyncService } from '@picsa/shared/services/asyncService.service';
Expand All @@ -17,36 +17,53 @@ import { IStationRow } from './types';
export class ClimateService extends PicsaAsyncService {
public apiStatus: number;
public stations = signal<IStationRow[]>([]);
public activeStation: IStationRow;

/** Lookup active station from stationId param and db stations list*/
public activeStation = computed<IStationRow>(() => {
const stations = this.stations();
const activeStationId = this.activeStationId();
if (stations.length > 0) {
const station = stations.find((station) => station.station_id === activeStationId);
if (station) return station;
else {
this.router.navigate(['climate', 'station']);
this.notificationService.showErrorNotification(`[Climate Station] data not found: ${activeStationId}`);
}
}
// UI components aren't rendered unless station defined so can safely ignore this return type
return null as any;
});
/** Country code used for api requests */
public apiCountryCode: IAPICountryCode;

/** Trigger API request that includes mapping response to local database */
public loadFromAPI = ApiMapping(
this.api,
this,
this.supabaseService.db,
this.supabaseService.storage,
this.deploymentSevice.activeDeployment() as IDeploymentRow
);

// Create a signal to represent current stationId as defined by route params
private activeStationId = toSignal(ngRouterMergedSnapshot$(this.router).pipe(map(({ params }) => params.stationId)));
private activeStationId = toSignal(
ngRouterMergedSnapshot$(this.router).pipe(map(({ params }) => decodeURIComponent(params.stationId)))
);

constructor(
private supabaseService: SupabaseService,
private api: ClimateApiService,
private notificationService: PicsaNotificationService,
private router: Router,
private deploymentSevice: DeploymentDashboardService
private deploymentSevice: DeploymentDashboardService,
private notificationService: PicsaNotificationService
) {
super();
this.ready();
// Update list of available stations for deployment country code and station id on change
effect(async () => {
const deployment = this.deploymentSevice.activeDeployment();
const activeStationId = this.activeStationId();
if (deployment) {
await this.loadData(deployment, activeStationId);
await this.loadData(deployment);
}
});
}
Expand All @@ -55,41 +72,19 @@ export class ClimateService extends PicsaAsyncService {
await this.supabaseService.ready();
}

public async refreshData() {
const deployment = this.deploymentSevice.activeDeployment();
if (deployment) {
const { country_code } = deployment;
await this.listStations(country_code);
}
}

private async loadData(deployment: IDeploymentRow, stationId?: string) {
private async loadData(deployment: IDeploymentRow) {
// allow configuration override for country_code
const configuration = deployment.configuration as any;
const targetCode = configuration.climate_country_code || deployment.country_code;
// refresh list of stations on deployment country code change
// refresh list of stations on deployment country code change (or if forced update)
if (targetCode !== this.apiCountryCode) {
this.apiCountryCode = targetCode;
await this.ready();
await this.listStations(targetCode);
}
// set active station if deployment and active station id selected
if (stationId) {
this.setActiveStation(stationId);
}
}

private setActiveStation(id: string) {
const station = this.stations().find((station) => station.station_id === id);
if (station) {
this.activeStation = station;
} else {
this.activeStation = undefined as any;
throw new Error(`[Climate Station] data not found: ${id}`);
}
}

private async listStations(country_code: string, allowRefresh = true) {
private async listStations(country_code: string) {
// TODO - when running should refresh from server as cron task
const { data, error } = await this.supabaseService.db
.table('climate_stations')
Expand All @@ -98,10 +93,11 @@ export class ClimateService extends PicsaAsyncService {
if (error) {
throw error;
}
if (data.length === 0 && allowRefresh) {
if (data?.length > 0) {
this.stations.set(data);
} else {
// NOTE - api will also update station signal
await this.loadFromAPI.station(country_code);
return this.listStations(country_code, false);
}
this.stations.set(data || []);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class RainfallSummaryComponent implements AfterViewInit {
// TODO - nicer if could include db lookups as part of mapping doc
const { data, error } = await this.db
.select<'*', IClimateProductRow>('*')
.eq('station_id', activeStation.id)
.eq('station_id', activeStation().id)
.eq('type', 'rainfallSummary')
.single();
if (data) {
Expand All @@ -80,8 +80,8 @@ export class RainfallSummaryComponent implements AfterViewInit {

public async refreshData() {
if (this.service.activeStation) {
this.apiClientId = `rainfallSummary_${this.service.activeStation.id}`;
const data = await this.service.loadFromAPI.rainfallSummaries(this.service.activeStation);
this.apiClientId = `rainfallSummary_${this.service.activeStation().id}`;
const data = await this.service.loadFromAPI.rainfallSummaries(this.service.activeStation());
const summary = data?.[0];
if (summary) {
this.loadData(summary.data as any);
Expand All @@ -98,11 +98,11 @@ export class RainfallSummaryComponent implements AfterViewInit {

private loadData(summary: IRainfallSummary) {
console.log('load data', summary);
this.tableOptions.exportFilename = `${this.service.activeStation.station_name}_rainfallSummary.csv`;
this.tableOptions.exportFilename = `${this.service.activeStation().station_name}_rainfallSummary.csv`;
const { data, metadata } = summary;
this.summaryData = this.convertAPIDataToLegacyFormat(data);
this.summaryMetadata = metadata;
const { country_code } = this.service.activeStation;
const { country_code } = this.service.activeStation();
const definitions = CLIMATE_CHART_DEFINTIONS[country_code] || CLIMATE_CHART_DEFINTIONS.default;
this.chartDefintions = Object.values(definitions);
}
Expand All @@ -111,10 +111,13 @@ export class RainfallSummaryComponent implements AfterViewInit {
private convertAPIDataToLegacyFormat(apiData: AnnualRainfallSummariesdata[] = []) {
const data: Partial<IStationData>[] = apiData.map((el) => ({
Year: el.year,
End: el.end_rains_doy,
// HACK - use either end_rains or end_season depending on which has data populated
// TODO - push for single value to be populated at api level
End: el.end_rains_doy || el.end_season_doy,
Extreme_events: 0,
Length: el.season_length,
Rainfall: el.annual_rain,
// HACK - replace 0mm with null value
Rainfall: el.annual_rain || undefined,
Start: el.start_rains_doy,
}));
return data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { RainfallSummaryComponent } from './components/rainfall-summary/rainfall
})
export class StationDetailsPageComponent implements OnInit {
public get station() {
return this.service.activeStation;
return this.service.activeStation();
}

public get stationSummary() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class ClimateStationPageComponent implements OnInit {
});

public apiStatusOptions: IApiStatusOptions = {
events: { refresh: () => this.service.loadFromAPI.station(this.service.apiCountryCode) },
events: { refresh: async () => this.service.loadFromAPI.station(this.service.apiCountryCode) },
showStatusCode: false,
};

Expand Down
7 changes: 2 additions & 5 deletions apps/picsa-server/supabase/data/climate_products_rows.csv

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions apps/picsa-server/supabase/data/climate_stations_rows.csv
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
station_id,country_code,station_name,latitude,longitude,elevation,district
Chipata Met,zm_workshops,Chipata Met,-13.6,32.6,1025,Chipata
Lundazi Met,zm_workshops,Lundazi Met,-12.28,33.2,1143,Lundazi
Mfuwe Met,zm_workshops,Mfuwe Met,-13.26,31.93,570,Mambwe
Msekera Agromet,zm_workshops,Msekera Agromet,-13.65,32.56,1025,Chipata
Petauke Met,zm_workshops,Petauke Met,-14.25,31.28,1036,Petauke
chipata_met,zm_workshops,Chipata Met,-13.6,32.6,1025,Chipata
lundazi_met,zm_workshops,Lundazi Met,-12.28,33.2,1143,Lundazi
mfuwe_met,zm_workshops,Mfuwe Met,-13.26,31.93,570,Mambwe
msekera_agromet,zm_workshops,Msekera Agromet,-13.65,32.56,1025,Chipata
petauke_met,zm_workshops,Petauke Met,-14.25,31.28,1036,Petauke
mansa_met,zm_workshops,MANSA MET,-11.1,28.85,1259,Mansa
nkhotakota,mw_workshops,Nkhotakota,-12.9,34.28,500,Nhkotakota
kasungu,mw_workshops,Kasungu,-13.02,33.47,1300,Kasungu
8 changes: 8 additions & 0 deletions apps/picsa-tools/climate-tool/src/app/data/stations/zm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const stations: IStationMeta[] = [
extreme_rainfall_days: { definition: 'For Petauke the 95th percentile calculated to be 41.35mm' },
}),
},
{
id: 'mansa',
name: 'Mansa',
latitude: -11.1,
longitude: 28.85,
countryCode: 'zm',
definitions: CLIMATE_CHART_DEFINTIONS.zm,
},
];

export default stations;
64 changes: 64 additions & 0 deletions apps/picsa-tools/climate-tool/src/assets/summaries/mansa.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Year,End,Extreme_events,Length,Rainfall,Start
1959,296,0,,,
1960,305,0,174,1293,131
1961,309,0,178,1490,131
1962,292,0,155,1136,137
1963,285,0,157,1174,128
1964,293,0,158,967,135
1965,302,0,176,1435,126
1966,308,0,176,1050,132
1967,280,0,156,1157,124
1968,298,0,163,1132,135
1969,277,0,153,1052,124
1970,289,0,161,1460,128
1971,301,0,160,1032,141
1972,302,0,172,1072,130
1973,300,0,167,1128,133
1974,289,0,162,1183,127
1975,318,0,161,1250,157
1976,291,0,159,1002,132
1977,312,0,185,1171,127
1978,317,0,183,1573,134
1979,313,0,180,1357,133
1980,292,0,152,1148,140
1981,289,0,148,917,141
1982,260,0,128,1224,132
1983,295,0,160,1181,135
1984,301,0,167,1258,134
1985,313,0,184,1292,129
1986,302,0,177,1175,125
1987,298,0,148,814,150
1988,301,0,172,1103,129
1989,299,0,160,1246,139
1990,302,0,148,1108,154
1991,300,0,155,949,145
1992,293,0,133,961,160
1993,283,0,146,826,137
1994,290,0,116,955,174
1995,298,0,161,1233,137
1996,303,0,161,934,142
1997,287,0,148,,139
1998,306,0,161,1376,145
1999,,0,,,129
2000,296,0,,1343,
2001,308,0,184,1337,124
2002,272,0,124,1142,148
2003,301,0,134,1008,167
2004,284,0,140,1179,144
2005,301,0,,,
2006,,0,,,135
2007,288,0,160,1110,128
2008,298,0,166,1446,132
2009,292,0,161,971,131
2010,299,0,157,1166,142
2011,293,0,166,1200,127
2012,287,0,146,,141
2013,305,0,160,,145
2014,311,0,168,1093,143
2015,294,0,,,
2016,319,0,160,1104,159
2017,,0,,,140
2018,292,0,,,
2019,296,0,157,1258,139
2020,293,0,159,,134
2021,,0,,,
Loading