File

services/app.service.ts

Description

Entrypoint to store for dynamic hazard application.

Extends

SharedService

Index

Properties
Methods
Accessors

Constructor

constructor(formBuilder: FormBuilder, spinnerService: SpinnerService, nshmpService: NshmpService, hazardService: HazardService, route: ActivatedRoute, location: LocationService)
Parameters :
Name Type Optional
formBuilder FormBuilder No
spinnerService SpinnerService No
nshmpService NshmpService No
hazardService HazardService No
route ActivatedRoute No
location LocationService No

Methods

Private addRequiredValidator
addRequiredValidator(control: AbstractControl)
Parameters :
Name Type Optional
control AbstractControl No
Returns : void
addValidators
addValidators()
Returns : void
callService
callService()

Call the hazard service.

Returns : void
Private createHazardPlotData
createHazardPlotData(responseData: HazardResponseData, plot: NshmpPlot, form: DynamicHazardControlForm, returnPeriod: number)

Create the hazard plot data.

Parameters :
Name Type Optional Description
responseData HazardResponseData No

The hazard response data

plot NshmpPlot No

Plot

form DynamicHazardControlForm No

Form values

returnPeriod number No

Return period

Returns : PlotlyPlot
createPlots
createPlots()
Returns : void
Private createServiceEndpoint
createServiceEndpoint(serviceUrl: string, values: DynamicHazardControlForm)
Parameters :
Name Type Optional
serviceUrl string No
values DynamicHazardControlForm No
Returns : string
defaultFormValues
defaultFormValues()

Returns the default form values.

Returns : DynamicHazardControlForm
init
init()

Initialize applicaiton.

Returns : void
Private initialFormSet
initialFormSet()
Returns : void
initialState
initialState()

Hazard app initial state

Returns : AppState
resetControlPanel
resetControlPanel()

Reset the control panel.

Returns : void
resetSettings
resetSettings()

Reset the plot settings.

Returns : void
resetState
resetState()
Returns : void
Private responseSpectrumPlotData
responseSpectrumPlotData(spectra: ResponseSpectra, plot: NshmpPlot, returnPeriodValue: number)

Return the response spectrum plot data.

Parameters :
Name Type Optional Description
spectra ResponseSpectra No

Response spectra

plot NshmpPlot No

plot

returnPeriodValue number No

Return period

Returns : PlotlyPlot
Private responseToPlots
responseToPlots(state: AppState, formGroup: FormGroupControls)

Transform the service response the plot data.

Parameters :
Name Type Optional Description
state AppState No

The current state

formGroup FormGroupControls<DynamicHazardControlForm> No
Returns : Map<string, NshmpPlot>
Private returnPeriodPlotData
returnPeriodPlotData(responseData: HazardResponseData, sourceType: string, returnPeriod: number)

Returns the plot data for the return period.

Parameters :
Name Type Optional Description
responseData HazardResponseData No

Hazard response data

sourceType string No

Hazard source type

returnPeriod number No

Retrun period

Returns : Partial<PlotData>
setLocation
setLocation(location: Location)

Set the location form fields.

Parameters :
Name Type Optional Description
location Location No

The location

Returns : void
updateState
updateState(state: Partial<AppState>)
Parameters :
Name Type Optional
state Partial<AppState> No
Returns : void
Private updateUrl
updateUrl()
Returns : void
Private updateUsageUrl
updateUsageUrl()
Returns : void

Properties

Readonly formGroup
Default value : this.formBuilder.group<DynamicHazardControlForm>( this.defaultFormValues(), )
Private nshmpHazWs
Default value : environment.webServices.nshmpHazWs

nshmp-haz-ws web config

Private serviceEndpoint
Default value : this.nshmpHazWs.services.curveServices.hazard

Hazard endpoint

Readonly state
Default value : signal<AppState>(this.initialState())

Application state

Accessors

availableModels
getavailableModels()

Returns the available models.

Returns : Signal<Parameter[]>
hazardPlotData
gethazardPlotData()

Returns the hazard plot data.

Returns : Signal<PlotlyPlot>
hazardPlotSettings
gethazardPlotSettings()

Returns the hazard plot settings form.

Returns : Signal<FormGroup<NshmpPlotSettingFormGroup>>
hazardPlotState
gethazardPlotState()

Returns the hazard plot.

Returns : Signal<NshmpPlot>
nshmService
getnshmService()

Returns the metadata of the NSHM observable.

Returns : Signal<NshmMetadata>
plots
getplots()
responseSpectra
getresponseSpectra()

Returns the response spectra

Returns : Signal<ResponseSpectra>
serviceCallInfo
getserviceCallInfo()

Returns the service call info.

Returns : Signal<ServiceCallInfo>
serviceResponse
getserviceResponse()

Returns the disagg response.

Returns : Signal<HazardCalcResponse>
spectrumPlotData
getspectrumPlotData()

Returns the spectrum plot data.

Returns : Signal<PlotlyPlot>
spectrumPlotSettings
getspectrumPlotSettings()

Returns the spectrum plot settings form.

Returns : Signal<FormGroup<NshmpPlotSettingFormGroup>>
spectrumPlotState
getspectrumPlotState()

Returns the response spectrum plot.

Returns : Signal<NshmpPlot>
usage
getusage()

Return the usage for the selected model.

Returns : Signal<HazardUsageResponse>
import {Location as LocationService} from '@angular/common';
import {HttpParams} from '@angular/common/http';
import {computed, Injectable, Signal, signal} from '@angular/core';
import {
  AbstractControl,
  FormBuilder,
  FormGroup,
  Validators,
} from '@angular/forms';
import {ActivatedRoute} from '@angular/router';
import {
  DynamicHazardControlForm,
  HazardPlots,
  HazardService,
  hazardUtils,
  ResponseSpectra,
} from '@ghsc/nshmp-lib-ng/hazard';
import {
  FormGroupControls,
  NshmpService,
  returnPeriodAltName,
  ServiceCallInfo,
  SpinnerService,
} from '@ghsc/nshmp-lib-ng/nshmp';
import {
  NshmpPlot,
  NshmpPlotSettingFormGroup,
  plotUtils,
} from '@ghsc/nshmp-lib-ng/plot';
import {
  HazardCalcResponse,
  HazardRequestMetadata,
  HazardResponseData,
  HazardUsageResponse,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service';
import {Location} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/geo';
import {Imt, imtToPeriod} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
import {
  SourceType,
  sourceTypeToCapitalCase,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
import {PlotlyPlot} from '@ghsc/nshmp-utils-ts/libs/plotly';
import deepEqual from 'deep-equal';
import {PlotData} from 'plotly.js';
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/operators';

import {DynamicHazardQuery} from '../models/query.model';
import {AppState} from '../models/state.model';

/**
 * Entrypoint to store for dynamic hazard application.
 */
@Injectable({
  providedIn: 'root',
})
export class AppService
  extends SharedService
  implements AppServiceModel<AppState, DynamicHazardControlForm>
{
  /** nshmp-haz-ws web config */
  private nshmpHazWs = environment.webServices.nshmpHazWs;
  /** Hazard endpoint */
  private serviceEndpoint = this.nshmpHazWs.services.curveServices.hazard;

  readonly formGroup = this.formBuilder.group<DynamicHazardControlForm>(
    this.defaultFormValues(),
  );

  /** Application state */
  readonly state = signal<AppState>(this.initialState());

  constructor(
    private formBuilder: FormBuilder,
    private spinnerService: SpinnerService,
    private nshmpService: NshmpService,
    private hazardService: HazardService,
    private route: ActivatedRoute,
    private location: LocationService,
  ) {
    super();
    this.addValidators();
  }

  /**
   * Returns the available models.
   */
  get availableModels(): Signal<Parameter[]> {
    return computed(() => this.state().availableModels);
  }

  /**
   * Returns the hazard plot data.
   */
  get hazardPlotData(): Signal<PlotlyPlot> {
    return computed(() => this.hazardPlotState().plotData);
  }

  /**
   * Returns the hazard plot settings form.
   */
  get hazardPlotSettings(): Signal<FormGroup<NshmpPlotSettingFormGroup>> {
    return computed(() => this.hazardPlotState().settingsForm);
  }

  /**
   * Returns the hazard plot.
   */
  get hazardPlotState(): Signal<NshmpPlot> {
    return computed(() => this.state().plots.get(HazardPlots.HAZARD));
  }

  /**
   * Returns the metadata of the NSHM observable.
   */
  get nshmService(): Signal<NshmMetadata> {
    return computed(() =>
      this.state().nshmServices.find(
        nshmService => nshmService.model === this.formGroup.getRawValue().model,
      ),
    );
  }

  get plots(): Signal<Map<string, NshmpPlot>> {
    return computed(() => this.state().plots);
  }

  /**
   * Returns the response spectra
   */
  get responseSpectra(): Signal<ResponseSpectra> {
    return computed(() => this.state().responseSpectra);
  }

  /**
   * Returns the service call info.
   */
  get serviceCallInfo(): Signal<ServiceCallInfo> {
    return computed(() => this.state().serviceCallInfo);
  }

  /**
   * Returns the disagg response.
   */
  get serviceResponse(): Signal<HazardCalcResponse> {
    return computed(() => this.state().serviceResponse);
  }

  /**
   * Returns the spectrum plot data.
   */
  get spectrumPlotData(): Signal<PlotlyPlot> {
    return computed(() => this.spectrumPlotState().plotData);
  }

  /**
   * Returns the spectrum plot settings form.
   */
  get spectrumPlotSettings(): Signal<FormGroup<NshmpPlotSettingFormGroup>> {
    return computed(() => this.spectrumPlotState().settingsForm);
  }

  /**
   * Returns the response spectrum plot.
   */
  get spectrumPlotState(): Signal<NshmpPlot> {
    return computed(() => this.state().plots.get(HazardPlots.SPECTRUM));
  }

  /**
   * Return the usage for the selected model.
   */
  get usage(): Signal<HazardUsageResponse> {
    return computed(() =>
      this.state().usageResponses?.get(this.formGroup.getRawValue().model),
    );
  }

  addValidators(): void {
    const controls = this.formGroup.controls;

    this.addRequiredValidator(controls.latitude);
    this.addRequiredValidator(controls.longitude);
    this.addRequiredValidator(controls.model);
    this.addRequiredValidator(controls.vs30);
    this.addRequiredValidator(controls.returnPeriod);

    controls.latitude.addValidators(this.validateNan());
    controls.longitude.addValidators(this.validateNan());
  }

  /**
   * Call the hazard service.
   */
  callService(): void {
    const spinnerRef = this.spinnerService.show(
      `${SpinnerService.MESSAGE_SERVICE}
      <br>
      (Could take 30+ seconds)
      `,
    );
    const values = this.formGroup.getRawValue();

    const service = this.state().nshmServices.find(
      nshmService => nshmService.model === values.model,
    );
    const serviceUrl = `${service.url}${this.serviceEndpoint}`;
    const url = this.createServiceEndpoint(serviceUrl, values);

    this.nshmpService
      .callService$<HazardCalcResponse>(url)
      .pipe(
        catchError((error: Error) => {
          spinnerRef.close();
          return this.nshmpService.throwError$(error);
        }),
      )
      .subscribe(serviceResponse => {
        spinnerRef.close();

        const responseSpectra = hazardUtils.responseSpectra(
          serviceResponse.response.hazardCurves,
          this.formGroup.getRawValue(),
        );

        this.updateState({
          responseSpectra,
          serviceCallInfo: {
            ...this.state().serviceCallInfo,
            serviceCalls: [url],
          },
          serviceResponse,
        });

        this.createPlots();
      });
  }

  createPlots(): void {
    const plots = this.responseToPlots(this.state(), this.formGroup);
    this.updateState({plots});
  }

  /**
   * Returns the default form values.
   */
  defaultFormValues(): DynamicHazardControlForm {
    return {
      ...hazardUtils.hazardDefaultFormValues(),
      sourceType: sourceTypeToCapitalCase(SourceType.TOTAL),
      vs30: 760,
    };
  }

  /**
   * Initialize applicaiton.
   */
  init(): void {
    const spinnerRef = this.spinnerService.show(
      SpinnerService.MESSAGE_METADATA,
    );

    this.hazardService
      .dynamicNshms$<HazardRequestMetadata>(
        `${this.nshmpHazWs.url}${this.nshmpHazWs.services.nshms}`,
        this.serviceEndpoint,
      )
      .pipe(
        catchError((error: Error) => {
          spinnerRef.close();
          return this.nshmpService.throwError$(error);
        }),
      )
      .subscribe(({models, nshmServices, usageResponses}) => {
        spinnerRef.close();

        this.updateState({
          availableModels: models,
          nshmServices,
          usageResponses,
        });

        this.updateUsageUrl();
        this.initialFormSet();
      });
  }

  /**
   * Hazard app initial state
   */
  initialState(): AppState {
    const usageResponses: Map<string, HazardUsageResponse> = new Map();
    usageResponses.set(NshmId.CONUS_2018, null);

    return {
      availableModels: [],
      nshmServices: [],
      plots: hazardUtils.hazardDefaultPlots(),
      responseSpectra: null,
      serviceCallInfo: {
        serviceCalls: [],
        serviceName: 'Dynamic Hazard Curves',
        usage: [],
      },
      serviceResponse: null,
      usageResponses,
    };
  }

  /**
   * Reset the control panel.
   */
  resetControlPanel(): void {
    this.formGroup.reset(this.defaultFormValues());
    this.resetState();
  }

  /**
   * Reset the plot settings.
   */
  resetSettings(): void {
    super.resetPlotSettings({
      currentPlots: this.state().plots,
      defaultPlots: this.initialState().plots,
    });
  }

  resetState(): void {
    this.updateState({
      plots: this.initialState().plots,
      serviceCallInfo: {
        ...this.state().serviceCallInfo,
        serviceCalls: [],
      },
      serviceResponse: null,
    });
    this.updateUsageUrl();
  }

  /**
   * Set the location form fields.
   *
   * @param location The location
   */
  setLocation(location: Location): void {
    this.formGroup.patchValue({
      latitude: location.latitude,
      longitude: location.longitude,
    });
  }

  updateState(state: Partial<AppState>): void {
    const updatedState = {
      ...this.state(),
      ...state,
    };

    if (!deepEqual(updatedState, this.state())) {
      this.state.set({
        ...this.state(),
        ...state,
      });
    }
  }

  private addRequiredValidator(control: AbstractControl): void {
    control.addValidators(control => Validators.required(control));
  }

  /**
   * Create the hazard plot data.
   *
   * @param responseData The hazard response data
   * @param plot Plot
   * @param form Form values
   * @param returnPeriod Return period
   */
  private createHazardPlotData(
    responseData: HazardResponseData,
    plot: NshmpPlot,
    form: DynamicHazardControlForm,
    returnPeriod: number,
  ): PlotlyPlot {
    const hazardCurves = responseData.hazardCurves.filter(
      response => response.imt.value !== Imt.PGV.toString(),
    );

    const data = hazardCurves.map((response, index) => {
      const imt = response.imt.value as Imt;

      const sourceTypeData = hazardUtils.getSourceTypeData(
        response.data,
        form.sourceType,
      );
      const xy = hazardUtils.updateXySequence(
        form,
        hazardUtils.cleanXySequence(sourceTypeData.values),
        imt,
      );

      const plotlyData: Partial<PlotData> = {
        hovertemplate: '%{x} g, %{y} AFE',
        line: {
          color: hazardUtils.color(index, hazardCurves.length),
        },
        mode: 'lines+markers',
        name:
          imt === Imt.PGA || imt === Imt.PGV
            ? imt
            : `${imtToPeriod(imt)} s ${imt.substring(0, 2)}`,
        x: xy.xs,
        y: xy.ys,
      };
      return plotlyData;
    });

    const title = `Hazard Curves - ${form.sourceType}`;

    plot.settingsForm.patchValue({
      layout: {
        title: {
          text: title,
        },
      },
    });

    const metadata = responseData.metadata;
    const layout = plotUtils.updatePlotLabels({
      layout: plot.plotData.layout,
      title,
      xLabel: metadata.xlabel,
      yLabel: metadata.ylabel,
    });

    const mobileLayout = plotUtils.updatePlotLabels({
      layout: plot.plotData.mobileLayout,
      title,
      xLabel: metadata.xlabel,
      yLabel: metadata.ylabel,
    });

    return {
      config: {...plot.plotData.config},
      data: [
        this.returnPeriodPlotData(responseData, form.sourceType, returnPeriod),
        ...data,
      ],
      id: 'hazard-curves',
      layout,
      mobileConfig: {...plot.plotData.mobileConfig},
      mobileLayout,
    };
  }

  private createServiceEndpoint(
    serviceUrl: string,
    values: DynamicHazardControlForm,
  ): string {
    const {longitude, latitude, vs30} = values;
    return `${serviceUrl}/${longitude}/${latitude}/${vs30}`;
  }

  private initialFormSet(): void {
    const query = this.route.snapshot.queryParams as DynamicHazardQuery;
    const defaultValues = this.defaultFormValues();

    const formValues: DynamicHazardControlForm = {
      commonReturnPeriods: defaultValues.commonReturnPeriods,
      latitude:
        query.latitude !== undefined
          ? Number.parseFloat(query.latitude)
          : defaultValues.latitude,
      longitude:
        query.longitude !== undefined
          ? Number.parseFloat(query.longitude)
          : defaultValues.longitude,
      maxDirection:
        query.maxDirection !== undefined
          ? (JSON.parse(query.maxDirection) as boolean)
          : defaultValues.maxDirection,
      model: query.model !== undefined ? query.model : defaultValues.model,
      returnPeriod:
        query.returnPeriod !== undefined
          ? Number.parseInt(query.returnPeriod, 10)
          : defaultValues.returnPeriod,
      siteClass:
        query.siteClass !== undefined
          ? query.siteClass
          : defaultValues.siteClass,
      sourceType:
        query.sourceType !== undefined
          ? query.sourceType
          : defaultValues.sourceType,
      truncate:
        query.truncate !== undefined
          ? (JSON.parse(query.truncate) as boolean)
          : defaultValues.truncate,
      vs30:
        query.vs30 !== undefined
          ? Number.parseFloat(query.vs30)
          : defaultValues.vs30,
    };

    this.formGroup.patchValue(formValues);

    if (this.formGroup.valid) {
      this.nshmpService.selectPlotControl();
      this.callService();
    } else if (this.formGroup.getRawValue() !== defaultValues) {
      this.formGroup.markAsDirty();
    }

    this.formGroup.valueChanges.subscribe(() => this.updateUrl());
  }

  /**
   * Return the response spectrum plot data.
   *
   * @param spectra Response spectra
   * @param plot plot
   * @param returnPeriodValue Return period
   */
  private responseSpectrumPlotData(
    spectra: ResponseSpectra,
    plot: NshmpPlot,
    returnPeriodValue: number,
  ): PlotlyPlot {
    if (spectra === null || spectra === undefined) {
      return;
    }

    const imts = spectra.imts;
    const lines: Partial<PlotData>[] = [];

    spectra.responseSpectrum.forEach((spectrum, iSpectra) => {
      let lineWidth = 2;
      let markerSize = 7;

      const color = hazardUtils.color(
        iSpectra,
        spectra.responseSpectrum.length,
      );

      if (spectrum.returnPeriod === returnPeriodValue) {
        lineWidth = 4;
        markerSize = 12;
      }

      const spectraX: number[] = [];
      const spectraY: number[] = [];
      const scatters: Partial<PlotData>[] = [];

      for (let iSpectrum = 0; iSpectrum < spectrum.values.length; iSpectrum++) {
        const imt = imts[iSpectrum];

        if (imt === Imt.PGV) {
          continue;
        } else if (imt === Imt.PGA) {
          scatters.push({
            hovertemplate: '%{y} g',
            legendgroup: imt,
            marker: {
              color,
              size: markerSize,
              symbol: 'square',
            },
            mode: 'markers',
            name: imt,
            showlegend: false,
            x: [imtToPeriod(imt)],
            y: [spectrum.values[iSpectrum]],
          });
        } else {
          spectraX.push(imtToPeriod(imt));
          spectraY.push(spectrum.values[iSpectrum]);
        }
      }

      lines.push({
        hovertemplate: '%{y} g',
        line: {
          color,
          width: lineWidth,
        },
        marker: {
          size: markerSize,
        },
        mode: 'lines+markers',
        name:
          returnPeriodAltName[spectrum.returnPeriod] ??
          `${spectrum.returnPeriod} yr`,
        x: spectraX,
        y: spectraY,
      });
      lines.push(...scatters);
    });

    const legendEntries: Partial<PlotData>[] = [
      {
        legendgroup: Imt.PGA,
        marker: {
          color: 'white',
          line: {
            width: 1.5,
          },
          size: 7,
          symbol: 'square',
        },
        mode: 'markers',
        name: 'PGA',
        x: [-1],
        y: [-1],
      },
    ];

    lines.push(...legendEntries);

    return {
      config: {...plot.plotData.config},
      data: lines,
      id: 'response-spectrum',
      layout: {...plot.plotData.layout},
      mobileConfig: {...plot.plotData.mobileConfig},
      mobileLayout: {...plot.plotData.mobileLayout},
    };
  }

  /**
   * Transform the service response the plot data.
   *
   * @param state The current state
   */
  private responseToPlots(
    state: AppState,
    formGroup: FormGroupControls<DynamicHazardControlForm>,
  ): Map<string, NshmpPlot> {
    if (state.serviceResponse === null || state.serviceResponse === undefined) {
      return state.plots;
    }

    const returnPeriod = formGroup.controls.returnPeriod.value;
    const responseData = state.serviceResponse.response;
    const plots = new Map<string, NshmpPlot>();
    const hazardPlot = state.plots.get(HazardPlots.HAZARD);
    const spectrumPlot = state.plots.get(HazardPlots.SPECTRUM);

    const hazardCurves = this.createHazardPlotData(
      responseData,
      hazardPlot,
      formGroup.getRawValue(),
      returnPeriod,
    );

    const spectrum = this.responseSpectrumPlotData(
      state.responseSpectra,
      spectrumPlot,
      returnPeriod,
    );

    plots.set(HazardPlots.HAZARD, {
      ...hazardPlot,
      plotData: hazardCurves,
    });

    plots.set(HazardPlots.SPECTRUM, {
      ...spectrumPlot,
      plotData: spectrum,
    });

    return plots;
  }

  /**
   * Returns the plot data for the return period.
   *
   * @param responseData Hazard response data
   * @param sourceType Hazard source type
   * @param returnPeriod Retrun period
   */
  private returnPeriodPlotData(
    responseData: HazardResponseData,
    sourceType: string,
    returnPeriod: number,
  ): Partial<PlotData> {
    const data = responseData.hazardCurves
      .filter(hazard => hazard.imt.value !== Imt.PGV.toString())
      .map(
        hazard => hazardUtils.getSourceTypeData(hazard.data, sourceType).values,
      );

    const xMin = Math.min(...data.map(xy => Math.min(...xy.xs)));
    const xMax = Math.max(...data.map(xy => Math.max(...xy.xs)));

    return {
      hoverinfo: 'none',
      line: {
        color: 'black',
      },
      mode: 'lines',
      name: `${returnPeriod} yr.`,
      x: [xMin, xMax],
      y: [1 / returnPeriod, 1 / returnPeriod],
    };
  }

  private updateUrl(): void {
    this.location.replaceState(
      apps().hazard.dynamic.routerLink,
      new HttpParams().appendAll(this.formGroup.getRawValue()).toString(),
    );
  }

  private updateUsageUrl() {
    const nshmService = this.state().nshmServices.find(
      nshm => nshm.model === this.formGroup.getRawValue().model,
    );

    this.updateState({
      serviceCallInfo: {
        ...this.state().serviceCallInfo,
        usage: [nshmService.url],
      },
    });
  }
}

results matching ""

    No results matching ""