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, zone: NgZone, route: ActivatedRoute, location: LocationService)
Parameters :
Name Type Optional
formBuilder FormBuilder No
spinnerService SpinnerService No
nshmpService NshmpService No
hazardService HazardService No
http HttpClient No
zone NgZone No
route ActivatedRoute No
location LocationService No

Methods

addValidators
addValidators()
Returns : void
baseLayer
baseLayer(baseLayer: MapBaseLayer)

Set the base layer on the map.

Parameters :
Name Type Optional Description
baseLayer MapBaseLayer No

The base layer to set

Returns : void
callEarthquakesService
callEarthquakesService()

Call latest earthquakes service.

Returns : void
callFeaturesService
callFeaturesService(featureType: FeatureType)

Call the features service with a specific feature type.

Parameters :
Name Type Optional Description
featureType FeatureType No

The feature type

Returns : void
callFeaturesUsages$
callFeaturesUsages$()
Returns : Observable<void>
Private callHazardTilesUsage
callHazardTilesUsage()
Returns : Observable<void>
callService
callService()

Call the map service to get NSHM map and boundary.

Returns : void
callTestSitesService
callTestSitesService()

Call test sites service.

Returns : void
Private callTreesUsage
callTreesUsage()
Returns : Observable<void>
createHazardLayer
createHazardLayer(hazardTile: HazardTile, opacity: number)

Create the hazard layer from hazard tile info.

Parameters :
Name Type Optional Description
hazardTile HazardTile No

The hazard tile

opacity number No
Returns : L.TileLayer
defaultFormValues
defaultFormValues()

Default control panel form field values.

Returns : ControlForm
Private defaultMapOptions
defaultMapOptions()

Default Leaflemt map options.

Returns : L.MapOptions
Private dimLayer
dimLayer(layer: L.GeoJSON)

Dim all features in a layer.

Parameters :
Name Type Optional Description
layer L.GeoJSON No

The layer to dim

Returns : void
dimLayers
dimLayers()

Dim all layers

Returns : void
Private earthquakesToLayer
earthquakesToLayer(earthquakes: GeoJSON.FeatureCollection)
Parameters :
Name Type Optional
earthquakes GeoJSON.FeatureCollection<GeoJSON.Point> No
Returns : L.GeoJSON
Private extendBounds
extendBounds(bounds: L.LatLngBounds, layers: Layers)

Returns the extended map bounds based on all layers.

Parameters :
Name Type Optional Description
bounds L.LatLngBounds No

The Leaflet map bounds

layers Layers No

Leaflet layers

Returns : L.LatLngBounds
faultsWeight
faultsWeight()
Returns : number
Private featureTypeToLayer
featureTypeToLayer(fc: GeoJSON.FeatureCollection, featureType: FeatureType)

Returns a Leaflet layer of a feature type.

Parameters :
Name Type Optional Description
fc GeoJSON.FeatureCollection No

The GeoJSON feature collection

featureType FeatureType No

The feature type

Returns : L.GeoJSON
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
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
mapRedraw
mapRedraw()

Redraw the map.

Returns : void
Private modelBounds
modelBounds(model: NshmId)

Returns the bounds for a specific NSHM.

Parameters :
Name Type Optional Description
model NshmId No

The NSHM

Returns : L.LatLngBounds
Private nshmIdFromString
nshmIdFromString(nshm: string)

Returns the NshmId from a string.

Parameters :
Name Type Optional Description
nshm string No

The NSHM string

Returns : NshmId
Private onEachFeatureSourceFeatures
onEachFeatureSourceFeatures(layer: L.GeoJSON, feature: GeoJSON.Feature, featureType: FeatureType)

Create tooltip with GeoJSON properties.

Parameters :
Name Type Optional Description
layer L.GeoJSON No

The Leaflet layer

feature GeoJSON.Feature No

The GeoJSON feature

featureType FeatureType No
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
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

Private faultZoomInWeight
Type : number
Default value : 4
Private faultZoomOutWeight
Type : number
Default value : 1
Private featuresHoverWeight
Type : number
Default value : 5
Readonly formGroup
Default value : this.formBuilder.group<ControlForm>( this.defaultFormValues(), )
Private nshmpHazWs
Default value : environment.webServices.nshmpHazWs

nshmp-haz-ws web config

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

Endpoint to NSHMs

Private opacity
Type : number
Default value : 0.6
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[]>
bounds
getbounds()

Returns the map bounds observable.

Returns : Signal<L.LatLngBounds>
earthquakeInfoPopupData
getearthquakeInfoPopupData()
hazardTiles
gethazardTiles()

Returns the hazard tiles.

infoPopupData
getinfoPopupData()

Returns the info popup data.

layers
getlayers()

Returns the Leaflet map layers observable.

Returns : Signal<Layers>
map
getmap()

Returns the Leaflet map observable.

Returns : Signal<L.Map>
setmap(map: L.Map)

Set the Leaflet map.

Parameters :
Name Type Optional Description
map L.Map No

The Leaflet map

Returns : void
mapOptions
getmapOptions()

Returns the Leaflet map options observable.

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

results matching ""

    No results matching ""