import {Injectable} from '@angular/core';
import {FormGroupControls} from '@ghsc/nshmp-lib-ng/nshmp';
import {NshmpPlot, plotUtils} from '@ghsc/nshmp-lib-ng/plot';
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 {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
import {PlotlyPlot} from '@ghsc/nshmp-utils-ts/libs/plotly';
import {Dash, PlotData} from 'plotly.js';
import {
AppState,
ControlForm,
Plots,
ServiceResponse,
ServiceResponses,
Spectra,
} from '../models/state.model';
import {MathService} from './math.service';
export interface SpectraDifference {
imts: Imt[];
percentDifference: number[];
}
/**
* Base options for creating spectra plots.
*/
interface BaseCreateSpectraPlotOptions {
/** Current avialable models */
availableModels: Parameter[];
/** Control panel form values */
form: ControlForm;
/** Function to return the name of the data */
name: (modelInfo: Parameter, spectra: Spectra) => string;
/** Function to filter the response spectra data */
spectraFilter: (spectra: Spectra) => boolean;
}
/**
* Options for creating spectra plot data.
*/
interface CreateSpectraPlotDataOptions extends BaseCreateSpectraPlotOptions {
/** The service response for a model */
serviceResponse: ServiceResponse;
/** The line colors to use for the spectra data */
colors?: string[];
/** The dash type */
dash?: Dash;
/** The PGA marker symbol */
pgaMarker?: string;
}
/**
* Options to create a spectra plot.
*/
interface CreateSpectraPlotOptions extends BaseCreateSpectraPlotOptions {
/** Plot id */
id: Plots;
/** Current plot */
plot: NshmpPlot;
/** The service resoponses */
serviceResponses: ServiceResponses;
/** The line colors to use for each model */
colors?: PlotColors;
/** Dash types per model */
dash?: PlotDash;
/** The PGA marker symbol per model */
pgaMarker?: PgaMarker;
}
/**
* Dash type for models
*/
interface PlotDash {
/** Dash type for model A */
modelA?: Dash;
/** Dash type for model B */
modelB?: Dash;
}
/**
* PGA marker symbol per model.
*/
interface PgaMarker {
/** Model A PGA symbol */
modelA?: string;
/** Model B PGA symbol */
modelB?: string;
}
/**
* Line plot colors per model.
*/
interface PlotColors {
/** Model A line colors */
modelA?: string[];
/** Model B line colors */
modelB?: string[];
}
@Injectable({
providedIn: 'root',
})
export class SpectraPlotsService {
constructor(private mathService: MathService) {}
/**
* Create the spectra plots:
* - Response spectra for total source type
* - Percent difference for total source type
* - Response spectra components
*
* @param state The app state
*/
createSpectraPlots(
state: AppState,
formGroup: FormGroupControls<ControlForm>,
): Map<string, NshmpPlot> {
const plots = new Map<string, NshmpPlot>();
const spectraPlot = state.plots.get(Plots.SPECTRUM);
const spectraComponentsPlot = state.plots.get(Plots.SPECTRUM_COMPONENTS);
const spectraDiffPlot = state.plots.get(Plots.SPECTRUM_DIFFERENCES);
const spectraCurves = this.createSpectraCurvesPlot(
state.serviceResponses,
state.availableModels,
formGroup.getRawValue(),
spectraPlot,
);
plots.set(Plots.SPECTRUM, {
...spectraPlot,
plotData: spectraCurves,
});
const spectraComponents = this.createSpectraComponentsPlot(
state.serviceResponses,
state.availableModels,
formGroup.getRawValue(),
spectraPlot,
);
plots.set(Plots.SPECTRUM_COMPONENTS, {
...spectraComponentsPlot,
plotData: spectraComponents,
});
const spectraDiff = this.createSpectraDiffPlot(
state.serviceResponses,
state.availableModels,
formGroup.getRawValue(),
spectraDiffPlot,
);
plots.set(Plots.SPECTRUM_DIFFERENCES, {
...spectraDiffPlot,
plotData: spectraDiff,
});
return plots;
}
spectraPercentDifference(
serviceResponses: ServiceResponses,
returnPeriod: number,
): SpectraDifference {
const {modelA, modelB} = serviceResponses;
const spectraA = modelA.spectra.find(
spectra => spectra.sourceType === SourceType.TOTAL,
);
const spectraB = modelB.spectra.find(
spectra => spectra.sourceType === SourceType.TOTAL,
);
const xValues = spectraA.responseSpectra.imts.filter(imt =>
spectraB.responseSpectra.imts.includes(imt),
);
const returnPeriodSpectraA = spectraA.responseSpectra.responseSpectrum.find(
spectra => spectra.returnPeriod === returnPeriod,
);
const returnPeriodSpectraB = spectraB.responseSpectra.responseSpectrum.find(
spectra => spectra.returnPeriod === returnPeriod,
);
const yValuesA = spectraA.responseSpectra.imts
.filter(imt => xValues.includes(imt))
.map((_, i) => returnPeriodSpectraA.values[i]);
const yValuesB = spectraB.responseSpectra.imts
.filter(imt => xValues.includes(imt))
.map((_, i) => returnPeriodSpectraB.values[i]);
const yValues = this.mathService.percentDifferences(yValuesA, yValuesB);
return {
imts: xValues,
percentDifference: yValues,
};
}
/**
* Create the response spectra curves plot.
*
* @param serviceResponses The service reponses
* @param availableModels The avaialable models
* @param form The control panel form values
* @param plot The current spectra curves plot
*/
private createSpectraCurvesPlot(
serviceResponses: ServiceResponses,
availableModels: Parameter[],
form: ControlForm,
plot: NshmpPlot,
): PlotlyPlot {
const spectraFilter = (spectra: Spectra) =>
spectra.sourceType === SourceType.TOTAL;
const name = (modelInfo: Parameter) => modelInfo.display;
const colors = plotUtils.COLORWAY;
return this.createSpectraPlot({
availableModels,
colors: {
modelA: [colors[0]],
modelB: [colors[1]],
},
form,
id: Plots.SPECTRUM,
name,
plot,
serviceResponses,
spectraFilter,
});
}
/**
* Create the response spectra components plot.
*
* @param serviceResponses The service reponses
* @param availableModels The avaialable models
* @param form The control panel form values
* @param plot The current spectra curves plot
*/
private createSpectraComponentsPlot(
serviceResponses: ServiceResponses,
availableModels: Parameter[],
form: ControlForm,
plot: NshmpPlot,
): PlotlyPlot {
const spectraFilter = (spectra: Spectra) =>
spectra.sourceType !== SourceType.TOTAL;
const name = (modelInfo: Parameter, spectra: Spectra) =>
`${modelInfo.display} - ${sourceTypeToCapitalCase(spectra.sourceType)}`;
return this.createSpectraPlot({
availableModels,
dash: {
modelA: 'solid',
modelB: 'dash',
},
form,
id: Plots.SPECTRUM_COMPONENTS,
name,
pgaMarker: {
modelA: 'square',
modelB: 'diamond',
},
plot,
serviceResponses,
spectraFilter,
});
}
/**
* Create the spectra difference plot.
*
* @param serviceResponses The service responses
* @param availableModels The available models
* @param form The control form values
* @param plot The current spectra diffecence plot
*/
private createSpectraDiffPlot(
serviceResponses: ServiceResponses,
availableModels: Parameter[],
form: ControlForm,
plot: NshmpPlot,
): PlotlyPlot {
const data = this.createSpectraDiffPlotData(
serviceResponses,
availableModels,
form,
);
const yValues = data
.map(d => d.y as number[])
.reduce((previous, current) => [...previous, ...current]);
const yMax = Math.max(...yValues.map(y => Math.abs(y))) * 1.3;
const yRange = [-yMax, yMax];
const {layout, mobileLayout} = plot.plotData;
return {
config: {...plot.plotData.config},
data,
id: Plots.SPECTRUM_DIFFERENCES,
layout: {
...layout,
yaxis: {
...layout.yaxis,
autorange: false,
range: yRange,
},
},
mobileConfig: {...plot.plotData.mobileConfig},
mobileLayout: {
...mobileLayout,
yaxis: {
...mobileLayout.yaxis,
autorange: false,
range: yRange,
},
},
};
}
/**
* Create the spectra difference plot data.
*
* @param serviceResponses The service responses
* @param availableModels The available models
* @param form The control form values
*/
private createSpectraDiffPlotData(
serviceResponses: ServiceResponses,
availableModels: Parameter[],
form: ControlForm,
): Partial<PlotData>[] {
const spectraA = serviceResponses.modelA.spectra.find(
spectra => spectra.sourceType === SourceType.TOTAL,
);
const {imts, percentDifference} = this.spectraPercentDifference(
serviceResponses,
form.returnPeriod,
);
const plotData = this.createSpectraPlotData({
availableModels,
form,
name: () => '',
serviceResponse: {
...serviceResponses.modelA,
spectra: [
{
responseSpectra: {
imts,
responseSpectrum: [
{
returnPeriod: form.returnPeriod,
values: percentDifference,
},
],
siteClass: spectraA.responseSpectra.siteClass,
},
sourceType: SourceType.TOTAL,
},
],
},
spectraFilter: (spectra: Spectra) =>
spectra.sourceType === SourceType.TOTAL,
});
plotData.forEach(data => (data.showlegend = false));
plotData.forEach(
data => (data.hovertemplate = '%{x} s, %{y} % difference'),
);
return plotData;
}
/**
* Create the `PlotData` for a spectra plot.
*
* Creates plot data for response spectra for current return period.
*
* @param options The plot data options
*/
private createSpectraPlotData(
options: CreateSpectraPlotDataOptions,
): Partial<PlotData>[] {
const modelInfo = this.mathService.getModelInfo(
options.serviceResponse.model,
options.availableModels,
);
const spectra = options.serviceResponse.spectra.filter(spectra =>
options.spectraFilter(spectra),
);
const lines: Partial<PlotData>[] = [];
let count = 0;
const lineColors =
options.colors === undefined ? plotUtils.COLORWAY : options.colors;
spectra.map(spectrum => {
const color =
lineColors[count++ % Math.min(spectra.length, lineColors.length)];
const returnPeriodSpectra =
spectrum.responseSpectra.responseSpectrum.find(
s => s.returnPeriod === options.form.returnPeriod,
);
const imts = spectrum.responseSpectra.imts;
const spectraX: number[] = [];
const spectraY: number[] = [];
const scatters: Partial<PlotData>[] = [];
const name = options.name(modelInfo, spectrum);
for (
let iSpectrum = 0;
iSpectrum < returnPeriodSpectra.values.length;
iSpectrum++
) {
const imt = imts[iSpectrum];
if (imt === Imt.PGV) {
continue;
} else if (imt === Imt.PGA) {
scatters.push({
hovertemplate: `${imt}, %{y} g`,
legendgroup: imt,
marker: {
color,
size: 7,
symbol: options.pgaMarker ?? 'square',
},
mode: 'markers',
name,
showlegend: false,
x: [imtToPeriod(imt)],
y: [returnPeriodSpectra.values[iSpectrum]],
});
} else {
spectraX.push(imtToPeriod(imt));
spectraY.push(returnPeriodSpectra.values[iSpectrum]);
}
}
lines.push({
hovertemplate: '%{x} s, %{y} g',
line: {
color,
dash: options.dash,
},
mode: 'lines+markers',
name,
x: spectraX,
y: spectraY,
});
lines.push(...scatters);
});
return lines;
}
/**
* Create a response spectra plot with given options.
*
* @param options The create plot options
*/
private createSpectraPlot(options: CreateSpectraPlotOptions): PlotlyPlot {
const {modelA, modelB} = options.serviceResponses;
const lines: Partial<PlotData>[] = [];
lines.push(
...this.createSpectraPlotData({
...options,
colors: options.colors?.modelA,
dash: options?.dash?.modelA,
pgaMarker: options?.pgaMarker?.modelA,
serviceResponse: modelA,
}),
);
lines.push(
...this.createSpectraPlotData({
...options,
colors: options.colors?.modelB,
dash: options?.dash?.modelB,
pgaMarker: options?.pgaMarker?.modelB,
serviceResponse: modelB,
}),
);
if (options?.pgaMarker?.modelA !== options?.pgaMarker?.modelB) {
const modelAInfo = this.mathService.getModelInfo(
options.serviceResponses.modelA.model,
options.availableModels,
);
const modelBInfo = this.mathService.getModelInfo(
options.serviceResponses.modelB.model,
options.availableModels,
);
lines.push(
this.spectraPgaLegendEntry(
modelAInfo.display,
options.pgaMarker.modelA,
),
);
lines.push(
this.spectraPgaLegendEntry(
modelBInfo.display,
options.pgaMarker.modelB,
),
);
} else {
lines.push(this.spectraPgaLegendEntry('PGA'));
}
return {
config: {...options.plot.plotData.config},
data: lines,
id: options.id,
layout: {...options.plot.plotData.layout},
mobileConfig: {...options.plot.plotData.mobileConfig},
mobileLayout: {...options.plot.plotData.mobileLayout},
};
}
/**
* Create the spectra PGA legend entry.
*
* @param name The legend entry name
* @param pgaMarker The PGA marker symbol
*/
private spectraPgaLegendEntry(name: string, pgaMarker = 'square') {
const legendEntry: Partial<PlotData> = {
legendgroup: Imt.PGA,
marker: {
color: 'white',
line: {
width: 1.5,
},
size: 7,
symbol: pgaMarker,
},
mode: 'markers',
name,
x: [-1],
y: [-1],
};
return legendEntry;
}
}