File

services/app.service.ts

Description

ArcGIS hazard service response.

Index

Properties

Properties

services
services: ArcService[]
Type : ArcService[]

Hazard maps

import {Location as LocationService} from '@angular/common';
import {HttpClient, HttpParams} from '@angular/common/http';
import {computed, DestroyRef, Inject, Injectable, LOCALE_ID, Signal, signal} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {AbstractControl, FormBuilder, FormControl, Validators} from '@angular/forms';
import {ActivatedRoute} from '@angular/router';
import Point from '@arcgis/core/geometry/Point';
import GeoJSONLayer from '@arcgis/core/layers/GeoJSONLayer';
import Layer from '@arcgis/core/layers/Layer';
import TileLayer from '@arcgis/core/layers/TileLayer';
import ArcGisMap from '@arcgis/core/Map';
import SimpleRenderer from '@arcgis/core/renderers/SimpleRenderer';
import SizeVariable from '@arcgis/core/renderers/visualVariables/SizeVariable';
import SimpleFillSymbol from '@arcgis/core/symbols/SimpleFillSymbol.js';
import SimpleLineSymbol from '@arcgis/core/symbols/SimpleLineSymbol.js';
import SimpleMarkerSymbol from '@arcgis/core/symbols/SimpleMarkerSymbol';
import {FormatLatitudePipe, FormatLongitudePipe, HazardService} from '@ghsc/nshmp-lib-ng/hazard';
import {ArcgisService} from '@ghsc/nshmp-lib-ng/map';
import {NshmpService, nshmpUtils, ReturnPeriod} from '@ghsc/nshmp-lib-ng/nshmp';
import {SpinnerService} from '@ghsc/nshmp-template';
import {
  FeaturesUsageResponse,
  FeatureType,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/features-service';
import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service';
import {
  SourceLogicTreesMetadata,
  SourceLogicTreesUsage,
} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/source-logic-trees-service';
import {Imt} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
import {NshmId, nshmRegion, nshmYear} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
import deepEqual from 'deep-equal';
import {environment} from 'projects/nshmp-apps/src/environments/environment';
import {AppServiceModel} from 'projects/nshmp-apps/src/shared/models/app-service.model';
import {apps} from 'projects/nshmp-apps/src/shared/utils/applications.utils';
import {catchError, forkJoin, from, map, mergeMap, Observable, of} from 'rxjs';

import {ControlForm, LatestEarthquakeTime} from '../models/control-form.model';
import {EarthquakeFeatureProperties} from '../models/earthquake-props.model';
import {HazardTile} from '../models/hazard-tile.model';
import {LayerId, Layers} from '../models/layers.model';
import {AppState} from '../models/state.model';

/**
 * ArcGIS hazard service info.
 */
interface ArcService {
  /** Name hazard map */
  name: string;
  /** Map type */
  type: string;
}

/**
 * ArcGIS hazard service response.
 */
interface ArcServiceResponse {
  /** Hazard maps */
  services: ArcService[];
}

interface Query {
  /** Whether devollement toggle is on */
  hasDecollementLayer: string;
  /** Whether latest earthquake toggle is on */
  hasEarthquakesLayer: string;
  /** Whether fault sections toggle is on */
  hasFaultSectionLayer: string;
  /** Whether zone source toggle is on */
  hasHazardTiles: string;
  /** Whether interface sections toggle is on */
  hasInterfaceSectionsLayer: string;
  /** Whether NSHM boundary toggle is on*/
  hasNshmBoundaryLayer: string;
  /** Whether test sites toggle is on */
  hasTestSitesLayer: string;
  /** Whether zone sources is toggled on */
  hasZoneSourcesLayer: string;
  /** IMT for hazard map tile */
  hazardTileImt: string;
  /** Return period for hazard map tile */
  hazardTileReturnPeriod: ReturnPeriod;
  /** Year for hazard map tile */
  hazardTileYear: string;
  /** Latest earthquake feed time frame */
  latestEarthquakeTime: LatestEarthquakeTime;
  /** The NSHM */
  model: NshmId;
  /** ArcGIS overlay opactiy */
  overlayOpacity: string;
}

const FAULT_POLY_ID = `${FeatureType.FAULT}_POLY`;

/**
 * Entrypoint to store for data mapping application.
 */
@Injectable({
  providedIn: 'root',
})
export class AppService implements AppServiceModel<AppState, ControlForm> {
  readonly formGroup = this.formBuilder.group<ControlForm>(this.defaultFormValues());

  /** Application state */
  readonly state = signal<AppState>(this.initialState());

  map2d = new ArcGisMap();
  map3d = new ArcGisMap();

  private minMw = 2.5;

  /** nshmp-haz-ws web config */
  private nshmpHazWs = environment.webServices.nshmpHazWs;

  /** nshmp-haz web services */
  private services = this.nshmpHazWs.services.curveServices;

  /** Endpoint to NSHMs */
  private nshmsEndpoint = this.nshmpHazWs.services.nshms;

  constructor(
    private formBuilder: FormBuilder,
    private spinnerService: SpinnerService,
    private nshmpService: NshmpService,
    private hazardService: HazardService,
    private http: HttpClient,
    private route: ActivatedRoute,
    private location: LocationService,
    private arcgisService: ArcgisService,
    private destroyRef: DestroyRef,
    @Inject(LOCALE_ID) private localId: string,
  ) {
    this.addValidators();
  }

  /**
   * Returns the avialable models, as `Parameter`s, observable.
   */
  get availableModels(): Signal<Parameter[]> {
    return computed(() => this.state().availableModels);
  }

  /**
   * Returns the hazard tiles.
   */
  get hazardTiles(): Signal<HazardTile[]> {
    return computed(() => this.state().hazardTiles);
  }

  /**
   * Returns the layers observable.
   */
  get layers(): Signal<Layers> {
    return computed(() => this.state().layers);
  }

  get nshmService(): Signal<NshmMetadata> {
    return computed(() =>
      this.state().nshmServices.find(nshm => nshm.model === this.formGroup.getRawValue().model),
    );
  }

  get sourceFeaturesUsage(): Signal<FeaturesUsageResponse> {
    return computed(() =>
      this.state().sourceFeaturesUsages.get(this.formGroup.getRawValue().model),
    );
  }

  /**
   * Returns the trees usage.
   */
  get treesUsageResponse(): Signal<SourceLogicTreesUsage> {
    return computed(() =>
      this.state().treesUsageResponses?.get(this.formGroup.getRawValue().model),
    );
  }

  addValidators(): void {
    this.formGroup.controls.model.addValidators(control => Validators.required(control));
  }

  /**
   * Call latest earthquakes service.
   */
  callEarthquakesService(layer?: Layer): void {
    const checked = this.formGroup.getRawValue().hasEarthquakesLayer;

    if (layer) {
      this.toggleLayer(layer, checked, 'Adding latest earthquakes ...');
    } else if (checked === undefined || checked) {
      this.getEarthquakes();
    }
  }

  /**
   * Call the features service with a specific feature type.
   *
   * @param featureType The feature type
   */
  callFeaturesService(featureType: FeatureType, layer: Layer, checked: boolean): void {
    if (layer) {
      this.toggleLayer(layer, checked, `Adding ${this.featureTypeDisplay(featureType)} ...`);
    } else if (checked) {
      this.getFeatures(featureType);
    } else {
      this.arcgisService.filterLayer(this.map(), featureType);

      if (featureType === FeatureType.FAULT) {
        this.arcgisService.filterLayer(this.map(), FAULT_POLY_ID);
      }
    }
  }

  callFeaturesUsages$(): Observable<void> {
    const usages$ = this.state().nshmServices.map(
      nshm =>
        this.nshmpService
          .callService$<FeaturesUsageResponse>(`${nshm.url}${this.services.features}`)
          .pipe(
            map(sourceFeaturesUsage => {
              return {
                nshm,
                sourceFeaturesUsage,
              };
            }),
          ),
      catchError((error: Error) => this.nshmpService.throwError$(error)),
    );

    return forkJoin(usages$).pipe(
      map(usages => {
        const sourceFeaturesUsages = new Map<string, FeaturesUsageResponse>();

        usages.forEach(usage => {
          sourceFeaturesUsages.set(usage.nshm.model, usage.sourceFeaturesUsage);
        });

        this.updateState({
          sourceFeaturesUsages,
        });

        this.resetSourceFeatureControls();
      }),
    );
  }

  /**
   * Call the map service to get NSHM map and boundary.
   */
  callService(layer?: Layer): void {
    const checked = this.formGroup.getRawValue().hasNshmBoundaryLayer;

    if (layer) {
      this.toggleLayer(layer, checked, 'Adding NSHM boundary ...');
    } else if (checked !== false) {
      this.getNshmBoundary();
    }
  }

  /**
   * Call test sites service.
   */
  callTestSitesService(layer?: Layer): void {
    const checked = this.formGroup.getRawValue().hasTestSitesLayer;

    if (layer) {
      this.toggleLayer(layer, checked, 'Adding test sites ...');
    } else if (checked) {
      this.getTestSites();
    }
  }

  /**
   * Create the hazard layer from hazard tile info.
   *
   * @param hazardTile The hazard tile
   */
  createHazardLayer(layer?: Layer): void {
    const checked = this.formGroup.getRawValue().hasHazardTiles;

    if (layer) {
      this.toggleLayer(layer, checked, 'Adding hazard layer ...');
    } else if (checked) {
      const hazardTile = this.findTile(this.hazardTiles(), this.formGroup.getRawValue());
      const opacity = this.formGroup.getRawValue().overlayOpacity;

      if (hazardTile === undefined) {
        return null;
      }

      const spinnerRef = this.spinnerService.show('Adding hazard ...');

      this.arcgisService.filterLayer(this.map(), LayerId.HAZARD);

      const url = `${environment.webServices.hazardTiles}/${hazardTile.mapName}/MapServer`;

      const hazardLayer = new TileLayer({
        id: LayerId.HAZARD,
        opacity: opacity / 100,
        url,
      });

      this.updateState({
        layers: {
          ...this.layers(),
          hazardLayer,
        },
      });

      this.map().add(hazardLayer);
      spinnerRef.close();
    }
  }

  /**
   * Default control panel form field values.
   */
  defaultFormValues(): ControlForm {
    return {
      hasDecollementLayer: null,
      hasEarthquakesLayer: null,
      hasFaultSectionLayer: null,
      hasHazardTiles: null,
      hasInterfaceSectionsLayer: null,
      hasNshmBoundaryLayer: true,
      hasTestSitesLayer: null,
      hasTracesOnlyFaultSections: false,
      hasTracesOnlyInterfaceSections: false,
      hasZoneSourcesLayer: null,
      hazardTileImt: null,
      hazardTileReturnPeriod: null,
      hazardTileYear: null,
      latestEarthquakeTime: LatestEarthquakeTime.WEEK,
      mapStyle: '2d',
      model: null,
      overlayOpacity: 60,
    };
  }

  /**
   * Returns the hazard tiles for a specific region, year, and IMT.
   *
   * @param tiles The hazard tiles
   * @param form Control panel form values
   */
  filterTilesByImt(tiles: HazardTile[], form: ControlForm): HazardTile[] {
    if (form.model === null) {
      return [];
    }
    return this.filterTilesByYear(tiles, form)
      ?.filter(tile => tile.imt === form.hazardTileImt)
      .sort((a, b) => a.returnPeriod - b.returnPeriod);
  }

  /**
   * Returns the hazard tiles for a specific region.
   *
   * @param tiles The hazard tiles
   * @param form Control panel form values
   */
  filterTilesByRegion(tiles: HazardTile[], form: ControlForm): HazardTile[] {
    if (form.model === null) {
      return [];
    }

    return tiles
      ?.filter(tile => nshmRegion(tile.nshm) === nshmRegion(form.model))
      .sort((a, b) => nshmYear(a.nshm) - nshmYear(b.nshm));
  }

  /**
   * Returns the hazard tiles for a specific region and year.
   *
   * @param tiles The hazard tiles
   * @param form Control panel form values
   */
  filterTilesByYear(tiles: HazardTile[], form: ControlForm): HazardTile[] {
    if (form.model === null) {
      return [];
    }
    return this.filterTilesByRegion(tiles, form)
      .filter(tile => nshmYear(tile.nshm) === form.hazardTileYear)
      .sort((a, b) => (a.imt.toLowerCase() > b.imt.toLowerCase() ? 1 : -1));
  }

  /**
   * Returns the hazard tile for a specific region, year, IMT, and return period.
   *
   * @param tiles The hazard tiles
   * @param form Control panel form values
   */
  findTile(tiles: HazardTile[], form: ControlForm): HazardTile {
    return this.filterTilesByImt(tiles, form)?.find(
      tile => tile.returnPeriod === form.hazardTileReturnPeriod,
    );
  }

  /**
   * Initialize the application.
   */
  init(): void {
    const spinnerRef = this.spinnerService.show(SpinnerService.MESSAGE_METADATA);

    this.callTreesUsage()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .pipe(
        mergeMap(() => forkJoin([this.callFeaturesUsages$(), this.callHazardTilesUsage()])),
        catchError(error => {
          this.initialFormSet();
          spinnerRef.close();
          console.error(error);
          return of();
        }),
      )
      .subscribe(() => {
        this.initialFormSet();
        spinnerRef.close();
      });
  }

  /**
   * Application initial state.
   */
  initialState(): AppState {
    const treesUsageResponses: Map<string, SourceLogicTreesUsage> = new Map();
    treesUsageResponses.set(NshmId.CONUS_2018, null);

    return {
      availableModels: [],
      hazardTiles: [],
      layers: {
        decollementLayer: null,
        earthquakesLayer: null,
        faultSectionLayer: null,
        faultSectionPolygonLayer: null,
        hazardLayer: null,
        interfaceSectionsLayer: null,
        nshmBoundaryLayer: null,
        testSitesLayer: null,
        zoneSourcesLayer: null,
      },
      nshmServices: [],
      sourceFeaturesUsages: new Map(),
      treesUsageResponses,
    };
  }

  onMapStyleChange(mapStyle: '2d' | '3d'): void {
    if (mapStyle === '2d') {
      this.map2d = new ArcGisMap();
    } else {
      this.map3d = new ArcGisMap();
    }
  }

  onMapViewChange(): void {
    const layers = this.layers();

    const values = this.formGroup.getRawValue();
    this.callFeaturesService(
      FeatureType.DECOLLEMENT,
      layers.decollementLayer,
      values.hasDecollementLayer,
    );
    this.callFeaturesService(
      FeatureType.FAULT,
      layers.faultSectionLayer,
      values.hasFaultSectionLayer,
    );
    this.callFeaturesService(
      FeatureType.INTERFACE,
      layers.interfaceSectionsLayer,
      values.hasInterfaceSectionsLayer,
    );
    this.callFeaturesService(FeatureType.ZONE, layers.zoneSourcesLayer, values.hasZoneSourcesLayer);

    this.callService(layers.nshmBoundaryLayer);
    this.callTestSitesService(layers.testSitesLayer);
    this.callEarthquakesService(layers.earthquakesLayer);
    this.createHazardLayer(layers.hazardLayer);
  }

  /**
   * Reset the style of all layers.
   */
  resetLayers(): void {
    Object.values(LayerId).forEach(id => this.arcgisService.filterLayer(this.map(), id));
  }

  resetState(): void {
    const values = this.formGroup.getRawValue();
    const hazardTiles = this.filterTilesByRegion(this.hazardTiles(), values);
    const defaultTile = [...hazardTiles].pop();
    this.resetSourceFeatureControls();
    this.resetLayers();

    this.updateState({
      layers: {
        decollementLayer: null,
        earthquakesLayer: null,
        faultSectionLayer: null,
        faultSectionPolygonLayer: null,
        hazardLayer: null,
        interfaceSectionsLayer: null,
        nshmBoundaryLayer: null,
        testSitesLayer: null,
        zoneSourcesLayer: null,
      },
    });

    if (hazardTiles.length > 0) {
      this.formGroup.controls.hasHazardTiles.enable();
    } else {
      this.formGroup.controls.hasHazardTiles.disable();
    }

    this.formGroup.patchValue({
      hasEarthquakesLayer: false,
      hasHazardTiles: false,
      hasTestSitesLayer: false,
      hazardTileImt: defaultTile?.imt,
      hazardTileReturnPeriod: defaultTile?.returnPeriod,
      hazardTileYear: defaultTile ? nshmYear(defaultTile?.nshm) : undefined,
    });

    this.callService();
  }

  /**
   * Check to see if feature type is included in the list of supported feature type to
   * enable or disable the control form field.
   *
   * @param featureType The feature type
   * @param featureTypes List of all feature types
   * @param control Feature type form state
   */
  sourceFeatureControl(
    featureType: FeatureType,
    featureTypes: string[],
    control: AbstractControl<boolean>,
  ): void {
    control.setValue(false);

    if (featureTypes.includes(featureType)) {
      control.enable();
    } else {
      control.disable();
    }
  }

  toggleLayer(layer: Layer, checked: boolean, spinnerText = 'Adding layer ...'): void {
    if (checked) {
      const spinnerRef = this.spinnerService.show(spinnerText);
      this.map().add(layer);
      spinnerRef.close();
    } else {
      this.arcgisService.filterLayer(this.map(), layer.id);
    }
  }

  updateState(state: Partial<AppState>): void {
    const updatedState = {
      ...this.state(),
      ...state,
    };

    if (!deepEqual(updatedState, this.state())) {
      this.state.set({
        ...this.state(),
        ...state,
      });
    }
  }

  private callHazardTilesUsage(): Observable<void> {
    return this.http.get<ArcServiceResponse>(`${environment.webServices.hazardTiles}?f=pjson`).pipe(
      map(response => {
        const hazardTiles = this.toHazardTiles(response);
        const controls = this.formGroup.controls;

        const hazardTilesForRegion = this.filterTilesByRegion(
          hazardTiles,
          this.formGroup.getRawValue(),
        );

        if (hazardTilesForRegion.length > 0) {
          controls.hasHazardTiles.enable();
        } else {
          controls.hasHazardTiles.disable();
        }

        const defaultTile = [...hazardTilesForRegion].pop();

        if (defaultTile) {
          this.formGroup.patchValue({
            hasHazardTiles: false,
            hazardTileImt: defaultTile?.imt,
            hazardTileReturnPeriod: defaultTile?.returnPeriod,
            hazardTileYear: nshmYear(defaultTile?.nshm),
          });
        }

        this.updateState({
          hazardTiles,
        });
      }),
    );
  }

  private callTreesUsage(): Observable<void> {
    return this.hazardService
      .dynamicNshms$<SourceLogicTreesMetadata>(
        `${this.nshmpHazWs.url}${this.nshmsEndpoint}`,
        environment.webServices.nshmpHazWs.services.curveServices.trees,
      )
      .pipe(
        map(({models, nshmServices, usageResponses}) => {
          this.updateState({
            availableModels: models,
            nshmServices,
            treesUsageResponses: usageResponses,
          });
        }),
        catchError((error: Error) => {
          return this.nshmpService.throwError$(error);
        }),
      );
  }

  private createEarthquakeLayer(
    earthquakes: GeoJSON.FeatureCollection<GeoJSON.Point, EarthquakeFeatureProperties>,
  ): GeoJSONLayer {
    const renderer = new SimpleRenderer({
      symbol: new SimpleMarkerSymbol({
        color: [255, 159, 0, 0.8],
        style: 'circle',
      }),
      visualVariables: [
        new SizeVariable({
          field: 'mag',
          maxDataValue: 10,
          maxSize: 35,
          minDataValue: this.minMw,
          minSize: 5,
        }),
      ],
    });

    const earthquakeLayer = new GeoJSONLayer({
      elevationInfo: {
        mode: 'on-the-ground',
      },
      id: LayerId.EARTHQUAKE,
      popupTemplate: {
        content: (event: __esri.GraphicHit) => {
          const id = event.graphic.getObjectId();
          const feature = earthquakes.features.find(feature => feature.id === id);
          const [latitude, longitude, depth] = feature.geometry.coordinates;
          const time = new Date(feature.properties.time);
          const latFormat = new FormatLatitudePipe(this.localId).transform(latitude);
          const lonFormat = new FormatLongitudePipe(this.localId).transform(longitude);

          const a = `
              <table>
                <tr>
                  <td class="text-bold">Time</td>
                  <td>${time.toUTCString()}}</td>
                </tr>
                <tr>
                  <td class="padding-right-3 text-bold">Magnitude</td>
                  <td>${feature.properties.mag}</td>
                </tr>
                <tr>
                  <td class="text-bold">Location</td>
                  <td>${lonFormat}, ${latFormat}</td>
                </tr>
                <tr>
                  <td class="text-bold">Depth</td>
                  <td>${depth} km</td>
                </tr>
              </table>
            `;
          return a;
        },
        returnGeometry: true,
        title: '{title}',
      },
      renderer,
      url: this.arcgisService.geoJsonToUrl(earthquakes),
    });

    return earthquakeLayer;
  }

  private earthquakesToLayer$(
    earthquakes: GeoJSON.FeatureCollection<GeoJSON.Point, EarthquakeFeatureProperties>,
  ): Observable<GeoJSONLayer> {
    return from(this.layers().nshmBoundaryLayer.when()).pipe(
      map(() => {
        const features = earthquakes.features.filter(feature => {
          const coords = feature.geometry.coordinates;
          return this.layers().nshmBoundaryLayer.fullExtent.contains(
            new Point({
              latitude: coords[1],
              longitude: coords[0],
            }),
          );
        });

        earthquakes = {
          ...earthquakes,
          features,
        };

        this.arcgisService.filterLayer(this.map(), LayerId.EARTHQUAKE);
        const layer = this.createEarthquakeLayer(earthquakes);
        this.map().add(layer);
        return layer;
      }),
    );
  }

  private featureTypeDisplay(featureType: FeatureType): string {
    switch (featureType) {
      case FeatureType.DECOLLEMENT:
        return 'Décollement Sections';
      case FeatureType.FAULT:
        return 'Fault Sections';
      case FeatureType.INTERFACE:
        return 'Interface Sections';
      case FeatureType.ZONE:
        return 'Zone Sources';
      default:
        return featureType.toString();
    }
  }

  /**
   * Returns a layer of a feature type.
   *
   * @param fc The GeoJSON feature collection
   * @param featureType The feature type
   * @returns
   */
  private featureTypeToLayer(
    fc: GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon>,
    featureType: FeatureType,
  ): GeoJSONLayer {
    let color = [0, 0, 0, 0];
    this.arcgisService.filterLayer(this.map(), featureType);
    let display = '';

    switch (featureType) {
      case FeatureType.DECOLLEMENT:
        color = [128, 0, 128];
        display = 'Décollement Sections';
        break;
      case FeatureType.FAULT:
        this.arcgisService.filterLayer(this.map(), FAULT_POLY_ID);
        return this.faultSectionToLayers(fc);
      case FeatureType.INTERFACE:
        color = [0, 138, 0];
        display = 'Interface Sections';
        break;
      case FeatureType.ZONE:
        color = [0, 0, 255];
        display = 'Zone Sources';

        fc.features.sort((featureA, featureB) => {
          const a = featureA.geometry.coordinates[0];
          const b = featureB.geometry.coordinates[0];

          if (a.length > b.length) {
            return -1;
          } else if (a.length < b.length) {
            return 1;
          }
          return 0;
        });

        fc.features = fc.features.map((feature, index) => {
          feature.id = index;
          return feature;
        });

        break;
      default:
        throw new Error(`Feature type [${featureType}] not allowed`);
    }

    const {hasTracesOnlyInterfaceSections} = this.formGroup.getRawValue();

    const renderer = new SimpleRenderer({
      symbol:
        featureType === FeatureType.ZONE ||
        (featureType === FeatureType.INTERFACE && !hasTracesOnlyInterfaceSections)
          ? new SimpleFillSymbol({
              color: [...color, 0.2],
              outline: {
                color,
                width: 2,
              },
            })
          : new SimpleLineSymbol({
              color,
              width: 2,
            }),
    });

    const geoJsonLayer = new GeoJSONLayer({
      elevationInfo: {
        mode: 'on-the-ground',
      },
      id: featureType,
      popupTemplate: this.featureTypeToPopupTemplate(fc, display),
      renderer,
      url: this.arcgisService.geoJsonToUrl(fc),
    });

    this.map().add(geoJsonLayer);

    return geoJsonLayer;
  }

  private featureTypeToPopupTemplate(
    fc: GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon>,
    display: string,
  ): __esri.PopupTemplateProperties {
    return {
      content: (event: __esri.GraphicHit) => {
        const feature = fc.features.find(feature => feature.id === event.graphic.getObjectId());

        const rows = Object.entries(feature.properties).map(([key, value]) => {
          return `
                <tr>
                  <td class="padding-right-3 text-bold">${key}</td>
                  <td>${value}</td>
                </tr>
              `;
        });

        return `
            <table>
              <tr>
                <td class="text-bold padding-right-3">Source Type</td>
                <td>${display}</td>
              </tr>
              ${rows.join('\n')}
            </table>
          `;
      },
    };
  }

  private faultSectionToLayers(
    fc: GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon>,
  ): GeoJSONLayer {
    const {hasTracesOnlyFaultSections} = this.formGroup.getRawValue();
    const color = [255, 0, 0];
    const display = 'Fault Sections';
    const width = 1;

    const polyRenderer = new SimpleRenderer({
      symbol: new SimpleFillSymbol({
        color: [...color, 0.2],
        outline: {
          color,
          width,
        },
      }),
    });

    const lineRenderer = new SimpleRenderer({
      symbol: new SimpleLineSymbol({
        color,
        width,
      }),
    });

    let lineLayer: GeoJSONLayer;

    if (hasTracesOnlyFaultSections) {
      lineLayer = new GeoJSONLayer({
        elevationInfo: {
          mode: 'on-the-ground',
        },
        id: FeatureType.FAULT,
        popupTemplate: this.featureTypeToPopupTemplate(fc, display),
        renderer: lineRenderer,
        url: this.arcgisService.geoJsonToUrl(fc),
      });

      this.map().add(lineLayer);
    } else {
      const fcPoly = {
        ...fc,
        features: fc.features.filter(feature => feature.geometry.type === 'Polygon'),
      };

      const fcLine = {
        ...fc,
        features: fc.features.filter(feature => feature.geometry.type === 'LineString'),
      };

      lineLayer = new GeoJSONLayer({
        elevationInfo: {
          mode: 'on-the-ground',
        },
        id: FeatureType.FAULT,
        popupTemplate: this.featureTypeToPopupTemplate(fcLine, display),
        renderer: lineRenderer,
        url: this.arcgisService.geoJsonToUrl(fcLine),
      });

      const polyLayer = new GeoJSONLayer({
        elevationInfo: {
          mode: 'on-the-ground',
        },
        id: FAULT_POLY_ID,
        popupTemplate: this.featureTypeToPopupTemplate(fcPoly, display),
        renderer: polyRenderer,
        url: this.arcgisService.geoJsonToUrl(fcPoly),
      });

      this.map().add(polyLayer);
      this.map().add(lineLayer);

      this.updateState({
        layers: {
          ...this.layers(),
          faultSectionPolygonLayer: polyLayer,
        },
      });
    }

    return lineLayer;
  }

  private getEarthquakes(): void {
    const spinnerRef = this.spinnerService.show('Adding latest earthquakes ...');
    const time = this.formGroup.getRawValue().latestEarthquakeTime.toLowerCase();
    const service = environment.webServices.latestEarthquakes;
    const url = `${service}/${this.minMw}_${time}.geojson`;

    this.http
      .get<GeoJSON.FeatureCollection<GeoJSON.Point>>(url)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
      .subscribe(response => {
        this.earthquakesToLayer$(response)
          .pipe(takeUntilDestroyed(this.destroyRef))
          .subscribe(earthquakesLayer => {
            spinnerRef.close();

            this.updateState({
              layers: {
                ...this.layers(),
                earthquakesLayer,
              },
            });
          });
      });
  }

  private getFeatures(featureType: FeatureType): void {
    const spinnerRef = this.spinnerService.show(
      `Adding ${this.featureTypeDisplay(featureType)} ...`,
    );

    let polygons = false;

    switch (featureType) {
      case FeatureType.INTERFACE: {
        polygons = !this.formGroup.getRawValue().hasTracesOnlyInterfaceSections;
        break;
      }
      case FeatureType.FAULT: {
        polygons = !this.formGroup.getRawValue().hasTracesOnlyFaultSections;
        break;
      }
    }

    const params = new HttpParams().appendAll({
      featureType,
      polygons,
      raw: true,
    });
    const faultSectionsUrl = `${this.nshmService().url}${this.services.features}?${params.toString()}`;

    this.http
      .get<
        GeoJSON.FeatureCollection<GeoJSON.MultiLineString | GeoJSON.LineString | GeoJSON.Polygon>
      >(faultSectionsUrl)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
      .subscribe(serviceResponse => {
        const layer = this.featureTypeToLayer(serviceResponse, featureType);
        spinnerRef.close();

        switch (featureType) {
          case FeatureType.DECOLLEMENT: {
            this.updateState({
              layers: {
                ...this.layers(),
                decollementLayer: layer,
              },
            });
            break;
          }
          case FeatureType.FAULT: {
            this.updateState({
              layers: {
                ...this.layers(),
                faultSectionLayer: layer,
              },
            });
            break;
          }
          case FeatureType.INTERFACE: {
            this.updateState({
              layers: {
                ...this.layers(),
                interfaceSectionsLayer: layer,
              },
            });
            break;
          }
          case FeatureType.ZONE: {
            this.updateState({
              layers: {
                ...this.layers(),
                zoneSourcesLayer: layer,
              },
            });
            break;
          }
          default:
            throw new Error(`Feature type [${featureType}] not allowed`);
        }
      });
  }

  private getNshmBoundary(): void {
    const url = `${this.nshmService().url}${this.services.map}?raw=true`;

    this.http
      .get<GeoJSON.FeatureCollection>(url)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
      .subscribe(response => {
        this.arcgisService.filterLayer(this.map(), LayerId.NSHM);

        const nshmBorder = response.features.find(feature => feature.id !== 'Extents');

        const renderer = new SimpleRenderer({
          symbol: {
            color: [0, 0, 0, 0],
            outline: {
              color: 'black',
              width: 1,
            },
            style: 'solid',
            type: 'simple-fill',
          },
        });

        const layer = new GeoJSONLayer({
          id: LayerId.NSHM,
          renderer,
          url: this.arcgisService.geoJsonToUrl(nshmBorder),
        });

        this.updateState({
          layers: {
            ...this.layers(),
            nshmBoundaryLayer: layer,
          },
        });

        this.map().add(layer);
      });
  }

  private getTestSites(): void {
    const spinnerRef = this.spinnerService.show('Adding test sites');
    const url = `${this.nshmService().url}${this.services.sites}?raw=true`;

    this.http
      .get<GeoJSON.FeatureCollection>(url)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
      .subscribe(testSites => {
        this.arcgisService.filterLayer(this.map(), LayerId.TEST_SITES);

        const testSitesLayer = this.arcgisService.testSitesToLayer(testSites, LayerId.TEST_SITES);

        this.map().add(testSitesLayer);
        spinnerRef.close();

        this.updateState({
          layers: {
            ...this.layers(),
            testSitesLayer,
          },
        });
      });
  }

  private initialControlSet(
    control: FormControl<boolean>,
    defaultValue: boolean,
    queryValue?: string,
  ): void {
    if (control.enabled) {
      control.patchValue(nshmpUtils.queryParseBoolean(defaultValue, queryValue));
    }
  }

  private initialFormSet(): void {
    const query = this.route.snapshot.queryParams as Query;
    const defaultValues = this.defaultFormValues();
    const controls = this.formGroup.controls;

    this.formGroup.patchValue({
      model: query?.model ?? NshmId.CONUS_2018,
    });

    this.formGroup.patchValue({
      hasTestSitesLayer: nshmpUtils.queryParseBoolean(
        defaultValues.hasTestSitesLayer,
        query?.hasTestSitesLayer,
      ),
    });

    this.initialControlSet(
      controls.hasDecollementLayer,
      defaultValues.hasDecollementLayer,
      query?.hasDecollementLayer,
    );
    this.initialControlSet(
      controls.hasEarthquakesLayer,
      defaultValues.hasEarthquakesLayer,
      query?.hasEarthquakesLayer,
    );
    this.initialControlSet(
      controls.hasFaultSectionLayer,
      defaultValues.hasFaultSectionLayer,
      query?.hasFaultSectionLayer,
    );
    this.initialControlSet(
      controls.hasHazardTiles,
      defaultValues.hasHazardTiles,
      query?.hasHazardTiles,
    );
    this.initialControlSet(
      controls.hasInterfaceSectionsLayer,
      defaultValues.hasInterfaceSectionsLayer,
      query?.hasInterfaceSectionsLayer,
    );
    this.initialControlSet(
      controls.hasNshmBoundaryLayer,
      defaultValues.hasNshmBoundaryLayer,
      query?.hasNshmBoundaryLayer,
    );
    this.initialControlSet(
      controls.hasZoneSourcesLayer,
      defaultValues.hasZoneSourcesLayer,
      query?.hasZoneSourcesLayer,
    );

    this.formGroup.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => this.updateUrl());
  }

  private map(): ArcGisMap {
    return this.formGroup.getRawValue().mapStyle === '2d' ? this.map2d : this.map3d;
  }

  /**
   * Returns the `NshmId` from a string.
   *
   * @param nshm The NSHM string
   */
  private nshmIdFromString(nshm: string): NshmId {
    const nshmId = Object.values(NshmId).find(id => id.toString() === nshm);

    if (nshmId === undefined) {
      throw new Error(`NSHM ${nshm} not found`);
    }

    return nshmId;
  }

  private resetSourceFeatureControls(): void {
    const usage = this.sourceFeaturesUsage();

    if (usage) {
      const featureTypes = this.sourceFeaturesUsage().response.featureType.map(type => type.value);

      const controls = this.formGroup.controls;

      this.sourceFeatureControl(
        FeatureType.DECOLLEMENT,
        featureTypes,
        controls.hasDecollementLayer,
      );
      this.sourceFeatureControl(FeatureType.FAULT, featureTypes, controls.hasFaultSectionLayer);
      this.sourceFeatureControl(
        FeatureType.INTERFACE,
        featureTypes,
        controls.hasInterfaceSectionsLayer,
      );
      this.sourceFeatureControl(FeatureType.ZONE, featureTypes, controls.hasZoneSourcesLayer);
    }
  }

  /**
   * Convert ArcGIS service response to `HazardTile`s.
   *
   * Example JSON response:
   *  ```
   * {
   *  "currentVersion": 10.61,
   *   "services": [
   *   {
   *     "name": "haz/AK1hz050_1999",
   *     "type": "MapServer"
   *   }
   * }
   *  ```
   *
   * @param response The ArcGIS service response
   */
  private toHazardTiles(response: ArcServiceResponse): HazardTile[] {
    const services = response.services.filter(
      service => service.name.includes('hz') || service.name.includes('pga'),
    );

    const hazardTiles: HazardTile[] = [];

    // Example name: haz/USpga050_2008
    services.forEach(service => {
      const mapName = service.name.split('haz/').pop();
      const region = mapName.substring(0, 2);
      const imt = this.toImt(mapName.substring(2, 5));
      const returnPeriod = this.toReturnPeriod(mapName.substring(5, 8));
      const year = Number.parseInt(mapName.split('_').pop());
      const nshm = this.toNshmId(region, year);

      const hazardTile: HazardTile = {
        imt,
        mapName,
        nshm,
        returnPeriod,
      };

      hazardTiles.push(hazardTile);
    });

    return hazardTiles;
  }

  /**
   * Returns the `Imt` corresponding to the IMT string in the map name.
   *
   * @param imt The IMT string
   */
  private toImt(imt: string): Imt {
    switch (imt) {
      case 'pga':
        return Imt.PGA;
      case '5hz':
        return Imt.SA0P2;
      case '1hz':
        return Imt.SA1P0;
      default:
        throw new Error(`IMT [${imt}] not supported`);
    }
  }

  /**
   * Convert the region and year from the map name into a corresponding `NshmId`.
   * @param region The region from map name
   * @param year The year from map name
   * @returns
   */
  private toNshmId(region: string, year: number): NshmId {
    switch (region) {
      case 'AK':
        return this.nshmIdFromString(`${nshmRegion(NshmId.ALASKA_2023)}_${year}`);
      case 'HI':
        return this.nshmIdFromString(`${nshmRegion(NshmId.HAWAII_2021)}_${year}`);
      case 'US':
        return this.nshmIdFromString(`${nshmRegion(NshmId.CONUS_2023)}_${year}`);
      default:
        throw new Error(`Region [${region}] year [${year}] not supported`);
    }
  }

  /**
   * Returns the `ReturnPeriod` from the corresponding string.
   *
   * @param returnPeriod The return period string from ArcGIS map name
   */
  private toReturnPeriod(returnPeriod: string): ReturnPeriod {
    switch (returnPeriod) {
      // 10% in 50
      case '050':
        return ReturnPeriod.RP_475;
      // 2% in 50
      case '250':
        return ReturnPeriod.RP_2475;
      default:
        throw new Error(`Return period [${returnPeriod}] not supported`);
    }
  }

  private updateUrl(): void {
    this.location.replaceState(
      apps().source.modelMaps.routerLink,
      new HttpParams().appendAll(this.formGroup.getRawValue()).toString(),
    );
  }
}

results matching ""

    No results matching ""