File

services/app.service.ts

Description

Entrypoint to store for data mapping application.

Index

Properties
Methods
Accessors

Constructor

constructor(formBuilder: FormBuilder, spinnerService: SpinnerService, nshmpService: NshmpService, hazardService: HazardService, http: HttpClient, route: ActivatedRoute, location: LocationService, arcgisService: ArcgisService, destroyRef: DestroyRef, localId: string)
Parameters :
Name Type Optional
formBuilder FormBuilder No
spinnerService SpinnerService No
nshmpService NshmpService No
hazardService HazardService No
http HttpClient No
route ActivatedRoute No
location LocationService No
arcgisService ArcgisService No
destroyRef DestroyRef No
localId string No

Methods

addValidators
addValidators()
Returns : void
callEarthquakesService
callEarthquakesService(layer?: Layer)

Call latest earthquakes service.

Parameters :
Name Type Optional
layer Layer Yes
Returns : void
callFeaturesService
callFeaturesService(featureType: FeatureType, layer: Layer, checked: boolean)

Call the features service with a specific feature type.

Parameters :
Name Type Optional Description
featureType FeatureType No

The feature type

layer Layer No
checked boolean No
Returns : void
callFeaturesUsages$
callFeaturesUsages$()
Returns : Observable<void>
Private callHazardTilesUsage
callHazardTilesUsage()
Returns : Observable<void>
callService
callService(layer?: Layer)

Call the map service to get NSHM map and boundary.

Parameters :
Name Type Optional
layer Layer Yes
Returns : void
callTestSitesService
callTestSitesService(layer?: Layer)

Call test sites service.

Parameters :
Name Type Optional
layer Layer Yes
Returns : void
Private callTreesUsage
callTreesUsage()
Returns : Observable<void>
Private createEarthquakeLayer
createEarthquakeLayer(earthquakes: GeoJSON.FeatureCollection<GeoJSON.Point | EarthquakeFeatureProperties>)
Parameters :
Name Type Optional
earthquakes GeoJSON.FeatureCollection<GeoJSON.Point | EarthquakeFeatureProperties> No
Returns : GeoJSONLayer
createHazardLayer
createHazardLayer(layer?: Layer)

Create the hazard layer from hazard tile info.

Parameters :
Name Type Optional
layer Layer Yes
Returns : void
defaultFormValues
defaultFormValues()

Default control panel form field values.

Returns : ControlForm
Private earthquakesToLayer$
earthquakesToLayer$(earthquakes: GeoJSON.FeatureCollection<GeoJSON.Point | EarthquakeFeatureProperties>)
Parameters :
Name Type Optional
earthquakes GeoJSON.FeatureCollection<GeoJSON.Point | EarthquakeFeatureProperties> No
Returns : Observable<GeoJSONLayer>
Private faultSectionToLayers
faultSectionToLayers(fc: GeoJSON.FeatureCollection)
Parameters :
Name Type Optional
fc GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon> No
Returns : GeoJSONLayer
Private featureTypeDisplay
featureTypeDisplay(featureType: FeatureType)
Parameters :
Name Type Optional
featureType FeatureType No
Returns : string
Private featureTypeToLayer
featureTypeToLayer(fc: GeoJSON.FeatureCollection, featureType: FeatureType)

Returns a layer of a feature type.

Parameters :
Name Type Optional Description
fc GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon> No

The GeoJSON feature collection

featureType FeatureType No

The feature type

Returns : GeoJSONLayer
Private featureTypeToPopupTemplate
featureTypeToPopupTemplate(fc: GeoJSON.FeatureCollection, display: string)
Parameters :
Name Type Optional
fc GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon> No
display string No
Returns : ___esri.PopupTemplateProperties
filterTilesByImt
filterTilesByImt(tiles: HazardTile[], form: ControlForm)

Returns the hazard tiles for a specific region, year, and IMT.

Parameters :
Name Type Optional Description
tiles HazardTile[] No

The hazard tiles

form ControlForm No

Control panel form values

Returns : HazardTile[]
filterTilesByRegion
filterTilesByRegion(tiles: HazardTile[], form: ControlForm)

Returns the hazard tiles for a specific region.

Parameters :
Name Type Optional Description
tiles HazardTile[] No

The hazard tiles

form ControlForm No

Control panel form values

Returns : HazardTile[]
filterTilesByYear
filterTilesByYear(tiles: HazardTile[], form: ControlForm)

Returns the hazard tiles for a specific region and year.

Parameters :
Name Type Optional Description
tiles HazardTile[] No

The hazard tiles

form ControlForm No

Control panel form values

Returns : HazardTile[]
findTile
findTile(tiles: HazardTile[], form: ControlForm)

Returns the hazard tile for a specific region, year, IMT, and return period.

Parameters :
Name Type Optional Description
tiles HazardTile[] No

The hazard tiles

form ControlForm No

Control panel form values

Returns : HazardTile
Private getEarthquakes
getEarthquakes()
Returns : void
Private getFeatures
getFeatures(featureType: FeatureType)
Parameters :
Name Type Optional
featureType FeatureType No
Returns : void
Private getNshmBoundary
getNshmBoundary()
Returns : void
Private getTestSites
getTestSites()
Returns : void
init
init()

Initialize the application.

Returns : void
Private initialControlSet
initialControlSet(control: FormControl, defaultValue: boolean, queryValue?: string)
Parameters :
Name Type Optional
control FormControl<boolean> No
defaultValue boolean No
queryValue string Yes
Returns : void
Private initialFormSet
initialFormSet()
Returns : void
initialState
initialState()

Application initial state.

Returns : AppState
Private map
map()
Returns : ArcGisMap
Private nshmIdFromString
nshmIdFromString(nshm: string)

Returns the NshmId from a string.

Parameters :
Name Type Optional Description
nshm string No

The NSHM string

Returns : NshmId
onMapStyleChange
onMapStyleChange(mapStyle: "2d" | "3d")
Parameters :
Name Type Optional
mapStyle "2d" | "3d" No
Returns : void
onMapViewChange
onMapViewChange()
Returns : void
resetLayers
resetLayers()

Reset the style of all layers.

Returns : void
Private resetSourceFeatureControls
resetSourceFeatureControls()
Returns : void
resetState
resetState()
Returns : void
sourceFeatureControl
sourceFeatureControl(featureType: FeatureType, featureTypes: string[], control: AbstractControl)

Check to see if feature type is included in the list of supported feature type to enable or disable the control form field.

Parameters :
Name Type Optional Description
featureType FeatureType No

The feature type

featureTypes string[] No

List of all feature types

control AbstractControl<boolean> No

Feature type form state

Returns : void
toggleLayer
toggleLayer(layer: Layer, checked: boolean, spinnerText: string)
Parameters :
Name Type Optional Default value
layer Layer No
checked boolean No
spinnerText string No 'Adding layer ...'
Returns : void
Private toHazardTiles
toHazardTiles(response: ArcServiceResponse)

Convert ArcGIS service response to HazardTiles.

Example JSON response:

Example :
{
"currentVersion": 10.61,
 "services": [
 {
   "name": "haz/AK1hz050_1999",
   "type": "MapServer"
 }
}
Parameters :
Name Type Optional Description
response ArcServiceResponse No

The ArcGIS service response

Returns : HazardTile[]
Private toImt
toImt(imt: string)

Returns the Imt corresponding to the IMT string in the map name.

Parameters :
Name Type Optional Description
imt string No

The IMT string

Returns : Imt
Private toNshmId
toNshmId(region: string, year: number)

Convert the region and year from the map name into a corresponding NshmId.

Parameters :
Name Type Optional Description
region string No

The region from map name

year number No

The year from map name

Returns : NshmId
Private toReturnPeriod
toReturnPeriod(returnPeriod: string)

Returns the ReturnPeriod from the corresponding string.

Parameters :
Name Type Optional Description
returnPeriod string No

The return period string from ArcGIS map name

Returns : ReturnPeriod
updateState
updateState(state: Partial<AppState>)
Parameters :
Name Type Optional
state Partial<AppState> No
Returns : void
Private updateUrl
updateUrl()
Returns : void

Properties

Readonly formGroup
Default value : this.formBuilder.group<ControlForm>(this.defaultFormValues())
map2d
Default value : new ArcGisMap()
map3d
Default value : new ArcGisMap()
Private minMw
Type : number
Default value : 2.5
Private nshmpHazWs
Default value : environment.webServices.nshmpHazWs

nshmp-haz-ws web config

Private nshmsEndpoint
Default value : this.nshmpHazWs.services.nshms

Endpoint to NSHMs

Private services
Default value : this.nshmpHazWs.services.curveServices

nshmp-haz web services

Readonly state
Default value : signal<AppState>(this.initialState())

Application state

Accessors

availableModels
getavailableModels()

Returns the avialable models, as Parameters, observable.

Returns : Signal<Parameter[]>
hazardTiles
gethazardTiles()

Returns the hazard tiles.

layers
getlayers()

Returns the layers observable.

Returns : Signal<Layers>
nshmService
getnshmService()
sourceFeaturesUsage
getsourceFeaturesUsage()
treesUsageResponse
gettreesUsageResponse()

Returns the trees usage.

Returns : Signal<SourceLogicTreesUsage>
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(),
    );
  }
}

results matching ""

    No results matching ""