File

components/geo-disagg/geo-disagg.component.ts

Description

ArcGIS map with geographical disaggregation.

Metadata

Index

Properties
Methods

Constructor

constructor(service: AppService, arcgisService: ArcgisService, destroyRef: DestroyRef)
Parameters :
Name Type Optional
service AppService No
arcgisService ArcgisService No
destroyRef DestroyRef No

Methods

Private color
color(sourceType: string)
Parameters :
Name Type Optional
sourceType string No
Returns : any
Private contributionGraphic
contributionGraphic(disaggSource: DisaggSource, shift: number)
Parameters :
Name Type Optional
disaggSource DisaggSource No
shift number No
Returns : Graphic
Private createBars
createBars(serviceResponse: DisaggResponse)
Parameters :
Name Type Optional
serviceResponse DisaggResponse No
Returns : GraphicsLayer
Private createFaultLayer
createFaultLayer(fc: GeoJSON.FeatureCollection, id: LayerId, color: number[])
Parameters :
Name Type Optional
fc GeoJSON.FeatureCollection No
id LayerId No
color number[] No
Returns : GeoJSONLayer
Private createFaultSections
createFaultSections(faultSections: GeoJSON.FeatureCollection)
Parameters :
Name Type Optional
faultSections GeoJSON.FeatureCollection No
Returns : void
Private createInterfaceSections
createInterfaceSections(interfaceSections: GeoJSON.FeatureCollection)
Parameters :
Name Type Optional
interfaceSections GeoJSON.FeatureCollection No
Returns : void
Private createSiteMarker
createSiteMarker()
Returns : GraphicsLayer
Private disaggSourceToGraphics
disaggSourceToGraphics(disaggSource: DisaggSource, sources: DisaggSource[])
Parameters :
Name Type Optional
disaggSource DisaggSource No
sources DisaggSource[] No
Returns : Graphic[]
Private endCapGraphic
endCapGraphic(disaggSource: DisaggSource, shift: number)
Parameters :
Name Type Optional
disaggSource DisaggSource No
shift number No
Returns : Graphic
Private faultSectionPopupContent
faultSectionPopupContent(graphic: Graphic, fc: GeoJSON.FeatureCollection)
Parameters :
Name Type Optional
graphic Graphic No
fc GeoJSON.FeatureCollection No
Returns : string
Private goToLayers
goToLayers(barLayer: GraphicsLayer, siteLayer: GraphicsLayer)
Parameters :
Name Type Optional
barLayer GraphicsLayer No
siteLayer GraphicsLayer No
Returns : void
onViewReady
onViewReady(scene: ArcgisScene)
Parameters :
Name Type Optional
scene ArcgisScene No
Returns : void
Private resetBarLayers
resetBarLayers()
Returns : void
Private responseToLayers
responseToLayers(response: DisaggResponse)
Parameters :
Name Type Optional
response DisaggResponse No
Returns : void
Private toDisaggSources
toDisaggSources(serviceResponse: DisaggResponse)
Parameters :
Name Type Optional
serviceResponse DisaggResponse No
Returns : DisaggSource[]
toggleLayer
toggleLayer(id: LayerId, checked: boolean)
Parameters :
Name Type Optional
id LayerId No
checked boolean No
Returns : void

Properties

Private endCapHeight
Type : number
Default value : 25
Private heightFactor
Type : number
Default value : 500
LayerId
Default value : LayerId
Private layers
Type : Record<string | Layer>
Default value : {}
legendSourceTypes
Default value : signal<LegendSource[]>([])
map
Default value : new Map()
mapEl
Default value : viewChild<NshmpMapArcgis3dComponent>('mapRef')
Private scene
Type : ArcgisScene | undefined
Private viewReady
Default value : false
Private width
Type : number
Default value : 2000
import '@arcgis/map-components/dist/components/arcgis-legend';
import '@arcgis/map-components/dist/components/arcgis-placement';

import {
  Component,
  CUSTOM_ELEMENTS_SCHEMA,
  DestroyRef,
  effect,
  signal,
  viewChild,
  ViewEncapsulation,
} from '@angular/core';
import {outputToObservable, takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {MatButtonModule} from '@angular/material/button';
import {MatExpansionModule} from '@angular/material/expansion';
import {MatIconModule} from '@angular/material/icon';
import {MatListModule} from '@angular/material/list';
import {MatMenuModule} from '@angular/material/menu';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import Graphic from '@arcgis/core/Graphic.js';
import GeoJSONLayer from '@arcgis/core/layers/GeoJSONLayer';
import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer';
import Layer from '@arcgis/core/layers/Layer';
import Map from '@arcgis/core/Map';
import SimpleRenderer from '@arcgis/core/renderers/SimpleRenderer.js';
import LineSymbol3D from '@arcgis/core/symbols/LineSymbol3D.js';
import LineSymbol3DLayer from '@arcgis/core/symbols/LineSymbol3DLayer.js';
import ObjectSymbol3DLayer from '@arcgis/core/symbols/ObjectSymbol3DLayer.js';
import PointSymbol3D from '@arcgis/core/symbols/PointSymbol3D.js';
import WebStyleSymbol from '@arcgis/core/symbols/WebStyleSymbol.js';
import {ArcgisScene} from '@arcgis/map-components/dist/components/arcgis-scene';
import {ArcgisService, defaultMapOptions, NshmpMapArcgis3dComponent} from '@ghsc/nshmp-lib-ng/map';
import {DisaggResponse, DisaggSource} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/disagg-service';
import {SourceType, sourceTypeFromPascalCase} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
import * as d3Color from 'd3-scale-chromatic';
import {from} from 'rxjs';

import {AppService} from '../../services/app.service';

interface LegendSource {
  color: string;
  sourceType: string;
}

enum LayerId {
  BARS = 'BARS',
  FAULT_SECTIONS = 'FAULT_SECTIONS',
  HOVER_TEXT = 'HOVER_TEXT',
  INTERFACE_SECTIONS = 'INTERFACE_SECTIONS',
  SITE_MARKER = 'SITE_MARKER',
}

interface Properties {
  name: string;
}

/**
 * ArcGIS map with geographical disaggregation.
 */
@Component({
  encapsulation: ViewEncapsulation.None,
  imports: [
    NshmpMapArcgis3dComponent,
    MatMenuModule,
    MatButtonModule,
    MatListModule,
    MatExpansionModule,
    MatIconModule,
    MatSlideToggleModule,
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  selector: 'app-geo-disagg',
  styleUrl: './geo-disagg.component.scss',
  templateUrl: './geo-disagg.component.html',
})
export class GeoDisaggComponent {
  legendSourceTypes = signal<LegendSource[]>([]);

  map = new Map();

  mapEl = viewChild<NshmpMapArcgis3dComponent>('mapRef');

  LayerId = LayerId;

  private layers: Record<string, Layer> = {};

  private endCapHeight = 25;
  private heightFactor = 500;
  private viewReady = false;
  private scene: ArcgisScene | undefined;
  private width = 2000;

  constructor(
    private service: AppService,
    private arcgisService: ArcgisService,
    private destroyRef: DestroyRef,
  ) {
    effect(() => {
      const serviceResponse = this.service.serviceResponse();
      this.responseToLayers(serviceResponse);
      this.arcgisService.filterLayer(this.map, LayerId.FAULT_SECTIONS);
      this.arcgisService.filterLayer(this.map, LayerId.INTERFACE_SECTIONS);

      if (serviceResponse) {
        this.createFaultSections(this.service.faultSections());
        this.createInterfaceSections(this.service.interfaceSections());
      }
    });
  }

  onViewReady(scene: ArcgisScene): void {
    this.scene = scene;
    this.viewReady = true;
  }

  toggleLayer(id: LayerId, checked: boolean): void {
    if (checked) {
      const layer = this.layers[id];
      if (layer) {
        this.map.add(layer);
      }
    } else {
      this.arcgisService.filterLayer(this.map, id);
    }
  }

  private color(sourceType: string) {
    const colors: Record<string, string> = {
      [SourceType.DECOLLEMENT]: d3Color.schemeCategory10[0],
      [SourceType.FAULT]: d3Color.schemeCategory10[1],
      [SourceType.FAULT_CLUSTER]: d3Color.schemeCategory10[2],
      [SourceType.FAULT_SYSTEM]: d3Color.schemeCategory10[3],
      [SourceType.GRID]: d3Color.schemeCategory10[4],
      [SourceType.INTERFACE]: d3Color.schemeCategory10[5],
      [SourceType.INTERFACE_CLUSTER]: d3Color.schemeCategory10[6],
      [SourceType.INTERFACE_GRID]: d3Color.schemeCategory10[7],
      [SourceType.INTERFACE_SYSTEM]: d3Color.schemeCategory10[8],
      [SourceType.SLAB]: d3Color.schemeCategory10[9],
      [SourceType.SLAB_GRID]: d3Color.schemePastel1[0],
      [SourceType.TOTAL]: 'black',
      [SourceType.ZONE]: d3Color.schemePastel1[1],
    };

    return colors[sourceTypeFromPascalCase(sourceType)] ?? 'red';
  }

  private contributionGraphic(disaggSource: DisaggSource, shift: number): Graphic {
    const height = disaggSource.contribution * this.heightFactor;

    const symbol = new PointSymbol3D({
      symbolLayers: [
        new ObjectSymbol3DLayer({
          anchor: 'bottom',
          depth: this.width,
          height,
          material: {
            color: this.color(disaggSource.source),
          },
          resource: {
            primitive: 'cylinder',
          },
          width: this.width,
        }),
      ],
    });

    const graphic = new Graphic({
      geometry: {
        latitude: disaggSource.latitude,
        longitude: disaggSource.longitude,
        type: 'point',
        z: shift,
      },
      popupTemplate: {
        content: [
          disaggSource.name,
          `Contribution: ${disaggSource.contribution}`,
          `m: ${disaggSource.m}`,
          `r: ${disaggSource.r}`,
          `type: ${disaggSource.type}`,
          `ε: ${disaggSource.ε}`,
        ].join('<br>'),
        title: disaggSource.source,
      },
      symbol,
    });

    return graphic;
  }

  private createBars(serviceResponse: DisaggResponse): GraphicsLayer {
    const sources: DisaggSource[] = [];

    const graphics = this.toDisaggSources(serviceResponse)
      .map(disaggSource => this.disaggSourceToGraphics(disaggSource, sources))
      .flat();

    const layer = new GraphicsLayer({
      graphics,
      id: LayerId.BARS,
    });

    this.legendSourceTypes.set(
      Array.from(new Set(sources.map(source => source.source))).map(source => ({
        color: this.color(source),
        sourceType: source,
      })),
    );

    this.map.add(layer);

    return layer;
  }

  private createFaultLayer(
    fc: GeoJSON.FeatureCollection,
    id: LayerId,
    color: number[],
  ): GeoJSONLayer {
    const renderer = new SimpleRenderer({
      symbol: new LineSymbol3D({
        symbolLayers: [
          new LineSymbol3DLayer({
            material: {
              color,
            },
            size: 3,
          }),
        ],
      }),
    });

    const layer = new GeoJSONLayer({
      elevationInfo: {
        mode: 'on-the-ground',
      },
      id,
      popupEnabled: true,
      popupTemplate: {
        content: (event: __esri.GraphicHit) => this.faultSectionPopupContent(event.graphic, fc),
        title: (event: __esri.GraphicHit) => {
          const id = event.graphic.getObjectId();
          const feature = fc.features.find(feature => feature.id === id);
          const properties = feature.properties as Properties;
          return properties?.name ?? '';
        },
      },
      renderer,
      url: this.arcgisService.geoJsonToUrl(fc),
    });

    this.map.add(layer);

    return layer;
  }

  private createFaultSections(faultSections: GeoJSON.FeatureCollection): void {
    if (faultSections) {
      const layer = this.createFaultLayer(faultSections, LayerId.FAULT_SECTIONS, [255, 0, 0, 0.8]);

      this.layers[LayerId.FAULT_SECTIONS] = layer;
    }
  }

  private createInterfaceSections(interfaceSections: GeoJSON.FeatureCollection): void {
    if (interfaceSections) {
      const layer = this.createFaultLayer(
        interfaceSections,
        LayerId.INTERFACE_SECTIONS,
        [0, 100, 0, 0.8],
      );

      this.layers[LayerId.INTERFACE_SECTIONS] = layer;
    }
  }

  private createSiteMarker(): GraphicsLayer {
    const values = this.service.formGroup.getRawValue();

    const symbol = new WebStyleSymbol({
      name: 'tear-pin-1',
      styleName: 'Esri2DPointSymbolsStyle',
    });

    const layer = new GraphicsLayer({
      graphics: [
        new Graphic({
          geometry: {
            latitude: values.latitude,
            longitude: values.longitude,
            type: 'point',
          },
          symbol,
        }),
      ],
      id: LayerId.SITE_MARKER,
    });

    this.map.add(layer);

    return layer;
  }

  private disaggSourceToGraphics(disaggSource: DisaggSource, sources: DisaggSource[]): Graphic[] {
    const height = disaggSource.contribution * this.heightFactor;

    const sameLocations = sources.filter(
      s => s.latitude === disaggSource.latitude && s.longitude === disaggSource.longitude,
    );

    const totalContribution = sameLocations.map(s => s.contribution).reduce((sum, x) => sum + x, 0);

    const endCapShift = this.endCapHeight * sameLocations.length;
    const shift = totalContribution * this.heightFactor + endCapShift;

    const graphic = this.contributionGraphic(disaggSource, shift);
    const graphicEndCap = this.endCapGraphic(disaggSource, shift + height);

    sources.push(disaggSource);
    return [graphic, graphicEndCap];
  }

  private endCapGraphic(disaggSource: DisaggSource, shift: number): Graphic {
    const symbol = new PointSymbol3D({
      symbolLayers: [
        new ObjectSymbol3DLayer({
          anchor: 'bottom',
          depth: this.width,
          height: this.endCapHeight,
          material: {
            color: [49, 49, 49, 0.5],
          },
          resource: {
            primitive: 'cylinder',
          },
          width: this.width,
        }),
      ],
    });

    const graphic = new Graphic({
      geometry: {
        latitude: disaggSource.latitude,
        longitude: disaggSource.longitude,
        type: 'point',
        z: shift,
      },
      symbol,
    });

    return graphic;
  }

  private faultSectionPopupContent(graphic: Graphic, fc: GeoJSON.FeatureCollection): string {
    const id = graphic.getObjectId();
    const feature = fc.features.find(feature => feature.id === id);

    const properties = Object.entries(feature.properties)
      .filter(([key]) => key !== 'mfd-tree' && key !== 'rate-map')
      .map(([key, value]: [string, string]) => {
        return ` <tr>
          <th style="padding-right: 1em">${key}</th>
          <td>${value}</td>
        </tr>
        `;
      });

    return `<table>
        <tbody>
          ${properties.join('')}
        </tbody>
      </table>
      `;
  }

  private goToLayers(barLayer: GraphicsLayer, siteLayer: GraphicsLayer): void {
    if (barLayer.graphics.length > 0) {
      from(
        this.scene.view.goTo({
          target: [...siteLayer.graphics, ...barLayer.graphics],
          tilt: 70,
        }),
      )
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe();
    } else {
      const values = this.service.formGroup.getRawValue();

      from(
        this.scene.view.goTo({
          center: [values.longitude, values.latitude],
          scale: 10_000_000,
        }),
      )
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe();
    }
  }

  private resetBarLayers(): void {
    Object.values(LayerId)
      .filter(id => id !== LayerId.FAULT_SECTIONS)
      .forEach(id => this.arcgisService.filterLayer(this.map, id));
  }

  private responseToLayers(response: DisaggResponse): void {
    this.resetBarLayers();

    if (response) {
      const siteLayer = this.createSiteMarker();
      this.layers[LayerId.SITE_MARKER] = siteLayer;
      const barLayer = this.createBars(response);
      this.layers[LayerId.BARS] = barLayer;

      if (this.viewReady) {
        from(this.scene.view.when())
          .pipe(takeUntilDestroyed(this.destroyRef))
          .subscribe(() => {
            this.goToLayers(barLayer, siteLayer);
          });
      } else {
        outputToObservable(this.mapEl().viewReady)
          .pipe(takeUntilDestroyed(this.destroyRef))
          .subscribe(scene => {
            from(scene.view.when())
              .pipe(takeUntilDestroyed(this.destroyRef))
              .subscribe(() => {
                this.viewReady = true;
                this.scene = scene;
                this.goToLayers(barLayer, siteLayer);
              });
          });
      }
    } else {
      if (this.viewReady) {
        const camera = defaultMapOptions.cameraPosition;
        this.scene.cameraTilt = defaultMapOptions.cameraTilt;
        this.scene.cameraPosition = [camera.longitude, camera.latitude, camera.elevation];
      }
    }
  }

  private toDisaggSources(serviceResponse: DisaggResponse): DisaggSource[] {
    const sources = serviceResponse.response.disaggs
      .map(disaggs =>
        disaggs.data
          .map(data =>
            data.sources.filter(source => source.latitude !== null && source.longitude !== null),
          )
          .flat(),
      )
      .flat();

    return sources.sort((a, b) => {
      if (a.contribution > b.contribution) {
        return -1;
      } else if (a.contribution < b.contribution) {
        return 1;
      }
      return 0;
    });
  }
}
<div class="geo-disagg-map">
  <nshmp-map-arcgis-3d #mapRef [map]="map" (viewReady)="onViewReady($event)">
    <arcgis-placement position="top-right">
      <div class="legend">
        <button mat-fab [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger">
          <img src="assets/map/legend.png" />
        </button>

        <mat-menu #menu="matMenu" xPosition="before" [hasBackdrop]="false">
          <div class="geo-disagg-legend-menu" (click)="$event.stopPropagation()">
            <div class="padding-2">
              <button mat-raised-button (click)="menuTrigger.closeMenu()" class="grid-col-12">
                Close
              </button>
            </div>

            <mat-list>
              <mat-list-item>
                <div>
                  <mat-slide-toggle
                    (change)="toggleLayer(LayerId.SITE_MARKER, $event.checked)"
                    checked
                  >
                    <div class="grid-row">
                      <div class="label">Site</div>
                      <div class="flex-1"></div>
                      <mat-icon>location_on</mat-icon>
                    </div>
                  </mat-slide-toggle>
                </div>
              </mat-list-item>

              <mat-list-item>
                <div>
                  <mat-slide-toggle
                    (change)="toggleLayer(LayerId.FAULT_SECTIONS, $event.checked)"
                    checked
                  >
                    <div class="grid-row">
                      <div class="label">Fault Sections</div>
                      <div class="flex-1"></div>
                      <div class="legend-line" style="background-color: rgb(255, 0, 0)"></div>
                    </div>
                  </mat-slide-toggle>
                </div>
              </mat-list-item>

              <mat-list-item>
                <div>
                  <mat-slide-toggle
                    (change)="toggleLayer(LayerId.INTERFACE_SECTIONS, $event.checked)"
                    checked
                  >
                    <div class="grid-row">
                      <div class="label">Interface Sections</div>
                      <div class="flex-1"></div>
                      <div class="legend-line" style="background-color: rgb(0, 100, 0)"></div>
                    </div>
                  </mat-slide-toggle>
                </div>
              </mat-list-item>

              @for (source of legendSourceTypes(); track source) {
                <mat-list-item>
                  <div class="grid-row">
                    <div class="padding-right-2">
                      {{ source.sourceType }}
                    </div>
                    <div class="legend-box" [style]="'background-color: ' + source.color"></div>
                  </div>
                </mat-list-item>
              }
            </mat-list>
          </div>
        </mat-menu>
      </div>
    </arcgis-placement>
  </nshmp-map-arcgis-3d>
</div>
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""