diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.html b/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.html new file mode 100644 index 0000000000000000000000000000000000000000..ee6f0cfe129dad9627c8f7ef5394dbd1fd83dc6e --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.html @@ -0,0 +1,15 @@ +@if (control) { + <!-- Building code --> + <mat-form-field class="grid-col-12 margin-bottom-3"> + <mat-label>{{ parameters()?.building_code.label }}</mat-label> + + <mat-select [formControl]="control"> + @for (buildingCode of buildingCodes(); track buildingCode) { + <mat-option [value]="buildingCode"> + {{ buildingCode }} + </mat-option> + } + </mat-select> + <mat-hint>{{ parameters()?.building_code.info }}</mat-hint> + </mat-form-field> +} diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.scss b/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.spec.ts b/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..881b7520dcb4db67f0b74e7f7077a477314b1868 --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.spec.ts @@ -0,0 +1,30 @@ +import {provideHttpClient} from '@angular/common/http'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {provideNoopAnimations} from '@angular/platform-browser/animations'; +import {provideRouter} from '@angular/router'; + +import {BuildingCodeControlComponent} from './building-code-control.component'; + +describe('BuildingCodeControlComponent', () => { + let component: BuildingCodeControlComponent; + let fixture: ComponentFixture<BuildingCodeControlComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BuildingCodeControlComponent], + providers: [ + provideHttpClient(), + provideNoopAnimations(), + provideRouter([]), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(BuildingCodeControlComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.ts b/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..86b2d1e0c6fbd86eee34c995583b3a4ca20bd815 --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/building-code-control/building-code-control.component.ts @@ -0,0 +1,59 @@ +import {Component, computed, Input, OnDestroy, OnInit} from '@angular/core'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatSelectModule} from '@angular/material/select'; +import {Subscription} from 'rxjs'; + +import {AppService} from '../../services/app.service'; + +@Component({ + imports: [MatFormFieldModule, ReactiveFormsModule, MatSelectModule], + selector: 'app-building-code-control', + standalone: true, + styleUrl: './building-code-control.component.scss', + templateUrl: './building-code-control.component.html', +}) +export class BuildingCodeControlComponent implements OnInit, OnDestroy { + @Input({required: true}) + control: FormControl<string>; + + buildingCodes = computed( + () => this.service.usage()?.response.parameters.building_code.values + ); + + parameters = computed(() => this.service.usage()?.response.parameters); + + private subs: Subscription[] = []; + + constructor(public service: AppService) {} + + ngOnInit(): void { + this.subs.push( + this.service.formGroup.controls.buildingCode.valueChanges.subscribe( + buildingCode => { + const values = this.service.hazardFormGroup.getRawValue(); + + if (values.buildingCode !== buildingCode) { + this.service.hazardFormGroup.patchValue({buildingCode}); + } + } + ) + ); + + this.subs.push( + this.service.hazardFormGroup.controls.buildingCode.valueChanges.subscribe( + buildingCode => { + const values = this.service.formGroup.getRawValue(); + + if (values.buildingCode !== buildingCode) { + this.service.formGroup.patchValue({buildingCode}); + } + } + ) + ); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } +} diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.html b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.html new file mode 100644 index 0000000000000000000000000000000000000000..983a81479ea0da1880d24090cb9e5a4d27c53641 --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.html @@ -0,0 +1,69 @@ +<!-- Control Panel --> +<form + class="height-full overflow-auto padding-top-1" + [formGroup]="formGroup" + (submit)="onSubmit()" +> + <!-- Building code --> + <app-building-code-control [control]="formGroup.controls.buildingCode" /> + + <!-- Model --> + <nshmp-lib-ng-hazard-model-form + [modelControl]="formGroup.controls.model" + [models]="service.availableModels()" + /> + + @if (service.hazardUsage()) { + @let usage = service.hazardUsage(); + + <!-- Latitude --> + <nshmp-lib-ng-hazard-location-form + class="latitude-input" + [locationControl]="formGroup.controls.latitude" + label="Latitude" + [bounds]="usage?.response?.models?.latitude" + /> + + <!-- Longitude --> + <nshmp-lib-ng-hazard-location-form + class="longitude-input" + [locationControl]="formGroup.controls.longitude" + label="Longitude" + [bounds]="usage.response?.models?.longitude" + /> + } + + <!-- Select site --> + <nshmp-lib-ng-map-select-site + [data]="selectSiteData()" + (siteSelect)="setLocation($event)" + /> + + <!-- Site Class --> + @if (siteClasses()) { + <nshmp-lib-ng-hazard-site-class-form + [siteClassControl]="formGroup.controls.siteClass" + [siteClasses]="siteClasses()" + [modelControl]="formGroup.controls.model" + /> + } + + <!-- Imt --> + <mat-form-field class="grid-col-12 imt-select"> + <mat-label> Intensity Measure Type </mat-label> + <mat-select [formControl]="formGroup.controls.imt"> + @for (imt of imts(); track imt) { + <mat-option [value]="imt?.value"> + {{ imt?.display }} + </mat-option> + } + </mat-select> + </mat-form-field> + + <nshmp-lib-ng-control-panel-buttons + [plotDisabled]="formGroup.invalid" + [serviceCallInfo]="service.serviceCallInfo()" + [resetDisabled]="formGroup.pristine" + (resetClick)="service.resetControlPanel()" + /> +</form> diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.scss b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.spec.ts b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1868b90816108ce53233ee046a94355b7432221 --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.spec.ts @@ -0,0 +1,30 @@ +import {provideHttpClient} from '@angular/common/http'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {provideNoopAnimations} from '@angular/platform-browser/animations'; +import {provideRouter} from '@angular/router'; + +import {ControlPanelHazardComponent} from './control-panel-hazard.component'; + +describe('ControlPanelHazardComponent', () => { + let component: ControlPanelHazardComponent; + let fixture: ComponentFixture<ControlPanelHazardComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ControlPanelHazardComponent], + providers: [ + provideHttpClient(), + provideNoopAnimations(), + provideRouter([]), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ControlPanelHazardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.ts b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f980f7d8cfda10a27917fd8311c63abd750a5bf --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-hazard/control-panel-hazard.component.ts @@ -0,0 +1,152 @@ +import {AsyncPipe} from '@angular/common'; +import {Component, computed, OnDestroy, OnInit, Signal} from '@angular/core'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatSelectModule} from '@angular/material/select'; +import { + NshmpLibNgHazardLocationFormComponent, + NshmpLibNgHazardModelFormComponent, + NshmpLibNgHazardSiteClassFormComponent, +} from '@ghsc/nshmp-lib-ng/hazard'; +import { + NshmpLibNgMapSelectSiteComponent, + SelectSiteDialogData, +} from '@ghsc/nshmp-lib-ng/map'; +import { + NshmpLibNgControlPanelButtonsComponent, + nshmpUtils, + RETURN_PERIOD_BOUNDS, + RETURN_PERIODS, +} from '@ghsc/nshmp-lib-ng/nshmp'; +import {Location} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/geo'; +import {environment} from 'projects/nshmp-apps/src/environments/environment'; +import {Subscription} from 'rxjs'; + +import {AppService} from '../../services/app.service'; +import {BuildingCodeControlComponent} from '../building-code-control/building-code-control.component'; + +@Component({ + imports: [ + NshmpLibNgHazardModelFormComponent, + NshmpLibNgHazardLocationFormComponent, + NshmpLibNgMapSelectSiteComponent, + NshmpLibNgHazardSiteClassFormComponent, + NshmpLibNgControlPanelButtonsComponent, + AsyncPipe, + MatFormFieldModule, + ReactiveFormsModule, + MatSelectModule, + BuildingCodeControlComponent, + ], + selector: 'app-control-panel-hazard', + standalone: true, + styleUrl: './control-panel-hazard.component.scss', + templateUrl: './control-panel-hazard.component.html', +}) +export class ControlPanelHazardComponent implements OnInit, OnDestroy { + /** Max and min bounds for return periods */ + returnPeriodBounds = RETURN_PERIOD_BOUNDS; + /** List of return periods */ + returnPeriods = RETURN_PERIODS; + /** Site class select menu holder */ + siteClassPlaceHolder = nshmpUtils.selectPlaceHolder(); + + /** Form field state */ + formGroup = this.service.hazardFormGroup; + + /** Data for select site component */ + selectSiteData: Signal<SelectSiteDialogData> = computed(() => { + const usage = this.service.hazardUsage(); + const nshmService = this.service.nshmService(); + + if (usage === null || nshmService === undefined) { + return; + } + + const services = environment.webServices.nshmpHazWs.services.curveServices; + + return { + mapUrl: `${nshmService.url}${services.map}?raw=true`, + nshm: this.service.hazardFormGroup.getRawValue().model, + sitesUrl: `${nshmService.url}${services.sites}?raw=true`, + }; + }); + + /** List of site class `Parameter`s */ + siteClasses = computed( + () => this.service.hazardUsage()?.response?.models.siteClasses + ); + + /** List of imt `Parameter`s */ + imts = computed(() => this.service.hazardUsage()?.response?.models.imts); + + private subs: Subscription[] = []; + + constructor(public service: AppService) {} + + ngOnInit(): void { + const controls = this.formGroup.controls; + + this.subs.push( + controls.latitude.valueChanges.subscribe(() => this.service.resetState()) + ); + + this.subs.push( + controls.longitude.valueChanges.subscribe(() => this.service.resetState()) + ); + + this.subs.push( + controls.model.valueChanges.subscribe(() => this.onModelChange()) + ); + + this.subs.push( + controls.imt.valueChanges.subscribe(() => this.service.hazardToRtgm()) + ); + + this.subs.push( + controls.siteClass.valueChanges.subscribe(() => + this.service.hazardToRtgm() + ) + ); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + + onSubmit() { + this.service.callHazardService(); + } + + setLocation(location: Location): void { + this.service.hazardFormGroup.patchValue({ + latitude: location.latitude, + longitude: location.longitude, + }); + } + + private onModelChange(): void { + const {latitude, longitude} = this.formGroup.getRawValue(); + const latitudeBounds = this.service.hazardUsage().response.models.latitude; + const longitudeBounds = + this.service.hazardUsage().response.models.longitude; + + if ( + !( + latitude >= latitudeBounds.min && + latitude <= latitudeBounds.max && + longitude >= longitudeBounds.min && + longitude <= longitudeBounds.max + ) + ) { + this.formGroup.patchValue({ + latitude: NaN, + longitude: NaN, + }); + } + + this.formGroup.controls.latitude.markAsPristine(); + this.formGroup.controls.longitude.markAsPristine(); + this.service.resetState(); + } +} diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.html b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4ac652ac54d9c398c7c0378b3df7033a3b5f3a62 --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.html @@ -0,0 +1,48 @@ +<form + [formGroup]="formGroup" + class="height-full overflow-auto padding-top-1" + (submit)="service.callService()" +> + <!-- Building code --> + <app-building-code-control [control]="formGroup.controls.buildingCode" /> + + <!-- Label --> + <mat-form-field class="grid-col-12 margin-bottom-05"> + <mat-label>{{ parameters()?.label.label }}</mat-label> + <input matInput type="text" [formControl]="formGroup.controls.label" /> + <mat-hint>{{ parameters()?.label.info }}</mat-hint> + </mat-form-field> + + <!-- IMLs --> + <mat-form-field class="grid-col-12 margin-bottom-05"> + <mat-label> + {{ parameters()?.iml.label }} ({{ parameters()?.iml.units }}) + </mat-label> + <input matInput type="text" [formControl]="formGroup.controls.iml" /> + <mat-hint>{{ parameters()?.iml.info }}</mat-hint> + @if (formGroup.controls.iml.errors?.imlNotSameLength) { + <mat-error>Must be same length as AFEs</mat-error> + } @else { + <mat-error>Must be a comma delimited list of numbers</mat-error> + } + </mat-form-field> + + <!-- AFEs --> + <mat-form-field class="grid-col-12 margin-bottom-05"> + <mat-label>{{ parameters()?.afe.label }}</mat-label> + <input matInput type="text" [formControl]="formGroup.controls.afe" /> + <mat-hint class="hint">{{ parameters()?.afe.info }}</mat-hint> + @if (formGroup.controls.afe.errors?.afeNotSameLength) { + <mat-error>Must be same length as IMLs</mat-error> + } @else { + <mat-error>Must be a comma delimited list of numbers</mat-error> + } + </mat-form-field> + + <nshmp-lib-ng-control-panel-buttons + [plotDisabled]="formGroup.invalid" + [serviceCallInfo]="service.serviceCallInfo()" + [resetDisabled]="formGroup.pristine" + (resetClick)="service.resetControlPanel()" + /> +</form> diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.scss b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..479ce114e334096c48ce47bfb3705bd69ef28ea9 --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.scss @@ -0,0 +1,3 @@ +.hint { + font-size: 10.9px; +} diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.spec.ts b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffcadf39d54a28a0250234fdbe31657c71a78bdb --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.spec.ts @@ -0,0 +1,30 @@ +import {provideHttpClient} from '@angular/common/http'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {provideNoopAnimations} from '@angular/platform-browser/animations'; +import {provideRouter} from '@angular/router'; + +import {ControlPanelInputComponent} from './control-panel-input.component'; + +describe('ControlPanelInputComponent', () => { + let component: ControlPanelInputComponent; + let fixture: ComponentFixture<ControlPanelInputComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ControlPanelInputComponent], + providers: [ + provideHttpClient(), + provideNoopAnimations(), + provideRouter([]), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ControlPanelInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.ts b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..73a25df9f0732bdce0a0f829ba512ea0a9ffb032 --- /dev/null +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel-input/control-panel-input.component.ts @@ -0,0 +1,63 @@ +import {Component, computed, OnDestroy, OnInit} from '@angular/core'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import {MatDivider} from '@angular/material/divider'; +import {MatIcon} from '@angular/material/icon'; +import {MatError, MatHint, MatInputModule} from '@angular/material/input'; +import {MatFormField, MatSelectModule} from '@angular/material/select'; +import {NshmpLibNgControlPanelButtonsComponent} from '@ghsc/nshmp-lib-ng/nshmp'; +import {Subscription} from 'rxjs'; + +import {AppService} from '../../services/app.service'; +import {BuildingCodeControlComponent} from '../building-code-control/building-code-control.component'; + +@Component({ + imports: [ + MatFormField, + MatSelectModule, + MatInputModule, + ReactiveFormsModule, + MatIcon, + MatButtonModule, + MatDivider, + NshmpLibNgControlPanelButtonsComponent, + MatHint, + MatError, + BuildingCodeControlComponent, + ], + selector: 'app-control-panel-input', + standalone: true, + styleUrl: './control-panel-input.component.scss', + templateUrl: './control-panel-input.component.html', +}) +export class ControlPanelInputComponent implements OnInit, OnDestroy { + formGroup = this.service.formGroup; + + parameters = computed(() => this.service.usage()?.response.parameters); + + subs: Subscription[] = []; + + constructor(public service: AppService) {} + + ngOnInit(): void { + const controls = this.formGroup.controls; + + this.subs.push( + controls.afe.valueChanges.subscribe(() => this.service.resetState()) + ); + + this.subs.push( + controls.buildingCode.valueChanges.subscribe(() => + this.service.resetState() + ) + ); + + this.subs.push( + controls.iml.valueChanges.subscribe(() => this.service.resetState()) + ); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } +} diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.html b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.html index eb8c7c4bc32957b4f35195484f8ef619bf87c09c..146d47c37be1819c282ff11a4fecc7b3fe087cb3 100644 --- a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.html +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.html @@ -1,59 +1,9 @@ -<form - [formGroup]="formGroup" - class="height-full overflow-auto padding-top-1" - (submit)="service.callService()" -> - <!-- Label --> - <mat-form-field class="grid-col-12 margin-bottom-05"> - <mat-label>{{ parameters()?.label.label }}</mat-label> - <input matInput type="text" [formControl]="formGroup.controls.label" /> - <mat-hint>{{ parameters()?.label.info }}</mat-hint> - </mat-form-field> - - <!-- Building code --> - <mat-form-field class="grid-col-12 margin-bottom-3"> - <mat-label>{{ parameters()?.building_code.label }}</mat-label> - - <mat-select [formControl]="formGroup.controls.buildingCode"> - @for (buildingCode of buildingCodes(); track buildingCode) { - <mat-option [value]="buildingCode"> - {{ buildingCode }} - </mat-option> - } - </mat-select> - <mat-hint>{{ parameters()?.building_code.info }}</mat-hint> - </mat-form-field> - - <!-- IMLs --> - <mat-form-field class="grid-col-12 margin-bottom-05"> - <mat-label> - {{ parameters()?.iml.label }} ({{ parameters()?.iml.units }}) - </mat-label> - <input matInput type="text" [formControl]="formGroup.controls.iml" /> - <mat-hint>{{ parameters()?.iml.info }}</mat-hint> - @if (formGroup.controls.iml.errors?.imlNotSameLength) { - <mat-error>Must be same length as AFEs</mat-error> - } @else { - <mat-error>Must be a comma delimited list of numbers</mat-error> - } - </mat-form-field> - - <!-- AFEs --> - <mat-form-field class="grid-col-12 margin-bottom-05"> - <mat-label>{{ parameters()?.afe.label }}</mat-label> - <input matInput type="text" [formControl]="formGroup.controls.afe" /> - <mat-hint class="hint">{{ parameters()?.afe.info }}</mat-hint> - @if (formGroup.controls.afe.errors?.afeNotSameLength) { - <mat-error>Must be same length as IMLs</mat-error> - } @else { - <mat-error>Must be a comma delimited list of numbers</mat-error> - } - </mat-form-field> - - <nshmp-lib-ng-control-panel-buttons - [plotDisabled]="formGroup.invalid" - [serviceCallInfo]="service.serviceCallInfo()" - [resetDisabled]="formGroup.pristine" - (resetClick)="service.resetControlPanel()" - /> -</form> +<mat-tab-group class="height-full"> + <mat-tab label="Manual Input" bodyClass="padding-top-2"> + <app-control-panel-input /> + </mat-tab> + + <mat-tab label="Hazard Calc" bodyClass="padding-top-2"> + <app-control-panel-hazard /> + </mat-tab> +</mat-tab-group> diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.scss b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.scss index 479ce114e334096c48ce47bfb3705bd69ef28ea9..9a03f6e52f9d722453770e186cdc7c6936e1885c 100644 --- a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.scss +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.scss @@ -1,3 +1,3 @@ -.hint { - font-size: 10.9px; +.mat-mdc-tab-body-wrapper { + height: 100%; } diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.ts b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.ts index 319278c965a0261ad84c1821832ade650800a12e..932848e3ce3fa10e1f3684da438f2e331c64d045 100644 --- a/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.ts +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/components/control-panel/control-panel.component.ts @@ -1,63 +1,19 @@ -import {Component, computed, OnDestroy, OnInit} from '@angular/core'; -import {ReactiveFormsModule} from '@angular/forms'; -import {MatButtonModule} from '@angular/material/button'; -import {MatDivider} from '@angular/material/divider'; -import {MatIcon} from '@angular/material/icon'; -import {MatError, MatHint, MatInputModule} from '@angular/material/input'; -import {MatFormField, MatSelectModule} from '@angular/material/select'; -import {NshmpLibNgControlPanelButtonsComponent} from '@ghsc/nshmp-lib-ng/nshmp'; -import {Subscription} from 'rxjs'; +import {Component, ViewEncapsulation} from '@angular/core'; +import {MatTabsModule} from '@angular/material/tabs'; -import {AppService} from '../../services/app.service'; +import {ControlPanelHazardComponent} from '../control-panel-hazard/control-panel-hazard.component'; +import {ControlPanelInputComponent} from '../control-panel-input/control-panel-input.component'; @Component({ + encapsulation: ViewEncapsulation.None, imports: [ - MatFormField, - MatSelectModule, - MatInputModule, - ReactiveFormsModule, - MatIcon, - MatButtonModule, - MatDivider, - NshmpLibNgControlPanelButtonsComponent, - MatHint, - MatError, + MatTabsModule, + ControlPanelInputComponent, + ControlPanelHazardComponent, ], selector: 'app-control-panel', standalone: true, styleUrl: './control-panel.component.scss', templateUrl: './control-panel.component.html', }) -export class ControlPanelComponent implements OnInit, OnDestroy { - formGroup = this.service.formGroup; - - buildingCodes = computed( - () => this.service.usage()?.response.parameters.building_code.values - ); - - parameters = computed(() => this.service.usage()?.response.parameters); - - subs: Subscription[] = []; - - constructor(public service: AppService) {} - - ngOnInit(): void { - const controls = this.formGroup.controls; - - this.subs.push( - controls.afe.valueChanges.subscribe(() => this.service.resetState()) - ); - this.subs.push( - controls.buildingCode.valueChanges.subscribe(() => - this.service.resetState() - ) - ); - this.subs.push( - controls.iml.valueChanges.subscribe(() => this.service.resetState()) - ); - } - - ngOnDestroy(): void { - this.subs.forEach(sub => sub.unsubscribe()); - } -} +export class ControlPanelComponent {} diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/models/state.model.ts b/projects/nshmp-apps/src/app/designmaps/rtgm/models/state.model.ts index f11fbd878c52bc4053ed9cbf8e1d0642bbdef2c9..544b4cb5df5b6fa9c4e1771cf2d847558b819dcb 100644 --- a/projects/nshmp-apps/src/app/designmaps/rtgm/models/state.model.ts +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/models/state.model.ts @@ -3,10 +3,24 @@ import { RtgmCalcResponse, RtgmUsageResponse, } from '@ghsc/nshmp-utils-ts/libs/erp/rtgm'; +import { + StaticHazardResponse, + StaticHazardUsage, +} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-static/hazard-service'; +import {StaticNshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-static/nshm-service'; +import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata'; import {RtgmPlots} from './plots.model'; export interface AppState { + /** Available NSHMs */ + availableModels: Parameter[]; + /** Hazard service responses */ + hazardServiceResponse: StaticHazardResponse; + /** Hazard usage responses */ + hazardUsageResponses: Map<string, StaticHazardUsage>; + /** NSHM services */ + nshmServices: StaticNshmMetadata[]; /** RTGM plots */ plots: RtgmPlots; /** Service call info */ diff --git a/projects/nshmp-apps/src/app/designmaps/rtgm/services/app.service.ts b/projects/nshmp-apps/src/app/designmaps/rtgm/services/app.service.ts index cc8b8f5e6a754e766db950f518d47d787421bc0a..0b11d9762012499a700355ee95162362b07f8d98 100644 --- a/projects/nshmp-apps/src/app/designmaps/rtgm/services/app.service.ts +++ b/projects/nshmp-apps/src/app/designmaps/rtgm/services/app.service.ts @@ -2,6 +2,12 @@ import {HttpParams} from '@angular/common/http'; import {computed, Injectable, Signal, signal} from '@angular/core'; import {AbstractControl, FormBuilder, Validators} from '@angular/forms'; import {ActivatedRoute, Router} from '@angular/router'; +import { + HazardControlForm, + HazardService, + hazardUtils, + StaticNshms, +} from '@ghsc/nshmp-lib-ng/hazard'; import { NshmpService, ServiceCallInfo, @@ -17,6 +23,16 @@ import { } from '@ghsc/nshmp-utils-ts/libs/erp/rtgm'; import {Maths} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/calc'; import {XySequence} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/data'; +import {Imt} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm'; +import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm'; +import { + HazardResponseMetadata, + StaticHazardResponse, + StaticHazardUsage, + StaticResponseData, +} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-static/hazard-service'; +import {StaticNshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-static/nshm-service'; +import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata'; import * as d3Color from 'd3-scale-chromatic'; import deepEqual from 'deep-equal'; import {AxisType, PlotData} from 'plotly.js'; @@ -24,7 +40,7 @@ import {environment} from 'projects/nshmp-apps/src/environments/environment'; import {AppServiceModel} from 'projects/nshmp-apps/src/shared/models/app-service.model'; import {SharedService} from 'projects/nshmp-apps/src/shared/services/shared.service'; import {apps} from 'projects/nshmp-apps/src/shared/utils/applications.utils'; -import {catchError} from 'rxjs'; +import {catchError, forkJoin} from 'rxjs'; import {ControlForm} from '../models/control-form.model'; import {RtgmPlots} from '../models/plots.model'; @@ -48,6 +64,11 @@ export interface IterationPlotData { integralHazardFragilityPlotData: Partial<PlotData>[]; } +export interface RtgmHazardControlForm extends HazardControlForm { + buildingCode: string; + imt: Imt; +} + @Injectable({ providedIn: 'root', }) @@ -55,7 +76,13 @@ export class AppService extends SharedService implements AppServiceModel<AppState, ControlForm> { + /** nshmp-haz-ws web config */ + private nshmpWsStatic = environment.webServices.nshmpWsStatic; + private aspectRatio = '21:9'; + /** Hazard endpoint */ + private hazardServiceEndpoint = + this.nshmpWsStatic.services.curveServices.hazard; /** RTGM web config */ private rtgm = environment.webServices.rtgm; /** RTGM calc url */ @@ -63,6 +90,11 @@ export class AppService /** Form group */ formGroup = this.formBuilder.group<ControlForm>(this.defaultFormValues()); + hazardFormGroup = this.formBuilder.group<RtgmHazardControlForm>({ + ...hazardUtils.hazardDefaultFormValues(), + buildingCode: this.defaultFormValues().buildingCode, + imt: Imt.PGA, + }); /** App state */ state = signal<AppState>(this.initialState()); @@ -71,12 +103,40 @@ export class AppService private spinnerService: SpinnerService, private nshmpService: NshmpService, private route: ActivatedRoute, - private router: Router + private router: Router, + private hazardService: HazardService ) { super(); this.addValidators(); } + /** + * Returns the available models. + */ + get availableModels(): Signal<Parameter[]> { + return computed(() => this.state().availableModels); + } + + get hazardUsage(): Signal<StaticHazardUsage> { + return computed(() => + this.state().hazardUsageResponses.get( + this.hazardFormGroup.getRawValue().model + ) + ); + } + + /** + * Returns the metadata of the NSHM observable. + */ + get nshmService(): Signal<StaticNshmMetadata> { + return computed(() => + this.state().nshmServices.find( + nshmService => + nshmService.model === this.hazardFormGroup.getRawValue().model + ) + ); + } + /** * Returns the plots. */ @@ -155,6 +215,31 @@ export class AppService this.formGroup.controls.label.addValidators(required); } + callHazardService(): void { + const spinnerRef = this.spinnerService.show('Calling hazard service ...'); + const values = this.hazardFormGroup.getRawValue(); + + const service = this.state().nshmServices.find( + nshmService => nshmService.model === values.model + ); + const serviceUrl = `${service.url}${this.hazardServiceEndpoint}`; + const url = `${serviceUrl}/${values.longitude}/${values.latitude}`; + + this.nshmpService + .callService$<StaticHazardResponse>(url) + .pipe( + catchError((error: Error) => { + spinnerRef.close(); + return this.nshmpService.throwError$(error); + }) + ) + .subscribe(hazardServiceResponse => { + spinnerRef.close(); + this.updateState({hazardServiceResponse}); + this.hazardToRtgm(); + }); + } + callService(): void { const spinnerRef = this.spinnerService.show(SpinnerService.MESSAGE_SERVICE); @@ -211,28 +296,60 @@ export class AppService }; } + hazardToRtgm(): void { + const values = this.hazardFormGroup.getRawValue(); + + const responseData = this.getHazardResponseData(this.state(), values); + + if (responseData === null || responseData === undefined) { + return; + } + + this.formGroup.patchValue({ + afe: responseData.data.ys.join(','), + iml: responseData.data.xs.join(','), + label: `${values.model} ${values.siteClass} ${values.imt}`, + }); + + this.callService(); + } + init(): void { const spinnerRef = this.spinnerService.show( SpinnerService.MESSAGE_METADATA ); - this.nshmpService - .callService$<RtgmUsageResponse>(this.serviceUrl) + const rtgm = this.nshmpService.callService$<RtgmUsageResponse>( + this.serviceUrl + ); + const hazard = this.hazardService.staticNshms$( + `${this.nshmpWsStatic.url}${this.nshmpWsStatic.services.nshms}`, + this.hazardServiceEndpoint + ); + + forkJoin([rtgm, hazard]) .pipe( catchError((error: Error) => { spinnerRef.close(); return this.nshmpService.throwError$(error); }) ) - .subscribe(usageResponse => { - this.handleUsageResponse(usageResponse); + .subscribe(([usageResponse, nshms]) => { + this.handleUsageResponse(usageResponse, nshms); this.initialFormSet(); spinnerRef.close(); }); } initialState(): AppState { + const hazardUsageResponses: Map<string, StaticHazardUsage> = new Map(); + hazardUsageResponses.set(NshmId.CONUS_2018, null); + return { + availableModels: [], + hazardServiceResponse: null, + hazardUsageResponses, + nshmServices: [], plots: this.defaultPlots(), serviceCallInfo: { serviceCalls: [], @@ -277,6 +394,7 @@ export class AppService resetState(): void { this.updateState({ + hazardServiceResponse: null, plots: this.defaultPlots(), serviceCallInfo: { ...this.state().serviceCallInfo, @@ -659,6 +777,28 @@ export class AppService }; } + /** + * Returns the response data for a site class. + * + * @param state The current state + */ + private getHazardResponseData( + state: AppState, + values: RtgmHazardControlForm + ): StaticResponseData<HazardResponseMetadata> { + const siteClass = values.siteClass; + + const responseData = state.hazardServiceResponse + ? state.hazardServiceResponse.response.find(response => { + return response.find(data => data.metadata.siteClass === siteClass); + }) + : null; + + return responseData?.find( + data => data.metadata.imt.value === values.imt.toString() + ); + } + private handleServiceResponse(serviceResponse: RtgmCalcResponse): void { this.updateState({ serviceCallInfo: { @@ -671,8 +811,14 @@ export class AppService this.createPlots(); } - private handleUsageResponse(usageResponse: RtgmUsageResponse): void { + private handleUsageResponse( + usageResponse: RtgmUsageResponse, + nshms: StaticNshms + ): void { this.updateState({ + availableModels: nshms.models, + hazardUsageResponses: nshms.usageResponses, + nshmServices: nshms.nshmServices, serviceCallInfo: { ...this.state().serviceCallInfo, usage: [this.serviceUrl], diff --git a/projects/nshmp-apps/src/app/services/components/content/content.component.ts b/projects/nshmp-apps/src/app/services/components/content/content.component.ts index 2f9ee7dcc31266216a44b629e4e32f9b7524e689..0d9ec4aa2a84861101ae801ba7bb493914f3440e 100644 --- a/projects/nshmp-apps/src/app/services/components/content/content.component.ts +++ b/projects/nshmp-apps/src/app/services/components/content/content.component.ts @@ -175,6 +175,29 @@ export class ContentComponent implements AfterViewInit { subtitle: 'Pre-Computed and Published Hazard Curves', title: 'NSHM Static Hazard Curve Services', }, + // RTGM services + { + applicationsUsedIn: [apps().designMaps.rtgm], + id: ServiceGroupId.RTGM, + images: [ + { + darkModeImage: 'rtgm-derivative-dark-mode.webp', + image: 'rtgm-derivative.webp', + }, + { + darkModeImage: 'rtgm-integral-dark-mode.webp', + image: 'rtgm-integral.webp', + }, + ], + services$: of([ + { + ...environment.webServices.rtgm, + swaggerEndPoint: environment.webServices.rtgm.services.swagger, + }, + ]), + subtitle: 'RTGM from hazard curves', + title: 'Risk-Targeted Ground Motion Service', + }, // NCM services { applicationsUsedIn: [], @@ -210,20 +233,6 @@ export class ContentComponent implements AfterViewInit { subtitle: 'Risk-Targeted Desgin Response Spectra for 2023 Edition', title: 'AASHTO 2023 Services', }, - // RTGM services - { - applicationsUsedIn: [], - id: ServiceGroupId.RTGM, - images: [], - services$: of([ - { - ...environment.webServices.rtgm, - swaggerEndPoint: environment.webServices.rtgm.services.swagger, - }, - ]), - subtitle: 'RTGM from hazard curves', - title: 'Risk-Targeted Ground Motion Service', - }, ]; constructor( diff --git a/projects/nshmp-apps/src/assets/services/rtgm-derivative-dark-mode.webp b/projects/nshmp-apps/src/assets/services/rtgm-derivative-dark-mode.webp new file mode 100644 index 0000000000000000000000000000000000000000..db9d0f7a99ebf8630813f3a0c1c838b623b55deb Binary files /dev/null and b/projects/nshmp-apps/src/assets/services/rtgm-derivative-dark-mode.webp differ diff --git a/projects/nshmp-apps/src/assets/services/rtgm-derivative.webp b/projects/nshmp-apps/src/assets/services/rtgm-derivative.webp new file mode 100644 index 0000000000000000000000000000000000000000..b248fed3e14b7266f17e8d4f6c0783f52ce0a03c Binary files /dev/null and b/projects/nshmp-apps/src/assets/services/rtgm-derivative.webp differ diff --git a/projects/nshmp-apps/src/assets/services/rtgm-integral-dark-mode.webp b/projects/nshmp-apps/src/assets/services/rtgm-integral-dark-mode.webp new file mode 100644 index 0000000000000000000000000000000000000000..334dde551b0dd33643fbefff7fcfe9f014a4b896 Binary files /dev/null and b/projects/nshmp-apps/src/assets/services/rtgm-integral-dark-mode.webp differ diff --git a/projects/nshmp-apps/src/assets/services/rtgm-integral.webp b/projects/nshmp-apps/src/assets/services/rtgm-integral.webp new file mode 100644 index 0000000000000000000000000000000000000000..cef31a580eee6243d1fbe0a29aee6e2f42a5df85 Binary files /dev/null and b/projects/nshmp-apps/src/assets/services/rtgm-integral.webp differ