File

services/app.service.ts

Description

Logic tree data

Index

Properties

Properties

labels
labels: string[]
Type : string[]

Logic tree labels

parents
parents: string[]
Type : string[]

Logic tree parents

text
text: string[]
Type : string[]

Logic tree text

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 {HazardService, hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
import {
  FormGroupControls,
  NshmpService,
  ServiceCallInfo,
  SpinnerService,
} from '@ghsc/nshmp-lib-ng/nshmp';
import {
  NshmpPlot,
  NshmpPlotSettingFormGroup,
  PlotOptions,
  plotUtils,
} from '@ghsc/nshmp-lib-ng/plot';
import {
  SourceLogicTreesMetadata,
  SourceLogicTreesResponse,
  SourceLogicTreesUsage,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/source-logic-trees-service';
import {Sequences} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/data';
import {
  MfdBranch,
  SettingGroup,
  SourceBranch,
  SourceGroup,
  SourceType,
  TectonicSettings,
  TreeInfo,
} 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 {ControlForm, MfdSource, MfdType} from '../models/control-form.model';
import {Plots} from '../models/plots.model';
import {AppState} from '../models/state.model';

/**
 * Logic tree data
 */
interface LogicTreeData {
  /** Logic tree labels */
  labels: string[];
  /** Logic tree parents */
  parents: string[];
  /** Logic tree text */
  text: string[];
}

/**
 * URL query parameters.
 */
export interface MfdQuery {
  /** Cumulative series */
  cumulative: string;
  /** NSHm */
  model: NshmId;
  /** Tectonic settings */
  setting: TectonicSettings;
  /** Source tree id number */
  sourceTree: string;
  /** Source type */
  sourceType: SourceType;
  /** Whether to apply weight */
  weightedMfds: string;
}

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

  readonly formGroup = this.formBuilder.group<ControlForm>(
    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.formGroup.controls.mfdType.disable();
    this.addValidators();
    this.formGroup.controls.source.setValue(this.defaultFormValues().source);
  }

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

  /**
   * Returns the logic tree plot data observable.
   */
  get logicTreePlotData(): Signal<PlotlyPlot> {
    return computed(() => this.state().plots.get(Plots.LOGIC_TREE).plotData);
  }

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

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

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

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

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

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

  /**
   * Returns the MFD usage response observable.
   */
  get usage(): Signal<SourceLogicTreesUsage> {
    return computed(() =>
      this.state().usageResponses?.get(this.formGroup.getRawValue().model),
    );
  }

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

    this.addRequiredValidator(controls.model);
    this.addRequiredValidator(controls.source);
    this.addRequiredValidator(controls.sourceTree);
  }

  /**
   * Call MFD services.
   */
  callService(): void {
    const spinnerRef = this.spinnerService.show(SpinnerService.MESSAGE_SERVICE);
    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$<SourceLogicTreesResponse>(url)
      .pipe(
        catchError((error: Error) => {
          spinnerRef.close();
          return this.nshmpService.throwError$(error);
        }),
      )
      .subscribe(serviceResponse => {
        spinnerRef.close();

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

        this.createPlots();
      });
  }

  createPlots(): void {
    if (this.state().serviceResponse === null) {
      return;
    }

    if (this.formGroup.getRawValue().mfdType === null) {
      this.formGroup.controls.mfdType.setValue(MfdType.ALL);
    }

    if (this.formGroup.controls.mfdType.disabled) {
      this.formGroup.controls.mfdType.enable();
    }

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

  /**
   * Returns the default control panel form field values.
   */
  defaultFormValues(): ControlForm {
    const source: MfdSource = {
      sourceType: SourceType.FAULT,
      tectonicSettings: TectonicSettings.ACTIVE_CRUST,
    };

    return {
      cumulative: false,
      mfdType: null,
      model: NshmId.CONUS_2018,
      source,
      sourceAsString: JSON.stringify(source),
      sourceTree: null,
      weightedMfds: false,
    };
  }

  /**
   * Returns the default plots.
   */
  defaultPlots(): Map<string, NshmpPlot> {
    const plots = new Map<string, NshmpPlot>();

    const logicTreeOptions: PlotOptions = {
      layout: {
        aspectRatio: undefined,
        margin: {
          b: 10,
          l: 10,
          r: 10,
        },
      },
    };

    const plotOptions: PlotOptions = {
      layout: {
        xaxis: {
          nticks: 10,
          range: [6.5, 7.7],
          type: 'linear',
        },
        yaxis: {
          type: 'log',
        },
      },
    };

    /** Default logic tree plot data */
    const logicTreePlotData: PlotlyPlot = {
      ...plotUtils.defaultPlot({
        id: 'mfd-logic-tree',
        mobileOptions: {...logicTreeOptions},
        options: {...logicTreeOptions},
        title: 'MFD Logic Tree',
        xLabel: '',
        yLabel: '',
      }),
      data: [
        {
          labels: ['Logic Tree Branches'],
          parents: [''],
          type: 'treemap',
        },
      ],
    };

    /** Default MFD plot data */
    const mfdPlotData = plotUtils.defaultPlot({
      id: 'mfd-plot',
      mobileOptions: {
        ...plotOptions,
        layout: {
          ...plotOptions.layout,
          aspectRatio: '1:1',
        },
      },
      options: {
        ...plotOptions,
        layout: {
          ...plotOptions.layout,
          legend: {
            font: {
              size: 11,
            },
          },
        },
      },
      title: 'Magnitude Frequency Distribution',
      xLabel: 'Magnitude',
      yLabel: 'Rate (yr<sup>-1</sup>)',
    });

    plots.set(Plots.MFD, {
      label: 'MFD',
      plotData: mfdPlotData,
      settingsForm: plotUtils.plotSettingsToFormGroup({
        config: mfdPlotData.config,
        layout: plotUtils.plotlyLayoutToSettings(mfdPlotData.layout),
      }),
    });

    plots.set(Plots.LOGIC_TREE, {
      label: Plots.LOGIC_TREE,
      plotData: logicTreePlotData,
      settingsForm: plotUtils.plotSettingsToFormGroup({
        config: logicTreePlotData.config,
        layout: plotUtils.plotlyLayoutToSettings(logicTreePlotData.layout),
      }),
    });

    return plots;
  }

  /**
   * Return the feault source to use from settings groups.
   *
   * @param trees Settings groups
   */
  defaultSource(trees: SettingGroup[]): MfdSource {
    const defaultTree = [
      ...trees.sort((a, b) => a.setting.localeCompare(b.setting)),
    ].shift();

    return {
      sourceType: [
        ...defaultTree.data.sort((a, b) => a.type.localeCompare(b.type)),
      ].shift().type,
      tectonicSettings: defaultTree.setting,
    };
  }

  /**
   * Returns the default source tree to use from the settings group and MFD source.
   *
   * @param trees The settings groups
   * @param source MFD source
   */
  defaultSourceTree(trees: SettingGroup[], source: MfdSource): TreeInfo {
    const settingsTree = [...trees].find(
      tree => tree.setting === source.tectonicSettings,
    );

    const tree = settingsTree.data.find(
      data => data.type === source.sourceType,
    );

    return [...tree.data.sort((a, b) => a.name.localeCompare(b.name))].shift();
  }

  /**
   * Filter MFD based on current {@link MfdType}.
   *
   * @param mfds The MFD branches
   * @param type The MFD type
   */
  filterMfds(mfds: MfdBranch[], type: MfdType): MfdBranch[] {
    return mfds
      .filter(mfdInfo => {
        if (type === MfdType.ALL) {
          return mfdInfo;
        } else if (mfdInfo.mfd.props.type === type) {
          return mfdInfo;
        }
      })
      .sort((a, b) => a.mfd.props.type.localeCompare(b.mfd.props.type));
  }

  /**
   * Retrurn the matching tree from MFD source and source tree id.
   *
   * @param trees The settings groups
   * @param source The MFD source
   * @param id The source tree id number
   */
  findTreeInfo(trees: SettingGroup[], source: MfdSource, id: number): TreeInfo {
    const sourceGroup = this.findSourceGroup(trees, source);
    return sourceGroup.data.find(data => data.id === id);
  }

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

    this.hazardService
      .dynamicNshms$<SourceLogicTreesMetadata>(
        `${this.nshmpHazWs.url}${this.nshmpHazWs.services.nshms}`,
        this.usageEndpoint,
      )
      .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();
      });
  }

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

    return {
      availableModels: [],
      nshmServices: [],
      plots: this.defaultPlots(),
      serviceCallInfo: {
        serviceCalls: [],
        serviceName: 'Hazard Source Trees',
        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,
    });

    if (this.formGroup.controls.mfdType.enabled) {
      this.formGroup.controls.mfdType.disable();
    }

    if (this.formGroup.getRawValue().mfdType !== null) {
      this.formGroup.controls.mfdType.setValue(null);
    }

    this.updateUsageUrl();
  }

  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));
  }

  private createServiceEndpoint(
    serviceUrl: string,
    values: ControlForm,
  ): string {
    const {sourceTree} = values;
    return `${serviceUrl}/${sourceTree}`;
  }

  /**
   * Return the matching setting group from a {@link MfdSource#tectonicSettings}.
   *
   * @param trees The settings groups
   * @param source The MFD source
   */
  private findSettingsGroup(
    trees: SettingGroup[],
    source: MfdSource,
  ): SettingGroup {
    return trees.find(tree => tree.setting === source.tectonicSettings);
  }

  /**
   * Return the matching setting group from a {@link MfdSource#tectonicSettings}
   * and {@link MfdSource#sourceType}.
   *
   * @param trees The setting groups
   * @param source The MFD source
   */
  private findSourceGroup(
    trees: SettingGroup[],
    source: MfdSource,
  ): SourceGroup {
    const settingsGroup = this.findSettingsGroup(trees, source);
    return settingsGroup.data.find(data => data.type === source.sourceType);
  }

  private initialFormSet(): void {
    this.formGroup.valueChanges.subscribe(() => this.updateUrl());

    const defaultValues = this.defaultFormValues();
    const queryParams = this.route.snapshot.queryParams as MfdQuery;

    const model = queryParams.model ? queryParams.model : defaultValues.model;
    const usage = this.state().usageResponses.get(model);
    const defaultSource = this.defaultSource(usage.response.trees);
    const defaultTreeInfo = this.defaultSourceTree(
      usage.response.trees,
      defaultSource,
    );

    const source: MfdSource =
      queryParams.setting && queryParams.sourceType
        ? {
            sourceType: queryParams.sourceType,
            tectonicSettings: queryParams.setting,
          }
        : defaultSource;
    const sourceTree = queryParams.sourceTree
      ? Number.parseInt(queryParams.sourceTree)
      : null;

    const formValues: ControlForm = {
      cumulative: queryParams?.cumulative === 'true',
      mfdType: defaultValues.mfdType,
      model,
      source,
      sourceAsString: JSON.stringify(source),
      sourceTree,
      weightedMfds: queryParams?.weightedMfds === 'true',
    };

    this.formGroup.patchValue(formValues);

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

    if (formValues.sourceTree === null) {
      this.formGroup.controls.sourceTree.setValue(defaultTreeInfo.id);
    }
  }

  /**
   * Add logic tree data by MFD branch.
   *
   * @param logicTreeData Logic tree data
   * @param branch Source branch
   * @param parent Logic tree parent
   * @param type MFD type
   */
  private logicTreeByMfdBranch(
    logicTreeData: LogicTreeData,
    branch: SourceBranch,
    parent: string,
    type: MfdType,
  ): void {
    branch.mfds
      .filter(mfdBranch => mfdBranch.mfd.props.type === type)
      .forEach(mfdBranch => {
        logicTreeData.labels.push(
          `Branch: ${branch.branch}<br>` + `ID: ${mfdBranch.id}<br>`,
        );

        logicTreeData.parents.push(parent);

        const props = Object.entries(mfdBranch.mfd.props)
          .map(([key, value]: [string, string]) => `<br>  ${key}: ${value}`)
          .join('');

        logicTreeData.text.push(
          `Weight: ${mfdBranch.weight}<br><br>` + 'Properties:' + props,
        );
      });
  }

  /**
   * Add logic tree data by type.
   *
   * @param logicTreeData Logic tree data
   * @param branch Source branch
   * @param parent Logic tree parent
   */
  private logicTreeByType(
    logicTreeData: LogicTreeData,
    branch: SourceBranch,
    parent: string,
  ): void {
    const types = new Set(
      branch.mfds.map(mfdBranch => mfdBranch.mfd.props.type),
    );

    types.forEach(type => {
      const typeLabel = [`Branch: ${branch.branch} <br>`, `Type: ${type}`].join(
        '',
      );

      logicTreeData.labels.push(typeLabel);
      logicTreeData.parents.push(parent);
      logicTreeData.text.push('');

      this.logicTreeByMfdBranch(logicTreeData, branch, typeLabel, type);
    });
  }

  /**
   * Create the logic tree data from service response.
   *
   * @param serviceResponse MFD service response
   */
  private logicTreeData(
    serviceResponse: SourceLogicTreesResponse,
  ): LogicTreeData {
    const tree = serviceResponse.response;
    const root = tree.name;
    const logicTreeData: LogicTreeData = {
      labels: [root],
      parents: [''],
      text: [''],
    };

    tree.branches.forEach(branch => {
      const branchLabel = [
        `Branch: ${branch.branch}<br>`,
        `Path: ${branch.path}<br>`,
        `Name: ${branch.name}<br>`,
        `Weight: ${branch.weight}`,
      ].join('');

      logicTreeData.labels.push(branchLabel);
      logicTreeData.parents.push(root);
      logicTreeData.text.push('');

      this.logicTreeByType(logicTreeData, branch, branchLabel);
    });

    return logicTreeData;
  }

  /**
   * Create the application plots from the MFD service response.
   *
   * @param state application state
   */
  private responseToPlots(
    state: AppState,
    formGroup: FormGroupControls<ControlForm>,
  ): Map<string, NshmpPlot> {
    if (state.serviceResponse === null || state.serviceResponse === undefined) {
      return state.plots;
    }

    const plots = new Map<string, NshmpPlot>();

    const logicTreePlot = state.plots.get(Plots.LOGIC_TREE);
    const logicTreePlotData = this.setLogicTreePlotData(
      state.serviceResponse,
      logicTreePlot,
    );
    plots.set(Plots.LOGIC_TREE, {
      ...logicTreePlot,
      plotData: logicTreePlotData,
    });

    const mfdPlot = state.plots.get(Plots.MFD);
    const mfdPlotData = this.setMfdPlotData(
      state.serviceResponse,
      mfdPlot,
      formGroup.getRawValue(),
    );
    plots.set(Plots.MFD, {
      ...mfdPlot,
      plotData: mfdPlotData,
    });

    return plots;
  }

  /**
   * Returns the logic tree plot data.
   *
   * @param serviceResponse The MFD service response
   * @param settings The plot settings
   */
  private setLogicTreePlotData(
    serviceResponse: SourceLogicTreesResponse,
    plot: NshmpPlot,
  ): PlotlyPlot {
    const {labels, parents, text} = this.logicTreeData(serviceResponse);

    const logicTreePlotData: Partial<PlotData> = {
      labels,
      parents,
      text,
      type: 'treemap',
    };

    return {
      config: {...plot.plotData.config},
      data: [logicTreePlotData],
      id: 'logic-tree-plot',
      layout: {...plot.plotData.layout},
      mobileConfig: {...plot.plotData.mobileConfig},
      mobileLayout: {...plot.plotData.mobileLayout},
    };
  }

  /**
   * Returns the MFD plot data.
   *
   * @param serviceResponse The MFD service response
   * @param form Control form field values
   * @param settings Plot settings
   */
  private setMfdPlotData(
    serviceResponse: SourceLogicTreesResponse,
    plot: NshmpPlot,
    form: ControlForm,
  ): PlotlyPlot {
    const tree = serviceResponse.response;

    const totalMfd = form.cumulative
      ? Sequences.toCumulative(tree.totalMfd)
      : tree.totalMfd;

    const totalMfdData: Partial<PlotData> = {
      line: {
        color: 'black',
      },
      mode: 'lines+markers',
      name: 'Total MFD',
      x: totalMfd.xs,
      y: totalMfd.ys,
    };

    const data = tree.branches
      .map(branch => {
        return this.filterMfds(branch.mfds, form.mfdType).map(mfdInfo => {
          const name = `${branch.path} - ${mfdInfo.id}`;
          let xySequence = hazardUtils.cleanXySequence(mfdInfo.mfd.data);

          if (form.cumulative) {
            xySequence = Sequences.toCumulative(xySequence);
          }

          let plotData: Partial<PlotData> = {
            name: name.length > 50 ? `${name.substring(0, 50)} ...` : name,
            x: xySequence.xs,
            y: form.weightedMfds
              ? xySequence.ys.map(y => y * branch.weight * mfdInfo.weight)
              : xySequence.ys,
          };

          if (mfdInfo.mfd.props.type === MfdType.SINGLE) {
            // points for SINGLE type
            plotData = {
              ...plotData,
              marker: {
                symbol: 'square',
              },
              mode: 'markers',
            };
          } else {
            // line plot for all other type (all based on GR)
            plotData = {
              ...plotData,
              mode: 'lines+markers',
            };
          }

          return plotData;
        });
      })
      .reduce((prev, curr) => [...prev, ...curr]);

    const title = `Magnitude Frequency Distribution: ${serviceResponse.response.name}`;

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

    const layout = plotUtils.updatePlotLabels({
      layout: plot.plotData.layout,
      title,
      xLabel: plot.settingsForm.value.layout.xaxis.title.text,
      yLabel: plot.settingsForm.value.layout.yaxis.title.text,
    });

    const mobileLayout = plotUtils.updatePlotLabels({
      layout: plot.plotData.mobileLayout,
      title,
      xLabel: plot.settingsForm.value.layout.xaxis.title.text,
      yLabel: plot.settingsForm.value.layout.yaxis.title.text,
    });

    return {
      config: {...plot.plotData.config},
      data:
        form.mfdType === MfdType.ALL || form.mfdType === MfdType.TOTAL
          ? [totalMfdData, ...data]
          : data,
      id: 'mfd-plot',
      layout,
      mobileConfig: {...plot.plotData.mobileConfig},
      mobileLayout,
    };
  }

  private updateUrl(): void {
    const values = this.formGroup.getRawValue();

    const source = values.source;

    const queryParams: MfdQuery = {
      cumulative: values.cumulative.toString(),
      model: values.model,
      setting: source.tectonicSettings ?? null,
      sourceTree: values.sourceTree?.toString() ?? null,
      sourceType: source.sourceType ?? null,
      weightedMfds: values.weightedMfds.toString(),
    };

    this.location.replaceState(
      apps().source.mfd.routerLink,
      new HttpParams().appendAll({...queryParams}).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 ""