components/geo-disagg/geo-disagg.component.ts
Leaflet map with geographical disaggregation.
encapsulation | ViewEncapsulation.None |
selector | app-geo-disagg |
imports |
LeafletModule
NshmpLibNgMapBaseLayersControlComponent
|
templateUrl | ./geo-disagg.component.html |
styleUrl | ./geo-disagg.component.scss |
Properties |
Methods |
|
constructor(service: AppService)
|
||||||
Parameters :
|
Private createFaultsLayer | ||||||
createFaultsLayer(fc: GeoJSON.FeatureCollection)
|
||||||
Parameters :
Returns :
L.GeoJSON
|
Private createSiteLayer | ||||||||
createSiteLayer(serviceResponse: DisaggResponse)
|
||||||||
Create the site marker.
Parameters :
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 :
Returns :
void
|
Private onEachFeatureFaults | |||||||||
onEachFeatureFaults(layer: L.GeoJSON, feature: GeoJSON.Feature)
|
|||||||||
Parameters :
Returns :
void
|
onMapReady | ||||||||
onMapReady(map: L.Map)
|
||||||||
Handle on map ready event.
Parameters :
Returns :
void
|
Private onZoom | ||||||||
onZoom(serviceResponse: DisaggResponse)
|
||||||||
Redraw the bars on zoom.
Parameters :
Returns :
void
|
Private setBounds | ||||||
setBounds(bounds: L.LatLngBounds[])
|
||||||
Set the bounds.
Parameters :
Returns :
void
|
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>