components/geo-disagg/geo-disagg.component.ts
ArcGIS map with geographical disaggregation.
encapsulation | ViewEncapsulation.None |
selector | app-geo-disagg |
imports |
NshmpMapArcgis3dComponent
MatMenuModule
MatButtonModule
MatListModule
MatExpansionModule
MatIconModule
MatSlideToggleModule
|
templateUrl | ./geo-disagg.component.html |
styleUrl | ./geo-disagg.component.scss |
Properties |
|
Methods |
|
constructor(service: AppService, arcgisService: ArcgisService, destroyRef: DestroyRef)
|
||||||||||||
Parameters :
|
Private color | ||||||
color(sourceType: string)
|
||||||
Parameters :
Returns :
any
|
Private contributionGraphic | |||||||||
contributionGraphic(disaggSource: DisaggSource, shift: number)
|
|||||||||
Parameters :
Returns :
Graphic
|
Private createBars | ||||||
createBars(serviceResponse: DisaggResponse)
|
||||||
Parameters :
Returns :
GraphicsLayer
|
Private createFaultLayer | ||||||||||||
createFaultLayer(fc: GeoJSON.FeatureCollection, id: LayerId, color: number[])
|
||||||||||||
Parameters :
Returns :
GeoJSONLayer
|
Private createFaultSections | ||||||
createFaultSections(faultSections: GeoJSON.FeatureCollection)
|
||||||
Parameters :
Returns :
void
|
Private createInterfaceSections | ||||||
createInterfaceSections(interfaceSections: GeoJSON.FeatureCollection)
|
||||||
Parameters :
Returns :
void
|
Private createSiteMarker |
createSiteMarker()
|
Returns :
GraphicsLayer
|
Private endCapGraphic | |||||||||
endCapGraphic(disaggSource: DisaggSource, shift: number)
|
|||||||||
Parameters :
Returns :
Graphic
|
Private faultSectionPopupContent | |||||||||
faultSectionPopupContent(graphic: Graphic, fc: GeoJSON.FeatureCollection)
|
|||||||||
Parameters :
Returns :
string
|
Private goToLayers | |||||||||
goToLayers(barLayer: GraphicsLayer, siteLayer: GraphicsLayer)
|
|||||||||
Parameters :
Returns :
void
|
onViewReady | ||||||
onViewReady(scene: ArcgisScene)
|
||||||
Parameters :
Returns :
void
|
Private resetBarLayers |
resetBarLayers()
|
Returns :
void
|
Private responseToLayers | ||||||
responseToLayers(response: DisaggResponse)
|
||||||
Parameters :
Returns :
void
|
Private toDisaggSources | ||||||
toDisaggSources(serviceResponse: DisaggResponse)
|
||||||
Parameters :
Returns :
DisaggSource[]
|
toggleLayer |
toggleLayer(id: LayerId, checked: boolean)
|
Returns :
void
|
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>