File

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

Description

Leaflet map with geographical disaggregation.

Metadata

Index

Properties
Methods

Constructor

constructor(service: AppService)
Parameters :
Name Type Optional
service AppService No

Methods

Private createBarLayers
createBarLayers(serviceResponse: DisaggResponse)

Create the bar graph layers for disagg contribution.

Parameters :
Name Type Optional Description
serviceResponse DisaggResponse No

The disagg service response

Returns : L.Rectangle[]
Private createFaultsLayer
createFaultsLayer(fc: GeoJSON.FeatureCollection)
Parameters :
Name Type Optional
fc GeoJSON.FeatureCollection No
Returns : L.GeoJSON
Private createSiteLayer
createSiteLayer(serviceResponse: DisaggResponse)

Create the site marker.

Parameters :
Name Type Optional Description
serviceResponse DisaggResponse No

The disagg service response

Returns : { bounds: any; marker: any; }
Private faultsWeight
faultsWeight()
Returns : number
Private mapRedraw
mapRedraw()
Returns : void
onBaseLayerChange
onBaseLayerChange(layer: MapBaseLayer)

Handle on base layer change event.

Parameters :
Name Type Optional
layer MapBaseLayer No
Returns : void
Private onEachFeatureFaults
onEachFeatureFaults(layer: L.GeoJSON, feature: GeoJSON.Feature)
Parameters :
Name Type Optional
layer L.GeoJSON No
feature GeoJSON.Feature No
Returns : void
onMapReady
onMapReady(map: L.Map)

Handle on map ready event.

Parameters :
Name Type Optional Description
map L.Map No

The leaflet map instance

Returns : void
Private onZoom
onZoom(serviceResponse: DisaggResponse)

Redraw the bars on zoom.

Parameters :
Name Type Optional Description
serviceResponse DisaggResponse No

Disagg service response

Returns : void
Private setBounds
setBounds(bounds: L.LatLngBounds[])

Set the bounds.

Parameters :
Name Type Optional
bounds L.LatLngBounds[] No
Returns : void

Properties

barLayers
Default value : computed(() => this.createBarLayers(this.serviceResponse()))

Disagg contribution bar layers

baseLayer
Default value : baseLayer(MapBaseLayer.OCEAN)

Base layer

bounds
Type : L.LatLngBounds
Default value : null

Map bounds state

defaultOpacity
Type : number
Default value : 0.6
faultLayer
Default value : computed(() => this.createFaultsLayer(this.service.faults()))
map
Type : L.Map

Leaflet map

mapOptions
Type : L.MapOptions
Default value : { attributionControl: false, center: L.latLng(40, -105), maxBounds, minZoom: 2, renderer: L.canvas({tolerance: 10}), zoom: 8, }

Map options state

serviceResponse
Default value : this.service.serviceResponse

The disagg service response

siteLayer
Default value : computed(() => this.createSiteLayer(this.serviceResponse()))

Site marker

zoomInWeight
Type : number
Default value : 4
zoomOutWeight
Type : number
Default value : 1
import {Component, computed, effect, ViewEncapsulation} from '@angular/core';
import {LeafletModule} from '@bluehalo/ngx-leaflet';
import {
  mapUtils,
  NshmpLibNgMapBaseLayersControlComponent,
} from '@ghsc/nshmp-lib-ng/map';
import {baseLayer, MapBaseLayer} from '@ghsc/nshmp-utils-ts/libs/leaflet';
import {DisaggResponse} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/disagg-service';
import * as L from 'leaflet';
import {} from 'rxjs';

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

/** 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);

/**
 * Leaflet map with geographical disaggregation.
 */
@Component({
  encapsulation: ViewEncapsulation.None,
  imports: [LeafletModule, NshmpLibNgMapBaseLayersControlComponent],
  selector: 'app-geo-disagg',
  styleUrl: './geo-disagg.component.scss',
  templateUrl: './geo-disagg.component.html',
})
export class GeoDisaggComponent {
  /** Disagg contribution bar layers */
  barLayers = computed(() => this.createBarLayers(this.serviceResponse()));

  /** Base layer */
  baseLayer = baseLayer(MapBaseLayer.OCEAN);

  /** Map bounds state */
  bounds: L.LatLngBounds = null;

  faultLayer = computed(() => this.createFaultsLayer(this.service.faults()));

  /** Leaflet map */
  map: L.Map;

  /** Map options state */
  mapOptions: L.MapOptions = {
    attributionControl: false,
    center: L.latLng(40, -105),
    maxBounds,
    minZoom: 2,
    renderer: L.canvas({tolerance: 10}),
    zoom: 8,
  };

  /** The disagg service response */
  serviceResponse = this.service.serviceResponse;

  /** Site marker  */
  siteLayer = computed(() => this.createSiteLayer(this.serviceResponse()));

  zoomInWeight = 4;
  zoomOutWeight = 1;
  defaultOpacity = 0.6;

  constructor(private service: AppService) {
    effect(() => {
      if (this.barLayers() && this.siteLayer()) {
        this.setBounds([
          ...this.barLayers().map(layer => layer.getBounds()),
          this.siteLayer().bounds,
        ]);
      }

      this.mapRedraw();
    });
  }

  /**
   * Handle on base layer change event.
   *
   * @param event The layer event
   */
  onBaseLayerChange(layer: MapBaseLayer) {
    this.baseLayer = baseLayer(layer);
    this.mapRedraw();
  }

  /**
   * Handle on map ready event.
   *
   * @param map The leaflet map instance
   */
  onMapReady(map: L.Map): void {
    this.map = map;

    map.on('zoom', () => this.onZoom(this.serviceResponse()));

    if (!L.Browser.mobile) {
      new mapUtils.MousePosition().addTo(map);
      L.control.attribution({prefix: false}).addTo(map);
    }
    L.control.scale({position: 'bottomleft'}).addTo(map);

    this.mapRedraw();
  }

  /**
   * Create the bar graph layers for disagg contribution.
   *
   * @param serviceResponse The disagg service response
   */
  private createBarLayers(serviceResponse: DisaggResponse): L.Rectangle[] {
    if (serviceResponse === null || serviceResponse === undefined) {
      return [];
    }

    const heightFactor = this.map.getZoom();

    const layers = serviceResponse.response.disaggs
      .map(disaggs => {
        return disaggs.data
          .map(data => {
            return data.sources
              .filter(
                source => source.latitude !== null && source.longitude !== null,
              )
              .map(source => {
                const latLng = L.latLng(source.latitude, source.longitude);
                const currentPoint = this.map.latLngToContainerPoint(latLng);
                const width = 4;
                const height = source.contribution * heightFactor;
                const southWest = L.point(
                  currentPoint.x - width / 2,
                  currentPoint.y,
                );
                const northEast = L.point(
                  currentPoint.x + width / 2,
                  currentPoint.y - height,
                );

                const bounds = L.latLngBounds(
                  this.map.containerPointToLatLng(southWest),
                  this.map.containerPointToLatLng(northEast),
                );
                const rect = L.rectangle(bounds, {
                  color: 'black',
                  fillColor: '#3388ff',
                  fillOpacity: this.defaultOpacity,
                  opacity: this.defaultOpacity,
                  weight: 1,
                });

                const properties = Object.entries(source).map(
                  ([key, value]: [string, string]) => {
                    return ` <tr>
                    <th style="padding-right: 1em">${key}</th>
                    <td>${value}</td>
                  </tr>
                  `;
                  },
                );

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

                rect.bindTooltip(popupContent, {
                  offset: L.point(10, 0),
                });

                return {
                  contribution: source.contribution,
                  layer: rect,
                };
              });
          })
          .flat();
      })
      .flat();

    const barLayers = layers
      .sort((a, b) => {
        if (a.contribution > b.contribution) {
          return -1;
        } else if (a.contribution < b.contribution) {
          return 1;
        }
        return 0;
      })
      .map(layer => layer.layer.bringToFront());

    barLayers.forEach(barLayer => {
      barLayer.on('mouseover', () => {
        barLayer.setStyle({fillOpacity: 1, opacity: 1, weight: 3});
      });

      barLayer.on('mouseout', () => {
        barLayers.forEach(l =>
          l.setStyle({
            fillOpacity: this.defaultOpacity,
            opacity: this.defaultOpacity,
            weight: 1,
          }),
        );
      });
    });

    this.mapRedraw();

    return barLayers;
  }

  private createFaultsLayer(fc: GeoJSON.FeatureCollection): L.GeoJSON {
    const faultLayer = L.geoJSON<GeoJSON.FeatureCollection>(fc, {
      onEachFeature: (feature, layer: L.GeoJSON) =>
        this.onEachFeatureFaults(layer, feature),
      style: {
        color: 'red',
        opacity: this.defaultOpacity,
        weight: this.faultsWeight(),
      },
    });

    faultLayer.eachLayer(layer => {
      layer.on('mouseover', event => {
        const target = event.target as L.GeoJSON;
        target.setStyle({opacity: 1, weight: this.faultsWeight() * 2});
      });

      layer.on('mouseout', () => {
        faultLayer.resetStyle();
      });
    });

    this.mapRedraw();

    return faultLayer;
  }

  /**
   * Create the site marker.
   *
   * @param serviceResponse The disagg service response
   */
  private createSiteLayer(serviceResponse: DisaggResponse) {
    if (serviceResponse === null || serviceResponse === undefined) {
      return null;
    }

    const site = L.latLng(
      serviceResponse.request.latitude,
      serviceResponse.request.longitude,
    );

    const siteMarker = L.marker(site, {
      icon: mapUtils.icon({
        color: mapUtils.IconColor.BLACK,
        height: 26,
        width: 16,
      }),
    });

    siteMarker.bindTooltip(`Site: ${site.lng}, ${site.lat}`);

    return {
      bounds: L.latLngBounds(site, site),
      marker: siteMarker,
    };
  }

  private faultsWeight(): number {
    return this.map.getZoom() >= 8 ? this.zoomInWeight : this.zoomOutWeight;
  }

  private onEachFeatureFaults(
    layer: L.GeoJSON,
    feature: GeoJSON.Feature,
  ): void {
    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>
      `;
      });

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

    layer.bindTooltip(popupContent, {
      offset: L.point(10, 0),
    });
  }

  /**
   * Redraw the bars on zoom.
   *
   * @param serviceResponse Disagg service response
   */
  private onZoom(serviceResponse: DisaggResponse): void {
    if (this.faultLayer()) {
      this.faultLayer().setStyle({
        weight: this.faultsWeight(),
      });
    }

    if (serviceResponse === null || serviceResponse === undefined) {
      return;
    }
    this.createBarLayers(serviceResponse);
  }

  private mapRedraw(): void {
    setTimeout(() => this.map.invalidateSize(true), 0);
  }

  /**
   * Set the bounds.
   *
   * @param layers The bar layers
   */
  private setBounds(bounds: L.LatLngBounds[]): void {
    const layerBounds = bounds[0];

    bounds.forEach(b => {
      layerBounds.extend(b);
    });

    this.bounds = layerBounds;
  }
}
<div
  class="geo-disagg-map"
  leaflet
  [leafletOptions]="mapOptions"
  [leafletFitBounds]="bounds"
  (leafletMapReady)="onMapReady($event)"
>
  <!-- Base layer-->
  <div [leafletLayer]="baseLayer"></div>

  <!-- Site marker -->
  <div [leafletLayer]="siteLayer()?.marker"></div>

  <!-- Fault layer -->
  <div [leafletLayer]="faultLayer()"></div>

  <!-- Bar layers -->
  @if (barLayers()?.length > 0) {
    @for (layer of barLayers(); track layer) {
      <div [leafletLayer]="layer"></div>
    }
  }

  <!-- Base layers control -->
  <nshmp-lib-ng-map-base-layers-control
    [map]="map"
    (baseLayerChanged)="onBaseLayerChange($event)"
  />
</div>
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""