services/app.service.ts
Entrypoint to store for data mapping application.
Properties |
|
Methods |
|
Accessors |
constructor(formBuilder: FormBuilder, spinnerService: SpinnerService, nshmpService: NshmpService, hazardService: HazardService, http: HttpClient, zone: NgZone, route: ActivatedRoute, location: LocationService)
|
|||||||||||||||||||||||||||
Defined in services/app.service.ts:126
|
|||||||||||||||||||||||||||
Parameters :
|
addValidators |
addValidators()
|
Defined in services/app.service.ts:226
|
Returns :
void
|
baseLayer | ||||||||
baseLayer(baseLayer: MapBaseLayer)
|
||||||||
Defined in services/app.service.ts:237
|
||||||||
Set the base layer on the map.
Parameters :
Returns :
void
|
callEarthquakesService |
callEarthquakesService()
|
Defined in services/app.service.ts:253
|
Call latest earthquakes service.
Returns :
void
|
callFeaturesService | ||||||||
callFeaturesService(featureType: FeatureType)
|
||||||||
Defined in services/app.service.ts:285
|
||||||||
Call the features service with a specific feature type.
Parameters :
Returns :
void
|
callFeaturesUsages$ |
callFeaturesUsages$()
|
Defined in services/app.service.ts:341
|
Returns :
Observable<void>
|
Private callHazardTilesUsage |
callHazardTilesUsage()
|
Defined in services/app.service.ts:730
|
Returns :
Observable<void>
|
callService |
callService()
|
Defined in services/app.service.ts:380
|
Call the map service to get NSHM map and boundary.
Returns :
void
|
callTestSitesService |
callTestSitesService()
|
Defined in services/app.service.ts:425
|
Call test sites service.
Returns :
void
|
Private callTreesUsage |
callTreesUsage()
|
Defined in services/app.service.ts:776
|
Returns :
Observable<void>
|
createHazardLayer | ||||||||||||
createHazardLayer(hazardTile: HazardTile, opacity: number)
|
||||||||||||
Defined in services/app.service.ts:457
|
||||||||||||
Create the hazard layer from hazard tile info.
Parameters :
Returns :
L.TileLayer
|
defaultFormValues |
defaultFormValues()
|
Defined in services/app.service.ts:476
|
Default control panel form field values.
Returns :
ControlForm
|
Private defaultMapOptions |
defaultMapOptions()
|
Defined in services/app.service.ts:799
|
Default Leaflemt map options.
Returns :
L.MapOptions
|
Private dimLayer | ||||||||
dimLayer(layer: L.GeoJSON)
|
||||||||
Defined in services/app.service.ts:821
|
||||||||
Dim all features in a layer.
Parameters :
Returns :
void
|
dimLayers |
dimLayers()
|
Defined in services/app.service.ts:498
|
Dim all layers
Returns :
void
|
Private earthquakesToLayer | ||||||
earthquakesToLayer(earthquakes: GeoJSON.FeatureCollection
|
||||||
Defined in services/app.service.ts:840
|
||||||
Parameters :
Returns :
L.GeoJSON
|
Private extendBounds | ||||||||||||
extendBounds(bounds: L.LatLngBounds, layers: Layers)
|
||||||||||||
Defined in services/app.service.ts:912
|
||||||||||||
Returns the extended map bounds based on all layers.
Parameters :
Returns :
L.LatLngBounds
|
faultsWeight |
faultsWeight()
|
Defined in services/app.service.ts:508
|
Returns :
number
|
Private featureTypeToLayer | ||||||||||||
featureTypeToLayer(fc: GeoJSON.FeatureCollection, featureType: FeatureType)
|
||||||||||||
Defined in services/app.service.ts:932
|
||||||||||||
Returns a Leaflet layer of a feature type.
Parameters :
Returns :
L.GeoJSON
|
filterTilesByImt | ||||||||||||
filterTilesByImt(tiles: HazardTile[], form: ControlForm)
|
||||||||||||
Defined in services/app.service.ts:520
|
||||||||||||
Returns the hazard tiles for a specific region, year, and IMT.
Parameters :
Returns :
HazardTile[]
|
filterTilesByRegion | ||||||||||||
filterTilesByRegion(tiles: HazardTile[], form: ControlForm)
|
||||||||||||
Defined in services/app.service.ts:532
|
||||||||||||
Returns the hazard tiles for a specific region.
Parameters :
Returns :
HazardTile[]
|
filterTilesByYear | ||||||||||||
filterTilesByYear(tiles: HazardTile[], form: ControlForm)
|
||||||||||||
Defined in services/app.service.ts:544
|
||||||||||||
Returns the hazard tiles for a specific region and year.
Parameters :
Returns :
HazardTile[]
|
findTile | ||||||||||||
findTile(tiles: HazardTile[], form: ControlForm)
|
||||||||||||
Defined in services/app.service.ts:556
|
||||||||||||
Returns the hazard tile for a specific region, year, IMT, and return period.
Parameters :
Returns :
HazardTile
|
init |
init()
|
Defined in services/app.service.ts:565
|
Initialize the application.
Returns :
void
|
Private initialControlSet | ||||||||||||
initialControlSet(control: FormControl
|
||||||||||||
Defined in services/app.service.ts:1000
|
||||||||||||
Parameters :
Returns :
void
|
Private initialFormSet |
initialFormSet()
|
Defined in services/app.service.ts:1012
|
Returns :
void
|
initialState |
initialState()
|
Defined in services/app.service.ts:591
|
Application initial state.
Returns :
AppState
|
mapRedraw |
mapRedraw()
|
Defined in services/app.service.ts:628
|
Redraw the map.
Returns :
void
|
Private modelBounds | ||||||||
modelBounds(model: NshmId)
|
||||||||
Defined in services/app.service.ts:1073
|
||||||||
Returns the bounds for a specific NSHM.
Parameters :
Returns :
L.LatLngBounds
|
Private nshmIdFromString | ||||||||
nshmIdFromString(nshm: string)
|
||||||||
Defined in services/app.service.ts:1101
|
||||||||
Returns the
Parameters :
Returns :
NshmId
|
Private onEachFeatureSourceFeatures | ||||||||||||||||
onEachFeatureSourceFeatures(layer: L.GeoJSON, feature: GeoJSON.Feature, featureType: FeatureType)
|
||||||||||||||||
Defined in services/app.service.ts:1117
|
||||||||||||||||
Create tooltip with GeoJSON properties.
Parameters :
Returns :
void
|
resetLayers |
resetLayers()
|
Defined in services/app.service.ts:635
|
Reset the style of all layers.
Returns :
void
|
Private resetSourceFeatureControls |
resetSourceFeatureControls()
|
Defined in services/app.service.ts:1156
|
Returns :
void
|
resetState |
resetState()
|
Defined in services/app.service.ts:653
|
Returns :
void
|
sourceFeatureControl | ||||||||||||||||
sourceFeatureControl(featureType: FeatureType, featureTypes: string[], control: AbstractControl
|
||||||||||||||||
Defined in services/app.service.ts:702
|
||||||||||||||||
Check to see if feature type is included in the list of supported feature type to enable or disable the control form field.
Parameters :
Returns :
void
|
Private toHazardTiles | ||||||||
toHazardTiles(response: ArcServiceResponse)
|
||||||||
Defined in services/app.service.ts:1202
|
||||||||
Convert ArcGIS service response to Example JSON response: Example :
Parameters :
Returns :
HazardTile[]
|
Private toImt | ||||||||
toImt(imt: string)
|
||||||||
Defined in services/app.service.ts:1236
|
||||||||
Returns the
Parameters :
Returns :
Imt
|
Private toNshmId | ||||||||||||
toNshmId(region: string, year: number)
|
||||||||||||
Defined in services/app.service.ts:1255
|
||||||||||||
Convert the region and year from the map name into a corresponding
Parameters :
Returns :
NshmId
|
Private toReturnPeriod | ||||||||
toReturnPeriod(returnPeriod: string)
|
||||||||
Defined in services/app.service.ts:1279
|
||||||||
Returns the
Parameters :
Returns :
ReturnPeriod
|
updateState | ||||||
updateState(state: Partial<AppState>)
|
||||||
Defined in services/app.service.ts:716
|
||||||
Parameters :
Returns :
void
|
Private updateUrl |
updateUrl()
|
Defined in services/app.service.ts:1292
|
Returns :
void
|
Private faultZoomInWeight |
Type : number
|
Default value : 4
|
Defined in services/app.service.ts:111
|
Private faultZoomOutWeight |
Type : number
|
Default value : 1
|
Defined in services/app.service.ts:112
|
Private featuresHoverWeight |
Type : number
|
Default value : 5
|
Defined in services/app.service.ts:113
|
Readonly formGroup |
Default value : this.formBuilder.group<ControlForm>(
this.defaultFormValues(),
)
|
Defined in services/app.service.ts:122
|
Private nshmpHazWs |
Default value : environment.webServices.nshmpHazWs
|
Defined in services/app.service.ts:115
|
nshmp-haz-ws web config |
Private nshmsEndpoint |
Default value : this.nshmpHazWs.services.nshms
|
Defined in services/app.service.ts:117
|
Endpoint to NSHMs |
Private opacity |
Type : number
|
Default value : 0.6
|
Defined in services/app.service.ts:118
|
Private services |
Default value : this.nshmpHazWs.services.curveServices
|
Defined in services/app.service.ts:120
|
nshmp-haz web services |
Readonly state |
Default value : signal<AppState>(this.initialState())
|
Defined in services/app.service.ts:126
|
Application state |
availableModels |
getavailableModels()
|
Defined in services/app.service.ts:144
|
Returns the avialable models, as
Returns :
Signal<Parameter[]>
|
bounds |
getbounds()
|
Defined in services/app.service.ts:151
|
Returns the map bounds observable.
Returns :
Signal<L.LatLngBounds>
|
earthquakeInfoPopupData |
getearthquakeInfoPopupData()
|
Defined in services/app.service.ts:155
|
hazardTiles |
gethazardTiles()
|
Defined in services/app.service.ts:162
|
Returns the hazard tiles.
Returns :
Signal<HazardTile[]>
|
infoPopupData |
getinfoPopupData()
|
Defined in services/app.service.ts:169
|
Returns the info popup data.
Returns :
Signal<InfoPopupData>
|
layers |
getlayers()
|
Defined in services/app.service.ts:176
|
Returns the Leaflet map layers observable.
Returns :
Signal<Layers>
|
map | ||||||||
getmap()
|
||||||||
Defined in services/app.service.ts:183
|
||||||||
Returns the Leaflet map observable.
Returns :
Signal<L.Map>
|
||||||||
setmap(map: L.Map)
|
||||||||
Defined in services/app.service.ts:192
|
||||||||
Set the Leaflet map.
Parameters :
Returns :
void
|
mapOptions |
getmapOptions()
|
Defined in services/app.service.ts:199
|
Returns the Leaflet map options observable.
Returns :
Signal<L.MapOptions>
|
nshmService |
getnshmService()
|
Defined in services/app.service.ts:203
|
sourceFeaturesUsage |
getsourceFeaturesUsage()
|
Defined in services/app.service.ts:211
|
treesUsageResponse |
gettreesUsageResponse()
|
Defined in services/app.service.ts:220
|
Returns the trees usage.
Returns :
Signal<SourceLogicTreesUsage>
|
import {Location as LocationService} from '@angular/common';
import {HttpClient, HttpParams} from '@angular/common/http';
import {computed, Injectable, NgZone, Signal, signal} from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormControl,
Validators,
} from '@angular/forms';
import {ActivatedRoute} from '@angular/router';
import {HazardService} from '@ghsc/nshmp-lib-ng/hazard';
import {mapUtils} from '@ghsc/nshmp-lib-ng/map';
import {
NshmpService,
nshmpUtils,
ReturnPeriod,
SpinnerService,
} from '@ghsc/nshmp-lib-ng/nshmp';
import {MapBaseLayer} from '@ghsc/nshmp-utils-ts/libs/leaflet';
import * as nshmpLeaflet from '@ghsc/nshmp-utils-ts/libs/leaflet';
import {FaultSectionProperties} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/fault-sections-service';
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 * as L from 'leaflet';
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, map, mergeMap, Observable} 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 {Layers} from '../models/layers.model';
import {
AppState,
EarthquakeInfoPopupData,
InfoPopupData,
} 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;
/** Leaflet overlay opactiy */
overlayOpacity: string;
}
/**
* Entrypoint to store for data mapping application.
*/
@Injectable({
providedIn: 'root',
})
export class AppService implements AppServiceModel<AppState, ControlForm> {
private faultZoomInWeight = 4;
private faultZoomOutWeight = 1;
private featuresHoverWeight = 5;
/** nshmp-haz-ws web config */
private nshmpHazWs = environment.webServices.nshmpHazWs;
/** Endpoint to NSHMs */
private nshmsEndpoint = this.nshmpHazWs.services.nshms;
private opacity = 0.6;
/** nshmp-haz web services */
private services = this.nshmpHazWs.services.curveServices;
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 http: HttpClient,
private zone: NgZone,
private route: ActivatedRoute,
private location: LocationService,
) {
this.addValidators();
}
/**
* Returns the avialable models, as `Parameter`s, observable.
*/
get availableModels(): Signal<Parameter[]> {
return computed(() => this.state().availableModels);
}
/**
* Returns the map bounds observable.
*/
get bounds(): Signal<L.LatLngBounds> {
return computed(() => this.state().bounds);
}
get earthquakeInfoPopupData(): Signal<EarthquakeInfoPopupData> {
return computed(() => this.state().earthquakeInfoPopupData);
}
/**
* Returns the hazard tiles.
*/
get hazardTiles(): Signal<HazardTile[]> {
return computed(() => this.state().hazardTiles);
}
/**
* Returns the info popup data.
*/
get infoPopupData(): Signal<InfoPopupData> {
return computed(() => this.state().infoPopupData);
}
/**
* Returns the Leaflet map layers observable.
*/
get layers(): Signal<Layers> {
return computed(() => this.state().layers);
}
/**
* Returns the Leaflet map observable.
*/
get map(): Signal<L.Map> {
return computed(() => this.state().map);
}
/**
* Set the Leaflet map.
*
* @param map The Leaflet map
*/
set map(map: L.Map) {
this.updateState({map});
}
/**
* Returns the Leaflet map options observable.
*/
get mapOptions(): Signal<L.MapOptions> {
return computed(() => this.state().mapOptions);
}
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),
);
}
/**
* Set the base layer on the map.
*
* @param baseLayer The base layer to set
*/
baseLayer(baseLayer: MapBaseLayer): void {
this.updateState({
layers: {
...this.layers(),
baseLayer: nshmpLeaflet.baseLayer(baseLayer),
hazardLayer: this.createHazardLayer(
this.findTile(this.state().hazardTiles, this.formGroup.getRawValue()),
this.formGroup.getRawValue().overlayOpacity,
),
},
});
}
/**
* Call latest earthquakes service.
*/
callEarthquakesService(): void {
this.updateState({
layers: {
...this.layers(),
earthquakesLayer: null,
},
});
const time = this.formGroup
.getRawValue()
.latestEarthquakeTime.toLowerCase();
const service = environment.webServices.latestEarthquakes;
const url = `${service}/2.5_${time}.geojson`;
this.http
.get<GeoJSON.FeatureCollection<GeoJSON.Point>>(url)
.pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
.subscribe(response => {
this.updateState({
layers: {
...this.layers(),
earthquakesLayer: this.earthquakesToLayer(response),
},
});
});
}
/**
* Call the features service with a specific feature type.
*
* @param featureType The feature type
*/
callFeaturesService(featureType: FeatureType): void {
const faultSectionsUrl = `${this.nshmService().url}${this.services.features}/${featureType}/true`;
this.http
.get<GeoJSON.FeatureCollection>(faultSectionsUrl)
.pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
.subscribe(serviceResponse => {
const layer = this.featureTypeToLayer(serviceResponse, featureType);
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`);
}
this.updateState({
bounds: this.extendBounds(layer.getBounds(), this.layers()),
});
});
}
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();
this.callService();
}),
);
}
/**
* Call the map service to get NSHM map and boundary.
*/
callService(): void {
const url = `${this.nshmService().url}${this.services.map}?raw=true`;
this.http
.get<GeoJSON.FeatureCollection>(url)
.pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
.subscribe(response => {
const nshmBorder = response.features.find(
feature => feature.id !== 'Extents',
);
this.updateState({
layers: {
...this.layers(),
nshmBoundaryLayer: L.geoJSON(nshmBorder, {
style: {
color: 'black',
fill: false,
weight: 1,
},
}),
},
});
const {hasFaultSectionLayer, hasTestSitesLayer} =
this.formGroup.getRawValue();
let bounds = this.layers().nshmBoundaryLayer.getBounds();
const {faultSectionLayer, testSitesLayer} = this.layers();
if (hasFaultSectionLayer && faultSectionLayer) {
bounds = bounds.extend(faultSectionLayer.getBounds());
}
if (hasTestSitesLayer && testSitesLayer) {
bounds = bounds.extend(testSitesLayer.getBounds());
}
this.updateState({
bounds,
});
});
}
/**
* Call test sites service.
*/
callTestSitesService(): void {
const url = `${this.nshmService().url}${this.services.sites}?raw=true`;
this.http
.get<GeoJSON.FeatureCollection>(url)
.pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
.subscribe(response => {
const testSitesLayer = mapUtils.testSitesToLayer(response);
let bounds = testSitesLayer.getBounds();
const {hasFaultSectionLayer} = this.formGroup.getRawValue();
const {faultSectionLayer} = this.layers();
if (hasFaultSectionLayer && faultSectionLayer) {
bounds = bounds.extend(faultSectionLayer.getBounds());
}
this.updateState({
bounds,
layers: {
...this.layers(),
testSitesLayer,
},
});
});
}
/**
* Create the hazard layer from hazard tile info.
*
* @param hazardTile The hazard tile
*/
createHazardLayer(hazardTile: HazardTile, opacity: number): L.TileLayer {
if (hazardTile === undefined) {
return null;
}
const url = `${environment.webServices.hazardTiles}/${hazardTile.mapName}/MapServer/tile/{z}/{y}/{x}`;
return L.tileLayer(url, {
attribution: '',
id: 'us-hazard',
maxZoom: 12,
minZoom: 0,
opacity: opacity / 100,
});
}
/**
* Default control panel form field values.
*/
defaultFormValues(): ControlForm {
return {
hasDecollementLayer: null,
hasEarthquakesLayer: null,
hasFaultSectionLayer: null,
hasHazardTiles: null,
hasInterfaceSectionsLayer: null,
hasNshmBoundaryLayer: true,
hasTestSitesLayer: null,
hasZoneSourcesLayer: null,
hazardTileImt: null,
hazardTileReturnPeriod: null,
hazardTileYear: null,
latestEarthquakeTime: LatestEarthquakeTime.WEEK,
model: NshmId.CONUS_2018,
overlayOpacity: this.opacity * 100,
};
}
/**
* Dim all layers
*/
dimLayers(): void {
const layers = this.layers();
this.dimLayer(layers.decollementLayer);
this.dimLayer(layers.faultSectionLayer);
this.dimLayer(layers.interfaceSectionsLayer);
this.dimLayer(layers.zoneSourcesLayer);
this.dimLayer(layers.testSitesLayer);
}
faultsWeight(): number {
return this.state().map.getZoom() >= 7
? this.faultZoomInWeight
: this.faultZoomOutWeight;
}
/**
* 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[] {
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[] {
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[] {
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(
mergeMap(() =>
forkJoin([this.callFeaturesUsages$(), this.callHazardTilesUsage()]),
),
catchError(error => {
this.initialFormSet();
spinnerRef.close();
console.error(error);
return [];
}),
)
.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: [],
bounds: null,
earthquakeInfoPopupData: {
feature: null,
},
hazardTiles: [],
infoPopupData: {
feature: null,
featureType: null,
},
layers: {
baseLayer: nshmpLeaflet.baseLayer(MapBaseLayer.OCEAN),
decollementLayer: null,
earthquakesLayer: null,
faultSectionLayer: null,
hazardLayer: null,
interfaceSectionsLayer: null,
nshmBoundaryLayer: null,
testSitesLayer: null,
zoneSourcesLayer: null,
},
map: null,
mapOptions: this.defaultMapOptions(),
nshmServices: [],
sourceFeaturesUsages: new Map(),
treesUsageResponses,
};
}
/**
* Redraw the map.
*/
mapRedraw(): void {
setTimeout(() => this.state().map?.invalidateSize());
}
/**
* Reset the style of all layers.
*/
resetLayers(): void {
this.layers()
.testSitesLayer?.getLayers()
.forEach((layer: L.Marker) => layer.setOpacity(1));
[
this.layers().decollementLayer,
this.layers().faultSectionLayer,
this.layers().interfaceSectionsLayer,
this.layers().zoneSourcesLayer,
this.layers().earthquakesLayer,
].forEach(layer => layer?.resetStyle());
this.layers().faultSectionLayer.setStyle({
weight: this.faultsWeight(),
});
}
resetState(): void {
const values = this.formGroup.getRawValue();
const hazardTiles = this.filterTilesByRegion(this.hazardTiles(), values);
const defaultTile = [...hazardTiles].pop();
this.resetSourceFeatureControls();
this.updateState({
bounds: this.modelBounds(values.model),
earthquakeInfoPopupData: this.initialState().earthquakeInfoPopupData,
infoPopupData: this.initialState().infoPopupData,
layers: {
baseLayer: this.layers().baseLayer,
decollementLayer: null,
earthquakesLayer: this.layers().earthquakesLayer,
faultSectionLayer: null,
hazardLayer: this.createHazardLayer(defaultTile, values.overlayOpacity),
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();
}
}
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(),
);
const defaultTile = [...hazardTilesForRegion].pop();
if (hazardTilesForRegion.length > 0) {
controls.hasHazardTiles.enable();
} else {
controls.hasHazardTiles.disable();
}
this.formGroup.patchValue({
hasHazardTiles: false,
hazardTileImt: defaultTile?.imt,
hazardTileReturnPeriod: defaultTile?.returnPeriod,
hazardTileYear: nshmYear(defaultTile?.nshm),
});
this.updateState({
hazardTiles,
layers: {
...this.layers(),
hazardLayer: this.createHazardLayer(
defaultTile,
this.formGroup.getRawValue().overlayOpacity,
),
},
});
this.formGroup.controls.model.setValue(
this.formGroup.getRawValue().model,
);
}),
);
}
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);
}),
);
}
/**
* Default Leaflemt map options.
*/
private defaultMapOptions(): L.MapOptions {
/** North-east default map bounds */
const northEastBounds = new L.LatLng(85, Infinity);
/** South-west default map bounds */
const southWestBounds = new L.LatLng(-85, -Infinity);
/** Default map bounds */
const maxBounds = new L.LatLngBounds(southWestBounds, northEastBounds);
return {
center: L.latLng(40, -105),
maxBounds,
minZoom: 3,
renderer: L.canvas({tolerance: 5}),
zoom: 4,
};
}
/**
* Dim all features in a layer.
*
* @param layer The layer to dim
*/
private dimLayer(layer: L.GeoJSON): void {
if (layer === null) {
return;
}
const opacity = 0.3;
layer.getLayers().forEach((feature: L.GeoJSON | L.Marker) => {
if (feature instanceof L.Marker) {
feature.setOpacity(opacity);
} else {
feature.setStyle({
fillOpacity: opacity,
opacity,
});
}
});
}
private earthquakesToLayer(
earthquakes: GeoJSON.FeatureCollection<GeoJSON.Point>,
): L.GeoJSON {
const features = earthquakes.features.filter(feature => {
const coords = feature.geometry.coordinates;
return this.bounds().contains(L.latLng(coords[1], coords[0]));
});
earthquakes = {
...earthquakes,
features,
};
const earthquakeLayer = L.geoJSON<
GeoJSON.FeatureCollection<GeoJSON.Point, EarthquakeFeatureProperties>
>(earthquakes, {
onEachFeature: (feature, layer: L.GeoJSON) => {
const popupContent = 'Select for properties';
layer.bindTooltip(popupContent, {
offset: L.point(10, 0),
});
layer.on('click', event => {
this.zone.run(() => {
this.resetLayers();
L.DomEvent.stopPropagation(event);
layer.setStyle({
fillColor: '#0ff',
});
this.updateState({
earthquakeInfoPopupData: {
feature: feature as GeoJSON.Feature<
GeoJSON.Point,
EarthquakeFeatureProperties
>,
},
infoPopupData: null,
});
});
});
},
pointToLayer: (
feature: GeoJSON.Feature<GeoJSON.Point, EarthquakeFeatureProperties>,
latlng,
) => {
const layer = L.circle(latlng, {
color: 'black',
fill: true,
fillColor: 'orange',
fillOpacity: this.opacity,
radius:
(feature.properties.mag * 150000) /
Math.pow(this.map().getZoom(), 2),
weight: 1,
});
return layer;
},
});
return earthquakeLayer;
}
/**
* Returns the extended map bounds based on all layers.
*
* @param bounds The Leaflet map bounds
* @param layers Leaflet layers
*/
private extendBounds(bounds: L.LatLngBounds, layers: Layers): L.LatLngBounds {
Object.values(layers)
.filter(layer => layer instanceof L.GeoJSON)
.filter(layer => layer !== layers.earthquakesLayer)
.forEach((layer: L.GeoJSON) => {
if (layer) {
bounds = bounds.extend(layer.getBounds());
}
});
return bounds;
}
/**
* Returns a Leaflet layer of a feature type.
*
* @param fc The GeoJSON feature collection
* @param featureType The feature type
* @returns
*/
private featureTypeToLayer(
fc: GeoJSON.FeatureCollection,
featureType: FeatureType,
): L.GeoJSON {
let color = 'red';
let weight = 2;
switch (featureType) {
case FeatureType.DECOLLEMENT:
color = 'purple';
break;
case FeatureType.FAULT:
color = 'red';
weight = this.faultsWeight();
break;
case FeatureType.INTERFACE:
color = 'green';
break;
case FeatureType.ZONE:
color = 'blue';
break;
default:
throw new Error(`Feature type [${featureType}] not allowed`);
}
const geoJsonLayer = L.geoJSON<GeoJSON.FeatureCollection>(fc, {
onEachFeature: (feature, layer: L.GeoJSON) =>
this.onEachFeatureSourceFeatures(layer, feature, featureType),
style: {
color,
fillOpacity: this.opacity,
opacity: this.opacity,
weight,
},
});
geoJsonLayer.eachLayer((layer: L.GeoJSON) => {
layer.on('mouseover', event => {
geoJsonLayer.resetStyle();
geoJsonLayer.setStyle({
weight:
featureType === FeatureType.FAULT ? this.faultsWeight() : weight,
});
const target = event.target as L.GeoJSON;
target.setStyle({
fillOpacity: 1,
opacity: 1,
weight:
featureType === FeatureType.FAULT
? this.faultsWeight() * 2
: this.featuresHoverWeight,
});
});
layer.on('mouseout', () => {
if (!deepEqual(this.infoPopupData().feature, layer.toGeoJSON())) {
geoJsonLayer.resetStyle();
geoJsonLayer.setStyle({
weight:
featureType === FeatureType.FAULT ? this.faultsWeight() : weight,
});
}
});
});
return geoJsonLayer;
}
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 ?? defaultValues.model,
});
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?.hasNshmBoundaryLayer,
);
this.initialControlSet(
controls.hasNshmBoundaryLayer,
defaultValues.hasNshmBoundaryLayer,
query?.hasNshmBoundaryLayer,
);
this.initialControlSet(
controls.hasZoneSourcesLayer,
defaultValues.hasZoneSourcesLayer,
query?.hasZoneSourcesLayer,
);
this.formGroup.valueChanges.subscribe(() => this.updateUrl());
this.mapRedraw();
}
/**
* Returns the bounds for a specific NSHM.
*
* @param model The NSHM
*/
private modelBounds(model: NshmId): L.LatLngBounds {
/** COUNS NSHM bounds */
const conusBounds = new L.LatLngBounds(
new L.LatLng(48.75, -68.8),
new L.LatLng(25.75, -124.25),
);
const bounds: Partial<Record<NshmId, L.LatLngBounds>> = {
ALASKA_2023: new L.LatLngBounds(
new L.LatLng(71.3, -131.7),
new L.LatLng(51.9, -176.7),
),
CONUS_2018: conusBounds,
CONUS_2023: conusBounds,
HAWAII_2021: new L.LatLngBounds(
new L.LatLng(22.22, -155.06),
new L.LatLng(19.1, -160.2),
),
};
return bounds[model];
}
/**
* 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;
}
/**
* Create tooltip with GeoJSON properties.
*
* @param feature The GeoJSON feature
* @param layer The Leaflet layer
*/
private onEachFeatureSourceFeatures(
layer: L.GeoJSON,
feature: GeoJSON.Feature,
featureType: FeatureType,
): void {
const popupContent = 'Select for properties';
layer.bindTooltip(popupContent, {
offset: L.point(10, 0),
});
layer.on('click', event => {
this.zone.run(() => {
L.DomEvent.stopPropagation(event);
this.resetLayers();
this.updateState({
earthquakeInfoPopupData: null,
infoPopupData: {
feature: feature as unknown as GeoJSON.Feature<
GeoJSON.Geometry,
FaultSectionProperties
>,
featureType,
},
});
layer.setStyle({
fillOpacity: 1,
opacity: 1,
weight:
featureType === FeatureType.FAULT
? this.faultsWeight() * 2
: this.featuresHoverWeight,
});
});
});
}
private resetSourceFeatureControls(): void {
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.data.routerLink,
new HttpParams().appendAll(this.formGroup.getRawValue()).toString(),
);
}
}