import { Component, ElementRef, Input, Output, OnChanges, AfterViewInit, EventEmitter } from '@angular/core';
import * as am4core from "@amcharts/amcharts4/core";
import * as am4charts from "@amcharts/amcharts4/charts";
import { ScriptLoaderService } from "../../library/script-loader.service";
import { StyleLoaderService } from "../../library/style-loader.service";
import { getLayers } from "./leafletLayers";
import { BaseMapClass, IMapItem, ICustomCtrlSettings, ISwoopySettings, ITempViewerSettings, IContextMenuItem, IPathSettings, IAirwayItem, IScatterChartOptions, IGeoJsonSettings } from './baseMap';
declare const L; // leaflet
declare const OSMBuildings;
declare const jQuery;
export type LayerTypes = "osm" | "osm_bw" | "osm_hot" | "osm_fr" | "wiki" | "toner" | "ocean" | "voyager";
export type LayerAndMeasurePositions = "bottomright" | "bottomleft" | "topright" | "topleft";
// L.Map.mergeOptions({})
@Component({
selector: 'rd-leaflet',
template: `
`
})
export class Leaflet extends BaseMapClass implements OnChanges, AfterViewInit {
constructor(elem: ElementRef, private script: ScriptLoaderService, private style: StyleLoaderService) {
super(elem);
this.element = elem;
// this.style.loadStyles("body", ["./assets/css/leaflet.css"]); from style.css
this.script.load([
"./assets/js/leaflet.min.js",
"./assets/js/L-cluster.min.js",
"./assets/js/L-layer.min.js",
"./assets/js/L-locale.min.js",
"./assets/js/L-toolbar.min.js",
"./assets/js/L-visualClick.min.js",
"./assets/js/L-fullScreen.min.js",
"./assets/js/L-contextmenu.min.js",
"./assets/js/L-pulse.min.js",
"./assets/js/L-extra-markers.min.js"
])
}
@Input("rd-has-tooltip") hasTooltip: boolean = false;
@Input("rd-popup-content") popupContent: any;
@Input("rd-tooltip-content") tooltipContent: any;
@Input("rd-default-layer") defaultLayer: LayerTypes = "osm";
@Input("rd-layerCtrl-position") layerCtrlPosition: LayerAndMeasurePositions = "bottomright";
@Input("rd-measure-position") measurePosition: LayerAndMeasurePositions = "topright";
@Input("rd-cluster-active") clusterActive: boolean = true;
@Input("rd-toolbar-active") toolbarActive: boolean = false;
@Input("rd-measure-active") measureActive: boolean = false;
@Input("rd-scaleBar-active") scaleBarActive: boolean = false;
@Input("rd-rainViewer-active") rainViewerActive: boolean = false;
@Input("rd-tempViewer-active") tempViewerActive: boolean = false;
@Input("rd-geocoder-active") geocoderActive: boolean = false;
@Input("rd-buildings-active") buildingsActive: boolean = false;
@Input("rd-airway-data") airwayData: Array = [];
@Input("rd-sideBySide-items") sideBySideItems = []; // new Array(2);
@Input("rd-swoopy-settings") swoopySettings: ISwoopySettings;
@Input("rd-tempViewer-settings") tempViewerSettings: ITempViewerSettings;
@Input("rd-custom-control-settings") customCtrlSettings: ICustomCtrlSettings;
@Input("rd-path-settings") pathSettings: IPathSettings;
@Input("rd-scatter-options") scatterOptions: IScatterChartOptions;
@Input("rd-context-menu") contextMenuItems: Array = [];
@Input("rd-sidebar-element") sidebarElement: HTMLElement; // id required
@Input("rd-toolbar-settings") toolbarSettings: object = {};
@Input("rd-zoom-level") zoomLevel: number = 6;
@Input("rd-geoJson-settings") geoJsonSettings: IGeoJsonSettings = {};
@Output("rd-click") clickEvent: EventEmitter = new EventEmitter();
@Output("rd-cluster-click") clusterClickEvent: EventEmitter = new EventEmitter();
@Output("rd-on-hover") onHover: EventEmitter = new EventEmitter();
@Output("rd-toolbar-event") toolbarEvent: EventEmitter = new EventEmitter();
@Output("rd-measure-result") measureEvent: EventEmitter = new EventEmitter();
@Output("rd-map-ready") mapReady: EventEmitter = new EventEmitter();
public map;
private layer;
private element;
private container;
private mapItemInstance;
private cluster;
private heatMap;
private layersCtrl;
private markerList = [];
private chartContainer;
public geoJson;
ngAfterViewInit() {
this.container = jQuery(this.element.nativeElement).find("#mLeatlef")[0];
this.chartContainer = jQuery(this.element.nativeElement).find("#scatterChart")[0];
}
ngOnChanges(changes) {
if (!this.container) return;
if (changes.type || changes.data) {
if (!this.map) this.setMap();
else {
this.map.eachLayer((layer) => {
this.map.removeLayer(layer);
});
this.setLayers();
}
this.setMapDisplay(this.type);
}
if (changes.popupContent && this.mapItemInstance) {
this.mapItemInstance.bindPopup(this.popupContent).openPopup();
}
if (changes.tooltipContent && this.mapItemInstance) {
this.mapItemInstance.bindTooltip(this.tooltipContent).openTooltip();
}
if (changes.customCtrlSettings) this.addCustomCtrl();
if (changes.tempViewerSettings) this.setTempViewer();
if (changes.sideBySideItems && this.sideBySideItems.length) {
this.setSideBySide();
}
if (changes.airwayData && changes.airwayData.length) {
this.setAirWay();
}
if (changes.height) {
jQuery(this.container).css("height", this.height);
this.map.invalidateSize();
if (this.sidebarElement) jQuery(this.sidebarElement).css({ "height": this.height });
}
}
setMap() {
if (this.buildingsActive) this.setBuildings();
else {
this.map = L.map(this.container, {
center: this.setCenter(this.data, "leaflet"),
zoom: this.zoomLevel,
maxZoom: 20,
fullscreenControl: true,
contextmenu: !!this.contextMenuItems.length,
contextmenuItems: this.contextMenuItems
});
this.mapReady.emit();
this.setSearch();
this.setLocaleCtrl();
this.setVisualClick();
if (!this.tempViewerActive) this.setLayers();
if (this.scaleBarActive) this.setScaleBar();
if (this.toolbarActive) this.setToolBar();
if (this.rainViewerActive) this.setRainViewer();
if (this.measureActive) this.setMeasure();
if (this.sidebarElement) this.setSidebar();
if (this.swoopySettings) this.addSwoopy();
}
}
setPointMap() {
if (this.cluster) this.cluster.removeLayers(this.markerList);
this.markerList = [];
if (this.clusterActive) this.setCluster();
for (const item of this.data as Array) {
if (item.lat && item.lng) {
let marker = this.createMarker(item);
if (this.clusterActive) this.cluster.addLayer(marker);
else marker.addTo(this.map);
this.addEventListeners(marker);
}
}
if (this.clusterActive) this.map.addLayer(this.cluster);
if (this.scatterOptions) this.setScatterChart();
}
setAreaMap() {
let polygon = L.polygon(this.data).addTo(this.map);
this.addEventListeners(polygon);
}
setHeatMap() {
this.script.load(["./assets/js/L-heat.min.js"]).then(() => {
let heatData = (this.data as Array).map((item) => {
return [item.lat, item.lng, item.weight];
})
this.heatMap = L.heatLayer(heatData, { radius: 25 }).addTo(this.map);
})
}
setLineMap() {
if (this.pathSettings) this.setPath();
if (this.airwayData.length) this.setAirWay();
}
setGeoJson() {
this.geoJson = L.geoJson(this.data, this.geoJsonSettings).addTo(this.map);
}
setRoutingMap() { }
/** Tools */
setSearch() {
this.script.load([
"./assets/js/L-esri.min.js",
"./assets/js/L-esri-geocoder.min.js"
])
.then(() => {
let marker;
let geocodeService = L.esri.Geocoding.geocodeService();
let searchControl = L.esri.Geocoding.geosearch().addTo(this.map);
let results = L.layerGroup().addTo(this.map);
searchControl.on("results", (data) => {
results.clearLayers();
for (let i = data.results.length - 1; i >= 0; i--) {
results.addLayer(L.marker(data.results[i].latlng));
}
});
if (this.geocoderActive) {
this.customCtrlSettings = {
title: "Show Location Info (by click)",
position: "topleft",
content: '',
events: {
click: (e) => {
this.geocoderActive = !this.geocoderActive;
jQuery(e.target).css({ "color": this.geocoderActive ? "green" : "red" });
if (!this.geocoderActive && marker) marker.remove();
}
}
}
this.addCustomCtrl();
this.map.on('click', (e) => {
if (this.geocoderActive) {
geocodeService.reverse().latlng(e.latlng).run((error, result) => {
if (error) return;
if (marker) marker.remove();
marker = L.marker(result.latlng).addTo(this.map);
marker.bindPopup(result.address.Match_addr).openPopup();
});
}
});
}
})
}
setLocaleCtrl() {
L.control.locate().addTo(this.map);
}
setVisualClick() {
this.map.visualClick.enable(); // or disable();
}
setLayers() {
// layers
if (!this.sideBySideItems.length) {
jQuery(this.container).find(".leaflet-iconLayers").remove();
let layers = getLayers();
if (this.defaultLayer != "osm") {
let defaultItem = layers.filter((item) => item.title == this.defaultLayer)[0];
layers = layers.filter((item) => item.title != this.defaultLayer);
layers.unshift(defaultItem);
}
this.layersCtrl = L.control.iconLayers(layers, { position: this.layerCtrlPosition }).addTo(this.map);
this.layersCtrl.on('activelayerchange', (e) => {
this.layer = e.layer
});
}
// default layer
this.layer = getLayers(this.defaultLayer).addTo(this.map);
}
setCluster() {
this.cluster = L.markerClusterGroup();
this.cluster.on('click', (e) => {
this.clusterClickEvent.emit(e);
});
}
setScaleBar() {
this.script.load(["./assets/js/L-scalebar.min.js"]).then(() => {
L.edgeScaleBar().addTo(this.map);
})
}
setToolBar() {
this.map.pm.addControls(this.toolbarSettings);
this.map.on('pm:create', (e) => {
this.toolbarEvent.emit(e);
e.layer.on('pm:update', (eupdate) => this.toolbarEvent.emit(eupdate));
e.layer.on('pm:dragend', (edrag) => this.toolbarEvent.emit(edrag));
});
this.map.on('pm:cut', (e) => this.toolbarEvent.emit(e));
this.map.on('pm:remove', (e) => this.toolbarEvent.emit(e));
}
setSideBySide() {
this.script.load(["./assets/js/L-sidebyside.min.js"]).then(() => {
let myLayer1 = (this.sideBySideItems[0]).addTo(this.map);
let myLayer2 = (this.sideBySideItems[1]).addTo(this.map);
L.control.sideBySide(myLayer1, myLayer2).addTo(this.map);
})
}
setRainViewer() {
this.script.load(["./assets/js/L-rainviewer.min.js"]).then(() => {
L.control.rainviewer({
position: 'topright',
nextButtonText: '>',
playStopButtonText: 'Start',
prevButtonText: '<',
positionSliderLabelText: "Time:",
opacitySliderLabelText: "Opacity:",
animationInterval: 500,
opacity: 0.5
}).addTo(this.map);
})
}
setTempViewer() {
this.style.load([
"./assets/css/highlight.min.css",
"./assets/css/L-timedimension.control.min.css"
]);
this.script.load([
"./assets/js/iso8601.min.js",
"./assets/js/highlight.min.js",
"./assets/js/L-timedimension.min.js",
"./assets/js/L-timedimension-util.min.js",
"./assets/js/L-timedimension-layer.min.js",
"./assets/js/L-timedimension-layer-wms.min.js",
"./assets/js/L-timedimension-player.min.js",
"./assets/js/L-timedimension-control.min.js"
])
.then(() => {
this.map.timeDimension = new L.TimeDimension({
// timeInterval: "2014-09-30/2014-10-30",
// period: "PT1H"
});
let player = new L.TimeDimension.Player({
transitionTime: 100,
loop: false,
startOver: true
}, this.map.timeDimension);
let timeDimensionControl = new L.Control.TimeDimension({
player: player,
timeDimension: this.map.timeDimension,
position: 'bottomleft',
autoPlay: false,
minSpeed: 1,
speedStep: 0.5,
maxSpeed: 15,
timeSliderDragUpdate: true
});
this.map.addControl(timeDimensionControl);
this.setTempViewerServer();
})
}
setTempViewerServer() {
let wms = "https://ogcie.iblsoft.com/metocean/wms"
L.tileLayer.wms(wms, {
layers: 'foreground-lines',
format: 'image/png',
transparent: true,
crs: L.CRS.EPSG4326
}).addTo(this.map);
let layer = L.tileLayer.wms(wms, {
layers: 'gfs-temperature-isbl', // isobaric levels, or -agl for above ground levels
format: 'image/png',
transparent: true,
opacity: 0.3,
crs: L.CRS.EPSG4326
});
let proxy = 'assets/js/proxy.php';
let timeLayer = L.timeDimension.layer.wms(layer, {
proxy: proxy,
updateTimeDimension: true
});
timeLayer.addTo(this.map);
let palette = L.control({ position: 'topright' });
palette.onAdd = function () {
let src = wms + "?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetLegendGraphic&LAYER=gfs-temperature-isbl&STYLE=default";
let div = L.DomUtil.create('div', 'info legend');
div.innerHTML +=
'
';
return div;
};
palette.addTo(this.map);
}
setBuildings() {
this.script.load(["./assets/js/L-buildings.min.js"]).then(() => {
let latlng = this.setCenter(this.data, "leaflet");
var map = new OSMBuildings({
container: this.container,
position: { latitude: latlng[0], longitude: latlng[1] },
zoom: 15
});
map.addMapTiles('https://tile-a.openstreetmap.fr/hot/{z}/{x}/{y}.png');
map.addGeoJSONTiles('https://{s}.data.osmbuildings.org/0.2/anonymous/tile/{z}/{x}/{y}.json');
// map.addGeoJSONTiles('https://{s}.data.osmbuildings.org/0.2/ph2apjye/tile/{z}/{x}/{y}.json', { fixedZoom: 15 });
map.on("doubleclick", (e) => {
console.log("buildingsdbClickEvent", e);
});
map.addMarker(
{ latitude: latlng[0], longitude: latlng[1] },
{ id: 1 },
{ url: (this.data[0]).icon.url, color: (this.data[0]).icon.color }
);
})
}
setMeasure() {
this.script.load(["./assets/js/L-measure.min.js"]).then(() => {
let measureControl = L.control.measure({
position: this.measurePosition,
primaryLengthUnit: 'meters',
secondaryLengthUnit: 'kilometers',
primaryAreaUnit: 'acres',
secondaryAreaUnit: "hectares"
});
measureControl.addTo(this.map);
this.map.on('measurefinish', (e) => this.measureEvent.emit(e));
});
}
setSidebar() {
this.script.load(["./assets/js/L-sidebar.min.js"]).then(() => {
let toprightControl = jQuery(this.container).find(".leaflet-top.leaflet-right");
jQuery(this.sidebarElement).css({ "height": this.height, "pointer-events": "all" }).appendTo(toprightControl)
L.control.sidebar(this.sidebarElement.id).addTo(this.map);
});
this.map.on('fullscreenchange', () => {
jQuery(this.sidebarElement).css({ "height": this.map.isFullscreen() ? window.innerHeight * .9 : this.height });
});
}
setPath() {
this.script.load(["./assets/js/L-path.min.js"]).then(() => {
let pathData;
if (this.pathSettings.type == "circle") {
pathData = [this.data[0], this.data[1]];
}
else {
pathData = (>this.data).map((item) => { return [item.lat, item.lng] });
}
switch (this.pathSettings.type) {
case "polyline": this.pathSettings.use = L.polyline; break;
case "polygon": this.pathSettings.use = L.polygon; break;
case "rectangle": this.pathSettings.use = L.rectangle; break;
case "circle": this.pathSettings.use = L.circle; break;
default:
this.pathSettings.use = L.polyline
break;
}
L.polyline.antPath(pathData, this.pathSettings).addTo(this.map);
})
}
setAirWay() {
this.script.load(["./assets/js/L-airway.min.js"]).then(() => {
new L.migrationLayer({
map: this.map,
data: this.airwayData
}).addTo(this.map)
})
}
addCustomCtrl() {
this.script.load(["./assets/js/L-customControl.min.js"]).then(() => {
L.control.custom(this.customCtrlSettings).addTo(this.map);
})
}
addSwoopy() {
this.script.load(["./assets/js/L-swoopy.min.js"]).then(() => {
let { fromLatlng, toLatlng, ...settigs } = this.swoopySettings;
L.swoopyArrow(fromLatlng, toLatlng, settigs).addTo(this.map);
})
}
/** Utils */
createMarker(item: IMapItem): any {
let marker;
let myIcon;
if (item.icon && item.icon.url) {
myIcon = L.icon({
iconUrl: item.icon.url,
iconSize: item.icon.size || [38, 95],
iconAnchor: [22, 94],
popupAnchor: [-3, -76],
riseOnHover: true
});
}
else if (item.icon && item.icon.animate) myIcon = L.icon.pulse(item.icon.animate);
else if (item.icon && item.icon.class) myIcon = L.ExtraMarkers.icon(item.icon.class);
if (myIcon) marker = L.marker([item.lat, item.lng], { icon: myIcon });
else marker = L.marker([item.lat, item.lng]);
this.markerList.push(marker);
return marker;
}
addEventListeners(item) {
item.on("click", (e) => {
this.mapItemInstance = item;
this.clickEvent.emit({ lat: e.latlng.lat, lng: e.latlng.lng });
});
if (this.hasTooltip) {
item.on("mouseover", (e) => {
this.mapItemInstance = item;
this.onHover.emit({ lat: e.latlng.lat, lng: e.latlng.lng })
});
item.on("mouseout", (e) => {
item.closeTooltip();
});
}
}
setScatterChart() {
let toprightControl = jQuery(this.container).find(".leaflet-top.leaflet-right");
jQuery("#leaflet-info-pane").css({ "pointer-events": "all", "cursor": "pointer" }).appendTo(toprightControl);
let scatter = am4core.create(this.chartContainer, am4charts.XYChart);
scatter.data = this.scatterOptions.data;
let valueAxisX = scatter.xAxes.push(new am4charts.ValueAxis());
valueAxisX.title.text = this.scatterOptions.series.valueX.axis;
valueAxisX.renderer.minGridDistance = 40;
let valueAxisY = scatter.yAxes.push(new am4charts.ValueAxis());
valueAxisY.title.text = this.scatterOptions.series.valueY.axis;
let lineSeries = scatter.series.push(new am4charts.LineSeries());
lineSeries.dataFields.valueY = this.scatterOptions.series.valueY.field;
lineSeries.dataFields.valueX = this.scatterOptions.series.valueX.field;
lineSeries.strokeOpacity = 0;
let bullet = lineSeries.bullets.push(new am4charts.CircleBullet());
bullet.circle.tooltipText = "({valueX}, {valueY})";
let hoverState = bullet.states.create("hover");
hoverState.properties.scale = 1.5;
let selectedMark;
let selectedBullet;
this.customCtrlSettings = {
title: "Clear Select",
position: "topright",
content: '',
events: {
click: (e) => {
selectedMark = null;
this.markerList.map((mark) => mark.setOpacity(1));
}
}
}
this.addCustomCtrl();
bullet.events.on("hit", (e) => {
selectedBullet = e.target;
selectedMark = e.target.dataItem.dataContext;
});
bullet.events.on("over", (e) => {
if (!selectedMark) {
let chartItem: any = e.target.dataItem.dataContext;
this.markerList.filter((mark) => {
if (mark._latlng.lat == chartItem.valueX && mark._latlng.lng == chartItem.valueY) {
mark.setZIndexOffset(10000);
}
else mark.setOpacity(0.1);
})
}
});
bullet.events.on("out", (e) => {
if (!selectedMark) this.markerList.map((mark) => mark.setOpacity(1));
});
this.map.on('move', (e) => {
let bounds = this.map.getBounds();
let includeMarks = this.markerList.filter((fItem) => {
if (bounds.contains(fItem._latlng)) return fItem;
}).map((mItem) => {
return { valueX: mItem._latlng.lat, valueY: mItem._latlng.lng }
});
scatter.data = includeMarks;
});
}
}