services/app.service.ts
Logic tree data
Properties |
labels |
labels:
|
Type : string[]
|
Logic tree labels |
parents |
parents:
|
Type : string[]
|
Logic tree parents |
text |
text:
|
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],
},
});
}
}