services/app.service.ts
ArcGIS hazard service response.
Properties |
services |
services:
|
Type : ArcService[]
|
Hazard maps |
import {Location as LocationService} from '@angular/common';
import {HttpClient, HttpParams} from '@angular/common/http';
import {computed, DestroyRef, Inject, Injectable, LOCALE_ID, Signal, signal} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {AbstractControl, FormBuilder, FormControl, Validators} from '@angular/forms';
import {ActivatedRoute} from '@angular/router';
import Point from '@arcgis/core/geometry/Point';
import GeoJSONLayer from '@arcgis/core/layers/GeoJSONLayer';
import Layer from '@arcgis/core/layers/Layer';
import TileLayer from '@arcgis/core/layers/TileLayer';
import ArcGisMap from '@arcgis/core/Map';
import SimpleRenderer from '@arcgis/core/renderers/SimpleRenderer';
import SizeVariable from '@arcgis/core/renderers/visualVariables/SizeVariable';
import SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol.js';
import SimpleLineSymbol from '@arcgis/core/symbols/SimpleLineSymbol.js';
import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol';
import {FormatLatitudePipe, FormatLongitudePipe, HazardService} from '@ghsc/nshmp-lib-ng/hazard';
import {ArcgisService} from '@ghsc/nshmp-lib-ng/map';
import {NshmpService, nshmpUtils, ReturnPeriod} from '@ghsc/nshmp-lib-ng/nshmp';
import {SpinnerService} from '@ghsc/nshmp-template';
import {
FeaturesUsageResponse,
FeatureType,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/features-service';
import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service';
import {
SourceLogicTreesMetadata,
SourceLogicTreesUsage,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/source-logic-trees-service';
import {Imt} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
import {NshmId, nshmRegion, nshmYear} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
import deepEqual from 'deep-equal';
import {environment} from 'projects/nshmp-apps/src/environments/environment';
import {AppServiceModel} from 'projects/nshmp-apps/src/shared/models/app-service.model';
import {apps} from 'projects/nshmp-apps/src/shared/utils/applications.utils';
import {catchError, forkJoin, from, map, mergeMap, Observable, of} from 'rxjs';
import {ControlForm, LatestEarthquakeTime} from '../models/control-form.model';
import {EarthquakeFeatureProperties} from '../models/earthquake-props.model';
import {HazardTile} from '../models/hazard-tile.model';
import {LayerId, Layers} from '../models/layers.model';
import {AppState} from '../models/state.model';
/**
* ArcGIS hazard service info.
*/
interface ArcService {
/** Name hazard map */
name: string;
/** Map type */
type: string;
}
/**
* ArcGIS hazard service response.
*/
interface ArcServiceResponse {
/** Hazard maps */
services: ArcService[];
}
interface Query {
/** Whether devollement toggle is on */
hasDecollementLayer: string;
/** Whether latest earthquake toggle is on */
hasEarthquakesLayer: string;
/** Whether fault sections toggle is on */
hasFaultSectionLayer: string;
/** Whether zone source toggle is on */
hasHazardTiles: string;
/** Whether interface sections toggle is on */
hasInterfaceSectionsLayer: string;
/** Whether NSHM boundary toggle is on*/
hasNshmBoundaryLayer: string;
/** Whether test sites toggle is on */
hasTestSitesLayer: string;
/** Whether zone sources is toggled on */
hasZoneSourcesLayer: string;
/** IMT for hazard map tile */
hazardTileImt: string;
/** Return period for hazard map tile */
hazardTileReturnPeriod: ReturnPeriod;
/** Year for hazard map tile */
hazardTileYear: string;
/** Latest earthquake feed time frame */
latestEarthquakeTime: LatestEarthquakeTime;
/** The NSHM */
model: NshmId;
/** ArcGIS overlay opactiy */
overlayOpacity: string;
}
const FAULT_POLY_ID = `${FeatureType.FAULT}_POLY`;
/**
* Entrypoint to store for data mapping application.
*/
@Injectable({
providedIn: 'root',
})
export class AppService implements AppServiceModel<AppState, ControlForm> {
readonly formGroup = this.formBuilder.group<ControlForm>(this.defaultFormValues());
/** Application state */
readonly state = signal<AppState>(this.initialState());
map2d = new ArcGisMap();
map3d = new ArcGisMap();
private minMw = 2.5;
/** nshmp-haz-ws web config */
private nshmpHazWs = environment.webServices.nshmpHazWs;
/** nshmp-haz web services */
private services = this.nshmpHazWs.services.curveServices;
/** Endpoint to NSHMs */
private nshmsEndpoint = this.nshmpHazWs.services.nshms;
constructor(
private formBuilder: FormBuilder,
private spinnerService: SpinnerService,
private nshmpService: NshmpService,
private hazardService: HazardService,
private http: HttpClient,
private route: ActivatedRoute,
private location: LocationService,
private arcgisService: ArcgisService,
private destroyRef: DestroyRef,
@Inject(LOCALE_ID) private localId: string,
) {
this.addValidators();
}
/**
* Returns the avialable models, as `Parameter`s, observable.
*/
get availableModels(): Signal<Parameter[]> {
return computed(() => this.state().availableModels);
}
/**
* Returns the hazard tiles.
*/
get hazardTiles(): Signal<HazardTile[]> {
return computed(() => this.state().hazardTiles);
}
/**
* Returns the layers observable.
*/
get layers(): Signal<Layers> {
return computed(() => this.state().layers);
}
get nshmService(): Signal<NshmMetadata> {
return computed(() =>
this.state().nshmServices.find(nshm => nshm.model === this.formGroup.getRawValue().model),
);
}
get sourceFeaturesUsage(): Signal<FeaturesUsageResponse> {
return computed(() =>
this.state().sourceFeaturesUsages.get(this.formGroup.getRawValue().model),
);
}
/**
* Returns the trees usage.
*/
get treesUsageResponse(): Signal<SourceLogicTreesUsage> {
return computed(() =>
this.state().treesUsageResponses?.get(this.formGroup.getRawValue().model),
);
}
addValidators(): void {
this.formGroup.controls.model.addValidators(control => Validators.required(control));
}
/**
* Call latest earthquakes service.
*/
callEarthquakesService(layer?: Layer): void {
const checked = this.formGroup.getRawValue().hasEarthquakesLayer;
if (layer) {
this.toggleLayer(layer, checked, 'Adding latest earthquakes ...');
} else if (checked === undefined || checked) {
this.getEarthquakes();
}
}
/**
* Call the features service with a specific feature type.
*
* @param featureType The feature type
*/
callFeaturesService(featureType: FeatureType, layer: Layer, checked: boolean): void {
if (layer) {
this.toggleLayer(layer, checked, `Adding ${this.featureTypeDisplay(featureType)} ...`);
} else if (checked) {
this.getFeatures(featureType);
} else {
this.arcgisService.filterLayer(this.map(), featureType);
if (featureType === FeatureType.FAULT) {
this.arcgisService.filterLayer(this.map(), FAULT_POLY_ID);
}
}
}
callFeaturesUsages$(): Observable<void> {
const usages$ = this.state().nshmServices.map(
nshm =>
this.nshmpService
.callService$<FeaturesUsageResponse>(`${nshm.url}${this.services.features}`)
.pipe(
map(sourceFeaturesUsage => {
return {
nshm,
sourceFeaturesUsage,
};
}),
),
catchError((error: Error) => this.nshmpService.throwError$(error)),
);
return forkJoin(usages$).pipe(
map(usages => {
const sourceFeaturesUsages = new Map<string, FeaturesUsageResponse>();
usages.forEach(usage => {
sourceFeaturesUsages.set(usage.nshm.model, usage.sourceFeaturesUsage);
});
this.updateState({
sourceFeaturesUsages,
});
this.resetSourceFeatureControls();
}),
);
}
/**
* Call the map service to get NSHM map and boundary.
*/
callService(layer?: Layer): void {
const checked = this.formGroup.getRawValue().hasNshmBoundaryLayer;
if (layer) {
this.toggleLayer(layer, checked, 'Adding NSHM boundary ...');
} else if (checked !== false) {
this.getNshmBoundary();
}
}
/**
* Call test sites service.
*/
callTestSitesService(layer?: Layer): void {
const checked = this.formGroup.getRawValue().hasTestSitesLayer;
if (layer) {
this.toggleLayer(layer, checked, 'Adding test sites ...');
} else if (checked) {
this.getTestSites();
}
}
/**
* Create the hazard layer from hazard tile info.
*
* @param hazardTile The hazard tile
*/
createHazardLayer(layer?: Layer): void {
const checked = this.formGroup.getRawValue().hasHazardTiles;
if (layer) {
this.toggleLayer(layer, checked, 'Adding hazard layer ...');
} else if (checked) {
const hazardTile = this.findTile(this.hazardTiles(), this.formGroup.getRawValue());
const opacity = this.formGroup.getRawValue().overlayOpacity;
if (hazardTile === undefined) {
return null;
}
const spinnerRef = this.spinnerService.show('Adding hazard ...');
this.arcgisService.filterLayer(this.map(), LayerId.HAZARD);
const url = `${environment.webServices.hazardTiles}/${hazardTile.mapName}/MapServer`;
const hazardLayer = new TileLayer({
id: LayerId.HAZARD,
opacity: opacity / 100,
url,
});
this.updateState({
layers: {
...this.layers(),
hazardLayer,
},
});
this.map().add(hazardLayer);
spinnerRef.close();
}
}
/**
* Default control panel form field values.
*/
defaultFormValues(): ControlForm {
return {
hasDecollementLayer: null,
hasEarthquakesLayer: null,
hasFaultSectionLayer: null,
hasHazardTiles: null,
hasInterfaceSectionsLayer: null,
hasNshmBoundaryLayer: true,
hasTestSitesLayer: null,
hasTracesOnlyFaultSections: false,
hasTracesOnlyInterfaceSections: false,
hasZoneSourcesLayer: null,
hazardTileImt: null,
hazardTileReturnPeriod: null,
hazardTileYear: null,
latestEarthquakeTime: LatestEarthquakeTime.WEEK,
mapStyle: '2d',
model: null,
overlayOpacity: 60,
};
}
/**
* Returns the hazard tiles for a specific region, year, and IMT.
*
* @param tiles The hazard tiles
* @param form Control panel form values
*/
filterTilesByImt(tiles: HazardTile[], form: ControlForm): HazardTile[] {
if (form.model === null) {
return [];
}
return this.filterTilesByYear(tiles, form)
?.filter(tile => tile.imt === form.hazardTileImt)
.sort((a, b) => a.returnPeriod - b.returnPeriod);
}
/**
* Returns the hazard tiles for a specific region.
*
* @param tiles The hazard tiles
* @param form Control panel form values
*/
filterTilesByRegion(tiles: HazardTile[], form: ControlForm): HazardTile[] {
if (form.model === null) {
return [];
}
return tiles
?.filter(tile => nshmRegion(tile.nshm) === nshmRegion(form.model))
.sort((a, b) => nshmYear(a.nshm) - nshmYear(b.nshm));
}
/**
* Returns the hazard tiles for a specific region and year.
*
* @param tiles The hazard tiles
* @param form Control panel form values
*/
filterTilesByYear(tiles: HazardTile[], form: ControlForm): HazardTile[] {
if (form.model === null) {
return [];
}
return this.filterTilesByRegion(tiles, form)
.filter(tile => nshmYear(tile.nshm) === form.hazardTileYear)
.sort((a, b) => (a.imt.toLowerCase() > b.imt.toLowerCase() ? 1 : -1));
}
/**
* Returns the hazard tile for a specific region, year, IMT, and return period.
*
* @param tiles The hazard tiles
* @param form Control panel form values
*/
findTile(tiles: HazardTile[], form: ControlForm): HazardTile {
return this.filterTilesByImt(tiles, form)?.find(
tile => tile.returnPeriod === form.hazardTileReturnPeriod,
);
}
/**
* Initialize the application.
*/
init(): void {
const spinnerRef = this.spinnerService.show(SpinnerService.MESSAGE_METADATA);
this.callTreesUsage()
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
mergeMap(() => forkJoin([this.callFeaturesUsages$(), this.callHazardTilesUsage()])),
catchError(error => {
this.initialFormSet();
spinnerRef.close();
console.error(error);
return of();
}),
)
.subscribe(() => {
this.initialFormSet();
spinnerRef.close();
});
}
/**
* Application initial state.
*/
initialState(): AppState {
const treesUsageResponses: Map<string, SourceLogicTreesUsage> = new Map();
treesUsageResponses.set(NshmId.CONUS_2018, null);
return {
availableModels: [],
hazardTiles: [],
layers: {
decollementLayer: null,
earthquakesLayer: null,
faultSectionLayer: null,
faultSectionPolygonLayer: null,
hazardLayer: null,
interfaceSectionsLayer: null,
nshmBoundaryLayer: null,
testSitesLayer: null,
zoneSourcesLayer: null,
},
nshmServices: [],
sourceFeaturesUsages: new Map(),
treesUsageResponses,
};
}
onMapStyleChange(mapStyle: '2d' | '3d'): void {
if (mapStyle === '2d') {
this.map2d = new ArcGisMap();
} else {
this.map3d = new ArcGisMap();
}
}
onMapViewChange(): void {
const layers = this.layers();
const values = this.formGroup.getRawValue();
this.callFeaturesService(
FeatureType.DECOLLEMENT,
layers.decollementLayer,
values.hasDecollementLayer,
);
this.callFeaturesService(
FeatureType.FAULT,
layers.faultSectionLayer,
values.hasFaultSectionLayer,
);
this.callFeaturesService(
FeatureType.INTERFACE,
layers.interfaceSectionsLayer,
values.hasInterfaceSectionsLayer,
);
this.callFeaturesService(FeatureType.ZONE, layers.zoneSourcesLayer, values.hasZoneSourcesLayer);
this.callService(layers.nshmBoundaryLayer);
this.callTestSitesService(layers.testSitesLayer);
this.callEarthquakesService(layers.earthquakesLayer);
this.createHazardLayer(layers.hazardLayer);
}
/**
* Reset the style of all layers.
*/
resetLayers(): void {
Object.values(LayerId).forEach(id => this.arcgisService.filterLayer(this.map(), id));
}
resetState(): void {
const values = this.formGroup.getRawValue();
const hazardTiles = this.filterTilesByRegion(this.hazardTiles(), values);
const defaultTile = [...hazardTiles].pop();
this.resetSourceFeatureControls();
this.resetLayers();
this.updateState({
layers: {
decollementLayer: null,
earthquakesLayer: null,
faultSectionLayer: null,
faultSectionPolygonLayer: null,
hazardLayer: null,
interfaceSectionsLayer: null,
nshmBoundaryLayer: null,
testSitesLayer: null,
zoneSourcesLayer: null,
},
});
if (hazardTiles.length > 0) {
this.formGroup.controls.hasHazardTiles.enable();
} else {
this.formGroup.controls.hasHazardTiles.disable();
}
this.formGroup.patchValue({
hasEarthquakesLayer: false,
hasHazardTiles: false,
hasTestSitesLayer: false,
hazardTileImt: defaultTile?.imt,
hazardTileReturnPeriod: defaultTile?.returnPeriod,
hazardTileYear: defaultTile ? nshmYear(defaultTile?.nshm) : undefined,
});
this.callService();
}
/**
* Check to see if feature type is included in the list of supported feature type to
* enable or disable the control form field.
*
* @param featureType The feature type
* @param featureTypes List of all feature types
* @param control Feature type form state
*/
sourceFeatureControl(
featureType: FeatureType,
featureTypes: string[],
control: AbstractControl<boolean>,
): void {
control.setValue(false);
if (featureTypes.includes(featureType)) {
control.enable();
} else {
control.disable();
}
}
toggleLayer(layer: Layer, checked: boolean, spinnerText = 'Adding layer ...'): void {
if (checked) {
const spinnerRef = this.spinnerService.show(spinnerText);
this.map().add(layer);
spinnerRef.close();
} else {
this.arcgisService.filterLayer(this.map(), layer.id);
}
}
updateState(state: Partial<AppState>): void {
const updatedState = {
...this.state(),
...state,
};
if (!deepEqual(updatedState, this.state())) {
this.state.set({
...this.state(),
...state,
});
}
}
private callHazardTilesUsage(): Observable<void> {
return this.http.get<ArcServiceResponse>(`${environment.webServices.hazardTiles}?f=pjson`).pipe(
map(response => {
const hazardTiles = this.toHazardTiles(response);
const controls = this.formGroup.controls;
const hazardTilesForRegion = this.filterTilesByRegion(
hazardTiles,
this.formGroup.getRawValue(),
);
if (hazardTilesForRegion.length > 0) {
controls.hasHazardTiles.enable();
} else {
controls.hasHazardTiles.disable();
}
const defaultTile = [...hazardTilesForRegion].pop();
if (defaultTile) {
this.formGroup.patchValue({
hasHazardTiles: false,
hazardTileImt: defaultTile?.imt,
hazardTileReturnPeriod: defaultTile?.returnPeriod,
hazardTileYear: nshmYear(defaultTile?.nshm),
});
}
this.updateState({
hazardTiles,
});
}),
);
}
private callTreesUsage(): Observable<void> {
return this.hazardService
.dynamicNshms$<SourceLogicTreesMetadata>(
`${this.nshmpHazWs.url}${this.nshmsEndpoint}`,
environment.webServices.nshmpHazWs.services.curveServices.trees,
)
.pipe(
map(({models, nshmServices, usageResponses}) => {
this.updateState({
availableModels: models,
nshmServices,
treesUsageResponses: usageResponses,
});
}),
catchError((error: Error) => {
return this.nshmpService.throwError$(error);
}),
);
}
private createEarthquakeLayer(
earthquakes: GeoJSON.FeatureCollection<GeoJSON.Point, EarthquakeFeatureProperties>,
): GeoJSONLayer {
const renderer = new SimpleRenderer({
symbol: new SimpleMarkerSymbol({
color: [255, 159, 0, 0.8],
style: 'circle',
}),
visualVariables: [
new SizeVariable({
field: 'mag',
maxDataValue: 10,
maxSize: 35,
minDataValue: this.minMw,
minSize: 5,
}),
],
});
const earthquakeLayer = new GeoJSONLayer({
elevationInfo: {
mode: 'on-the-ground',
},
id: LayerId.EARTHQUAKE,
popupTemplate: {
content: (event: __esri.GraphicHit) => {
const id = event.graphic.getObjectId();
const feature = earthquakes.features.find(feature => feature.id === id);
const [latitude, longitude, depth] = feature.geometry.coordinates;
const time = new Date(feature.properties.time);
const latFormat = new FormatLatitudePipe(this.localId).transform(latitude);
const lonFormat = new FormatLongitudePipe(this.localId).transform(longitude);
const a = `
<table>
<tr>
<td class="text-bold">Time</td>
<td>${time.toUTCString()}}</td>
</tr>
<tr>
<td class="padding-right-3 text-bold">Magnitude</td>
<td>${feature.properties.mag}</td>
</tr>
<tr>
<td class="text-bold">Location</td>
<td>${lonFormat}, ${latFormat}</td>
</tr>
<tr>
<td class="text-bold">Depth</td>
<td>${depth} km</td>
</tr>
</table>
`;
return a;
},
returnGeometry: true,
title: '{title}',
},
renderer,
url: this.arcgisService.geoJsonToUrl(earthquakes),
});
return earthquakeLayer;
}
private earthquakesToLayer$(
earthquakes: GeoJSON.FeatureCollection<GeoJSON.Point, EarthquakeFeatureProperties>,
): Observable<GeoJSONLayer> {
return from(this.layers().nshmBoundaryLayer.when()).pipe(
map(() => {
const features = earthquakes.features.filter(feature => {
const coords = feature.geometry.coordinates;
return this.layers().nshmBoundaryLayer.fullExtent.contains(
new Point({
latitude: coords[1],
longitude: coords[0],
}),
);
});
earthquakes = {
...earthquakes,
features,
};
this.arcgisService.filterLayer(this.map(), LayerId.EARTHQUAKE);
const layer = this.createEarthquakeLayer(earthquakes);
this.map().add(layer);
return layer;
}),
);
}
private featureTypeDisplay(featureType: FeatureType): string {
switch (featureType) {
case FeatureType.DECOLLEMENT:
return 'Décollement Sections';
case FeatureType.FAULT:
return 'Fault Sections';
case FeatureType.INTERFACE:
return 'Interface Sections';
case FeatureType.ZONE:
return 'Zone Sources';
default:
return featureType.toString();
}
}
/**
* Returns a layer of a feature type.
*
* @param fc The GeoJSON feature collection
* @param featureType The feature type
* @returns
*/
private featureTypeToLayer(
fc: GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon>,
featureType: FeatureType,
): GeoJSONLayer {
let color = [0, 0, 0, 0];
this.arcgisService.filterLayer(this.map(), featureType);
let display = '';
switch (featureType) {
case FeatureType.DECOLLEMENT:
color = [128, 0, 128];
display = 'Décollement Sections';
break;
case FeatureType.FAULT:
this.arcgisService.filterLayer(this.map(), FAULT_POLY_ID);
return this.faultSectionToLayers(fc);
case FeatureType.INTERFACE:
color = [0, 138, 0];
display = 'Interface Sections';
break;
case FeatureType.ZONE:
color = [0, 0, 255];
display = 'Zone Sources';
fc.features.sort((featureA, featureB) => {
const a = featureA.geometry.coordinates[0];
const b = featureB.geometry.coordinates[0];
if (a.length > b.length) {
return -1;
} else if (a.length < b.length) {
return 1;
}
return 0;
});
fc.features = fc.features.map((feature, index) => {
feature.id = index;
return feature;
});
break;
default:
throw new Error(`Feature type [${featureType}] not allowed`);
}
const {hasTracesOnlyInterfaceSections} = this.formGroup.getRawValue();
const renderer = new SimpleRenderer({
symbol:
featureType === FeatureType.ZONE ||
(featureType === FeatureType.INTERFACE && !hasTracesOnlyInterfaceSections)
? new SimpleFillSymbol({
color: [...color, 0.2],
outline: {
color,
width: 2,
},
})
: new SimpleLineSymbol({
color,
width: 2,
}),
});
const geoJsonLayer = new GeoJSONLayer({
elevationInfo: {
mode: 'on-the-ground',
},
id: featureType,
popupTemplate: this.featureTypeToPopupTemplate(fc, display),
renderer,
url: this.arcgisService.geoJsonToUrl(fc),
});
this.map().add(geoJsonLayer);
return geoJsonLayer;
}
private featureTypeToPopupTemplate(
fc: GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon>,
display: string,
): __esri.PopupTemplateProperties {
return {
content: (event: __esri.GraphicHit) => {
const feature = fc.features.find(feature => feature.id === event.graphic.getObjectId());
const rows = Object.entries(feature.properties).map(([key, value]) => {
return `
<tr>
<td class="padding-right-3 text-bold">${key}</td>
<td>${value}</td>
</tr>
`;
});
return `
<table>
<tr>
<td class="text-bold padding-right-3">Source Type</td>
<td>${display}</td>
</tr>
${rows.join('\n')}
</table>
`;
},
};
}
private faultSectionToLayers(
fc: GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon>,
): GeoJSONLayer {
const {hasTracesOnlyFaultSections} = this.formGroup.getRawValue();
const color = [255, 0, 0];
const display = 'Fault Sections';
const width = 1;
const polyRenderer = new SimpleRenderer({
symbol: new SimpleFillSymbol({
color: [...color, 0.2],
outline: {
color,
width,
},
}),
});
const lineRenderer = new SimpleRenderer({
symbol: new SimpleLineSymbol({
color,
width,
}),
});
let lineLayer: GeoJSONLayer;
if (hasTracesOnlyFaultSections) {
lineLayer = new GeoJSONLayer({
elevationInfo: {
mode: 'on-the-ground',
},
id: FeatureType.FAULT,
popupTemplate: this.featureTypeToPopupTemplate(fc, display),
renderer: lineRenderer,
url: this.arcgisService.geoJsonToUrl(fc),
});
this.map().add(lineLayer);
} else {
const fcPoly = {
...fc,
features: fc.features.filter(feature => feature.geometry.type === 'Polygon'),
};
const fcLine = {
...fc,
features: fc.features.filter(feature => feature.geometry.type === 'LineString'),
};
lineLayer = new GeoJSONLayer({
elevationInfo: {
mode: 'on-the-ground',
},
id: FeatureType.FAULT,
popupTemplate: this.featureTypeToPopupTemplate(fcLine, display),
renderer: lineRenderer,
url: this.arcgisService.geoJsonToUrl(fcLine),
});
const polyLayer = new GeoJSONLayer({
elevationInfo: {
mode: 'on-the-ground',
},
id: FAULT_POLY_ID,
popupTemplate: this.featureTypeToPopupTemplate(fcPoly, display),
renderer: polyRenderer,
url: this.arcgisService.geoJsonToUrl(fcPoly),
});
this.map().add(polyLayer);
this.map().add(lineLayer);
this.updateState({
layers: {
...this.layers(),
faultSectionPolygonLayer: polyLayer,
},
});
}
return lineLayer;
}
private getEarthquakes(): void {
const spinnerRef = this.spinnerService.show('Adding latest earthquakes ...');
const time = this.formGroup.getRawValue().latestEarthquakeTime.toLowerCase();
const service = environment.webServices.latestEarthquakes;
const url = `${service}/${this.minMw}_${time}.geojson`;
this.http
.get<GeoJSON.FeatureCollection<GeoJSON.Point>>(url)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
.subscribe(response => {
this.earthquakesToLayer$(response)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(earthquakesLayer => {
spinnerRef.close();
this.updateState({
layers: {
...this.layers(),
earthquakesLayer,
},
});
});
});
}
private getFeatures(featureType: FeatureType): void {
const spinnerRef = this.spinnerService.show(
`Adding ${this.featureTypeDisplay(featureType)} ...`,
);
let polygons = false;
switch (featureType) {
case FeatureType.INTERFACE: {
polygons = !this.formGroup.getRawValue().hasTracesOnlyInterfaceSections;
break;
}
case FeatureType.FAULT: {
polygons = !this.formGroup.getRawValue().hasTracesOnlyFaultSections;
break;
}
}
const params = new HttpParams().appendAll({
featureType,
polygons,
raw: true,
});
const faultSectionsUrl = `${this.nshmService().url}${this.services.features}?${params.toString()}`;
this.http
.get<
GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon>
>(faultSectionsUrl)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
.subscribe(serviceResponse => {
const layer = this.featureTypeToLayer(serviceResponse, featureType);
spinnerRef.close();
switch (featureType) {
case FeatureType.DECOLLEMENT: {
this.updateState({
layers: {
...this.layers(),
decollementLayer: layer,
},
});
break;
}
case FeatureType.FAULT: {
this.updateState({
layers: {
...this.layers(),
faultSectionLayer: layer,
},
});
break;
}
case FeatureType.INTERFACE: {
this.updateState({
layers: {
...this.layers(),
interfaceSectionsLayer: layer,
},
});
break;
}
case FeatureType.ZONE: {
this.updateState({
layers: {
...this.layers(),
zoneSourcesLayer: layer,
},
});
break;
}
default:
throw new Error(`Feature type [${featureType}] not allowed`);
}
});
}
private getNshmBoundary(): void {
const url = `${this.nshmService().url}${this.services.map}?raw=true`;
this.http
.get<GeoJSON.FeatureCollection>(url)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
.subscribe(response => {
this.arcgisService.filterLayer(this.map(), LayerId.NSHM);
const nshmBorder = response.features.find(feature => feature.id !== 'Extents');
const renderer = new SimpleRenderer({
symbol: {
color: [0, 0, 0, 0],
outline: {
color: 'black',
width: 1,
},
style: 'solid',
type: 'simple-fill',
},
});
const layer = new GeoJSONLayer({
id: LayerId.NSHM,
renderer,
url: this.arcgisService.geoJsonToUrl(nshmBorder),
});
this.updateState({
layers: {
...this.layers(),
nshmBoundaryLayer: layer,
},
});
this.map().add(layer);
});
}
private getTestSites(): void {
const spinnerRef = this.spinnerService.show('Adding test sites');
const url = `${this.nshmService().url}${this.services.sites}?raw=true`;
this.http
.get<GeoJSON.FeatureCollection>(url)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
.subscribe(testSites => {
this.arcgisService.filterLayer(this.map(), LayerId.TEST_SITES);
const testSitesLayer = this.arcgisService.testSitesToLayer(testSites, LayerId.TEST_SITES);
this.map().add(testSitesLayer);
spinnerRef.close();
this.updateState({
layers: {
...this.layers(),
testSitesLayer,
},
});
});
}
private initialControlSet(
control: FormControl<boolean>,
defaultValue: boolean,
queryValue?: string,
): void {
if (control.enabled) {
control.patchValue(nshmpUtils.queryParseBoolean(defaultValue, queryValue));
}
}
private initialFormSet(): void {
const query = this.route.snapshot.queryParams as Query;
const defaultValues = this.defaultFormValues();
const controls = this.formGroup.controls;
this.formGroup.patchValue({
model: query?.model ?? NshmId.CONUS_2018,
});
this.formGroup.patchValue({
hasTestSitesLayer: nshmpUtils.queryParseBoolean(
defaultValues.hasTestSitesLayer,
query?.hasTestSitesLayer,
),
});
this.initialControlSet(
controls.hasDecollementLayer,
defaultValues.hasDecollementLayer,
query?.hasDecollementLayer,
);
this.initialControlSet(
controls.hasEarthquakesLayer,
defaultValues.hasEarthquakesLayer,
query?.hasEarthquakesLayer,
);
this.initialControlSet(
controls.hasFaultSectionLayer,
defaultValues.hasFaultSectionLayer,
query?.hasFaultSectionLayer,
);
this.initialControlSet(
controls.hasHazardTiles,
defaultValues.hasHazardTiles,
query?.hasHazardTiles,
);
this.initialControlSet(
controls.hasInterfaceSectionsLayer,
defaultValues.hasInterfaceSectionsLayer,
query?.hasInterfaceSectionsLayer,
);
this.initialControlSet(
controls.hasNshmBoundaryLayer,
defaultValues.hasNshmBoundaryLayer,
query?.hasNshmBoundaryLayer,
);
this.initialControlSet(
controls.hasZoneSourcesLayer,
defaultValues.hasZoneSourcesLayer,
query?.hasZoneSourcesLayer,
);
this.formGroup.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.updateUrl());
}
private map(): ArcGisMap {
return this.formGroup.getRawValue().mapStyle === '2d' ? this.map2d : this.map3d;
}
/**
* Returns the `NshmId` from a string.
*
* @param nshm The NSHM string
*/
private nshmIdFromString(nshm: string): NshmId {
const nshmId = Object.values(NshmId).find(id => id.toString() === nshm);
if (nshmId === undefined) {
throw new Error(`NSHM ${nshm} not found`);
}
return nshmId;
}
private resetSourceFeatureControls(): void {
const usage = this.sourceFeaturesUsage();
if (usage) {
const featureTypes = this.sourceFeaturesUsage().response.featureType.map(type => type.value);
const controls = this.formGroup.controls;
this.sourceFeatureControl(
FeatureType.DECOLLEMENT,
featureTypes,
controls.hasDecollementLayer,
);
this.sourceFeatureControl(FeatureType.FAULT, featureTypes, controls.hasFaultSectionLayer);
this.sourceFeatureControl(
FeatureType.INTERFACE,
featureTypes,
controls.hasInterfaceSectionsLayer,
);
this.sourceFeatureControl(FeatureType.ZONE, featureTypes, controls.hasZoneSourcesLayer);
}
}
/**
* Convert ArcGIS service response to `HazardTile`s.
*
* Example JSON response:
* ```
* {
* "currentVersion": 10.61,
* "services": [
* {
* "name": "haz/AK1hz050_1999",
* "type": "MapServer"
* }
* }
* ```
*
* @param response The ArcGIS service response
*/
private toHazardTiles(response: ArcServiceResponse): HazardTile[] {
const services = response.services.filter(
service => service.name.includes('hz') || service.name.includes('pga'),
);
const hazardTiles: HazardTile[] = [];
// Example name: haz/USpga050_2008
services.forEach(service => {
const mapName = service.name.split('haz/').pop();
const region = mapName.substring(0, 2);
const imt = this.toImt(mapName.substring(2, 5));
const returnPeriod = this.toReturnPeriod(mapName.substring(5, 8));
const year = Number.parseInt(mapName.split('_').pop());
const nshm = this.toNshmId(region, year);
const hazardTile: HazardTile = {
imt,
mapName,
nshm,
returnPeriod,
};
hazardTiles.push(hazardTile);
});
return hazardTiles;
}
/**
* Returns the `Imt` corresponding to the IMT string in the map name.
*
* @param imt The IMT string
*/
private toImt(imt: string): Imt {
switch (imt) {
case 'pga':
return Imt.PGA;
case '5hz':
return Imt.SA0P2;
case '1hz':
return Imt.SA1P0;
default:
throw new Error(`IMT [${imt}] not supported`);
}
}
/**
* Convert the region and year from the map name into a corresponding `NshmId`.
* @param region The region from map name
* @param year The year from map name
* @returns
*/
private toNshmId(region: string, year: number): NshmId {
switch (region) {
case 'AK':
return this.nshmIdFromString(`${nshmRegion(NshmId.ALASKA_2023)}_${year}`);
case 'HI':
return this.nshmIdFromString(`${nshmRegion(NshmId.HAWAII_2021)}_${year}`);
case 'US':
return this.nshmIdFromString(`${nshmRegion(NshmId.CONUS_2023)}_${year}`);
default:
throw new Error(`Region [${region}] year [${year}] not supported`);
}
}
/**
* Returns the `ReturnPeriod` from the corresponding string.
*
* @param returnPeriod The return period string from ArcGIS map name
*/
private toReturnPeriod(returnPeriod: string): ReturnPeriod {
switch (returnPeriod) {
// 10% in 50
case '050':
return ReturnPeriod.RP_475;
// 2% in 50
case '250':
return ReturnPeriod.RP_2475;
default:
throw new Error(`Return period [${returnPeriod}] not supported`);
}
}
private updateUrl(): void {
this.location.replaceState(
apps().source.modelMaps.routerLink,
new HttpParams().appendAll(this.formGroup.getRawValue()).toString(),
);
}
}