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 += 'legend'; 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; }); } }