import { Component, Host, Input, Output, EventEmitter,OnDestroy, OnInit, OnChanges, SimpleChanges, forwardRef } from '@angular/core'; import { LayerGroupComponent, MapComponent } from 'ngx-openlayers'; import { ItemService,IItem,AppConfig } from '@farmmaps/common'; import { IItemLayer, ITemporalItemLayer} from '../../../models/item.layer'; import { ILayerData} from '../../../models/layer.data'; import { IRenderoutputTiles,IRenderoutputImage,IGradientstop,ILayer,IHistogram,IColor} from '../../../models/color.map'; import {Extent} from 'ol/extent'; import Projection from 'ol/proj/Projection'; import * as proj from 'ol/proj'; import * as loadingstrategy from 'ol/loadingstrategy'; import * as style from 'ol/style'; import {Tile,Layer,Image} from 'ol/layer'; import {XYZ,ImageStatic,OSM,BingMaps,TileWMS,TileArcGISRest,TileJSON,Source} from 'ol/source'; import {Vector as VectorSource} from 'ol/source'; import { Vector as VectorLayer } from 'ol/layer'; import VectorTileSource from 'ol/source/VectorTile'; import VectorTileLayer from 'ol/layer/VectorTile'; import {GeoJSON,MVT} from 'ol/format'; import { Geometry } from 'ol/geom'; import BaseLayer from 'ol/layer/Base'; @Component({ selector: 'fm-map-item-layers', template: ``, providers: [ { provide: LayerGroupComponent, useExisting: forwardRef(() => ItemLayersComponent) } ] }) export class ItemLayersComponent extends LayerGroupComponent implements OnChanges, OnInit,OnDestroy { @Input() itemLayers: IItemLayer[]; @Input() itemLayer: IItemLayer; @Output() onFeatureSelected: EventEmitter = new EventEmitter(); @Output() onFeatureHover: EventEmitter = new EventEmitter(); @Output() onPrerender: EventEmitter = new EventEmitter(); private _apiEndPoint: string; private initialized:boolean = false; private mapEventHandlerInstalled = false; private topLayerPrerenderEventhandlerInstalled = false; private selectedFeatures = {}; private selectionLayer:Layer = null; constructor(private itemService: ItemService, private map: MapComponent, public appConfig: AppConfig) { super(map); this._apiEndPoint = appConfig.getConfig("apiEndPoint"); } private styleCache = {} componentToHex(c) { var hex = c.toString(16); return hex.length == 1 ? "0" + hex : hex; } rgbaToHex(r, g, b,a) { return "#" + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b) + this.componentToHex(a); } getColorFromGradient(layer: ILayer, value: number): IColor { var gradient: IGradientstop[] = layer.renderer.colorMap.gradient; var histogram: IHistogram = layer.renderer.band.histogram; var index = (value - histogram.min) / histogram.max; var min = gradient[0]; var max = gradient[gradient.length - 1]; for (var n = 0; n < gradient.length; n++) { var s = gradient[n]; if (s.relativestop <= index && min.relativestop < s.relativestop && n < gradient.length - 1) min = s; if (s.relativestop >= index && max.relativestop > s.relativestop && n > 0) max = s; } var i = index - min.relativestop; var size = max.relativestop - min.relativestop; var alpha = Math.round(min.color.alpha + ((max.color.alpha - min.color.alpha) * i / size)); var red = Math.round(min.color.red + ((max.color.red - min.color.red) * i / size)); var green = Math.round(min.color.green + ((max.color.green - min.color.green) * i / size)); var blue = Math.round(min.color.blue + ((max.color.blue - min.color.blue) * i / size)); return { alpha: alpha, red: red, green: green, blue: blue }; } getColorForValue(layer: ILayer, value: number): IColor { var color: IColor = { alpha:0,red:0,green:0,blue:0}; if(layer.renderer.colorMap.entries.length>0) { color=layer.renderer.colorMap.noValue; } layer.renderer.colorMap.entries.forEach((entry) => { if(entry.value==value) { color =entry.color; return; } }); return color; } getColor(item: IItem, layer: ILayer, feature): style.Style { var value = layer.indexKey ? feature.get(layer.indexKey) : feature.get(layer.name); var key = item.code + "_" + value; if (!this.styleCache[key]) { var color: IColor; if(layer.renderer.colorMap.colormapType == "manual") { color = this.getColorForValue(layer, value); } else { color = this.getColorFromGradient(layer, value); } this.styleCache[key] = new style.Style( { image: new style.Circle({ fill: new style.Fill({ color: this.rgbaToHex(color.red, color.green, color.blue, color.alpha) }), radius: 3 }), fill: new style.Fill({ color: this.rgbaToHex(color.red, color.green, color.blue, color.alpha) }), stroke: new style.Stroke({ color: this.rgbaToHex(color.red, color.green, color.blue, 255), width: 1.25 }), }); } return this.styleCache[key]; } createGeotiffLayer(item:IItem,itemLayer:IItemLayer):Layer { var layerIndex = -1; var layer: Layer = null; layerIndex = itemLayer.layerIndex != -1 ? itemLayer.layerIndex : item.data.layers[0].index; let source = new XYZ({ maxZoom: 19, minZoom: 1, url: `${this._apiEndPoint}/api/v1/items/${item.code}/tiles/${layerIndex}/{z}/{x}/{y}.png?v=${Date.parse(item.updated)}` }); layer = new Tile({ source: source }); var data = item.data; var l = (data && data.layers && data.layers.length > 0) ? data.layers[0] : null; if (l && l.rendering && l.rendering.renderoutputType == "Tiles") { var rt = l.rendering as IRenderoutputTiles; let source = new XYZ({crossOrigin: 'use-credentials', maxZoom: rt.maxzoom, minZoom: rt.minzoom, url: `${this._apiEndPoint}/api/v1/items/${item.code}/tiles/${layerIndex}/{z}/{x}/{y}.png?v=${Date.parse(item.updated)}` }); layer = new Tile({ source: source }); } if (l && l.rendering && l.rendering.renderoutputType == "Image") { var ri = l.rendering as IRenderoutputImage; // convert to EPSG:4326 asworkaround for cesium var projectedExtent = proj.transformExtent( ri.extent, 'EPSG:3857','EPSG:4326'); let source = new ImageStatic({ imageExtent:projectedExtent,projection:'EPSG:4326', crossOrigin: 'use-credentials', url: `${this._apiEndPoint}/api/v1/items/${item.code}/mapimage/${layerIndex}?v=${Date.parse(item.updated)}` }); layer = new Image({ source: source }); } return layer; } createShapeLayer(item:IItem,itemLayer:IItemLayer):Layer { var layerIndex = -1; var layer: Layer = null; layerIndex = itemLayer.layerIndex != -1 ? itemLayer.layerIndex : item.data.layers[0].index; var data = item.data; var l:ILayer = (data && data.layers && data.layers.length > 0) ? data.layers[layerIndex] : null; if (l && l.rendering && l.rendering.renderoutputType == "VectorTiles") { var rt = l.rendering as IRenderoutputTiles; layer = new VectorTileLayer({ declutter: true, source: new VectorTileSource({ maxZoom: rt.maxzoom, minZoom: rt.minzoom, format: new MVT(), url: `${this._apiEndPoint}/api/v1/items/${item.code}/vectortiles/{z}/{x}/{y}.pbf?v=${Date.parse(item.updated)}` }), style: (feature) => { return this.getColor(item,l, feature); } }) } else if (l && l.rendering && l.rendering.renderoutputType == "Tiles") { var rt = l.rendering as IRenderoutputTiles; layer = new Tile({ source: new XYZ({ maxZoom: rt.maxzoom, minZoom: rt.minzoom, url: `${this._apiEndPoint}/api/v1/items/${item.code}/vectortiles/image_tiles/${layerIndex}/{z}/{x}/{y}.png?v=${Date.parse(item.updated)}` }) }); } else { let __this = this; let format = new GeoJSON(); let source = new VectorSource({ strategy: loadingstrategy.bbox, loader: function (extent: Extent, resolution: number, projection: Projection) { var source = this as VectorSource; __this.itemService.getItemFeatures(item.code, extent, projection.getCode(), layerIndex).subscribe(function (data) { var features = format.readFeatures(data); for (let f of features) { if (f.get("code")) { f.setId(f.get("code")); } } source.addFeatures(features); }); } }); layer = new VectorLayer({ source: source, style: (feature) => { var key =feature.get("code") + "_" + feature.get("color"); if (!this.styleCache[key]) { var color = feature.get("color"); this.styleCache[key] = new style.Style( { fill: new style.Fill({ color: color }), stroke: new style.Stroke({ color: color, width: 1.25 }), image: new style.Circle({ fill: new style.Fill({ color: color }), stroke: new style.Stroke({ color: color, width: 1.25 }), radius: 5 }), } ) } return this.styleCache[key]; } }); } if(l.minzoom) { layer.setMinZoom(l.minzoom); } if(l.maxzoom) { layer.setMaxZoom(l.maxzoom); } return layer; } createSelectionLayer(itemLayer:IItemLayer):Layer { var layerIndex = -1; var layer: Layer = null; layerIndex = itemLayer.layerIndex != -1 ? itemLayer.layerIndex : itemLayer.item.data.layers[0].index; var data = itemLayer.item.data; var l:ILayer = (data && data.layers && data.layers.length > 0) ? data.layers[layerIndex] : null; if (l && l.rendering && l.rendering.renderoutputType == "VectorTiles") { return new VectorTileLayer({ renderMode: 'vector', source: (itemLayer.layer as VectorTileLayer).getSource(), style: (feature) => { if (feature.getId() in this.selectedFeatures) { return new style.Style( { stroke: new style.Stroke({ color: 'red', width: 2 }) } ); } }, minZoom: itemLayer.layer.getMinZoom(), maxZoom: itemLayer.layer.getMaxZoom() }); } return null; } createExternalLayer(item:IItem,itemLayer:IItemLayer):Layer { let data = item.data as ILayerData; var layer: Layer = null; switch (data.interfaceType) { case 'OSM': { let source = new OSM(); layer = new Tile({ source: source }); break; } case 'BingMaps': { let source = new BingMaps(data.options); layer = new Tile({ source: source }); break; } case 'TileWMS': { let source = new TileWMS(data.options); layer = new Tile({ source: source }); break; } case 'TileJSON': { let source = new TileJSON(data.options); layer = new Tile({ source: source }); break; } case 'TileArcGISRest': { let source = new TileArcGISRest(data.options); layer = new Tile({ source: source }); break; } case 'VectorWFSJson': { let source = new VectorSource({ format: new GeoJSON(), url: function (extent) { return ( data.options.url + '&srsname=' + data.projection + '&bbox=' + extent.join(',') + ',EPSG:3857' ); }, strategy: loadingstrategy.bbox, }); layer = new VectorLayer({ source: source }); break; } default: { break; } } return layer; } createLayer(itemLayer: IItemLayer): Layer { var layer: Layer = null; var layerIndex = -1; if (itemLayer.item.itemType == 'vnd.farmmaps.itemtype.geotiff.processed') { layer = this.createGeotiffLayer(itemLayer.item,itemLayer); } else if (itemLayer.item.itemType == 'vnd.farmmaps.itemtype.shape.processed') { layer = this.createShapeLayer(itemLayer.item,itemLayer); } else if (itemLayer.item.itemType == 'vnd.farmmaps.itemtype.layer') { layer = this.createExternalLayer(itemLayer.item,itemLayer); } if (layer) { let geometry = new GeoJSON().readGeometry(itemLayer.item.geometry); let extent = geometry ? proj.transformExtent(geometry.getExtent(), 'EPSG:4326', 'EPSG:3857') : null; if (extent) layer.setExtent(extent); } return layer; } ngOnInit() { super.ngOnInit(); if(this.itemLayers) { this.updateLayers(this.itemLayers); } else if(this.itemLayer) { if(this.getItemlayer(this.itemLayer).item.itemType == 'vnd.farmmaps.itemtype.shape.processed') { this.installMapEventHandler(); } this.updateLayers([this.itemLayer]) } else { this.updateLayers([]); } this.initialized=true; } installMapEventHandler() { if(!this.mapEventHandlerInstalled) { this.map.instance.on(['click', 'pointermove'],this.mapEventHandler); this.mapEventHandlerInstalled=true; } } unInstallMapEventHandler() { if(this.mapEventHandlerInstalled) { this.map.instance.un(['click', 'pointermove'],this.mapEventHandler); this.mapEventHandlerInstalled=false; } } installTopLayerPrerenderEventhandler() { if(!this.topLayerPrerenderEventhandlerInstalled && this.onPrerender.observers.length > 0 ) { if(this.instance.getVisible()) { var olLayers = this.instance.getLayers().getArray().filter(l=> l.getVisible()); if(olLayers.length >0) { var topLayer = olLayers[0] as any; topLayer.on('prerender',this.topLayerPrerenderEventhandler); topLayer.on('postrender',this.topLayerPostrenderEventhandler); this.topLayerPrerenderEventhandlerInstalled = true; } } } } unInstallTopLayerPrerenderEventhandler() { if(this.topLayerPrerenderEventhandlerInstalled && this.onPrerender.observers.length > 0 ) { if(this.instance.getVisible()) { var olLayers = this.instance.getLayers().getArray().filter(l=> l.getVisible()); if(olLayers.length >0) { var topLayer = olLayers[0] as any; topLayer.un('prerender',this.topLayerPrerenderEventhandler); topLayer.un('postrender',this.topLayerPostrenderEventhandler); this.topLayerPrerenderEventhandlerInstalled = false; } } } } addOrUpdateOlLayer(itemLayer:IItemLayer,index:number):Layer { if(!itemLayer) return null; var olLayers = this.instance.getLayers(); var layer = itemLayer.layer; let olIndex = olLayers.getArray().indexOf(layer); if (olIndex < 0) { // New layer: we add it to the map layer = this.createLayer(itemLayer); if (layer) { olLayers.insertAt(index, layer); } } else if (index !== olIndex) { // layer has moved inside the layers list olLayers.removeAt(olIndex); olLayers.insertAt(index, layer); } if(layer) { itemLayer.layer = layer; layer.setOpacity(itemLayer.opacity); layer.setVisible(itemLayer.visible); } return layer; } updateLayers(itemLayers: IItemLayer[]) { this.unInstallTopLayerPrerenderEventhandler(); let newLayers: Layer[] = []; if (itemLayers) { itemLayers.forEach((itemLayer, index) => { if(itemLayer.item.itemType == 'vnd.farmmaps.itemtype.temporal') { let il = itemLayer as ITemporalItemLayer; let previousLayer = this.addOrUpdateOlLayer(il.previousItemLayer,newLayers.length); if(previousLayer) newLayers.push(previousLayer); let selectedLayer = this.addOrUpdateOlLayer(il.selectedItemLayer,newLayers.length); if(selectedLayer) newLayers.push(selectedLayer); let nextLayer = this.addOrUpdateOlLayer(il.nextItemLayer,newLayers.length); if(nextLayer) newLayers.push(nextLayer); } else { let layer = this.addOrUpdateOlLayer(itemLayer,newLayers.length); if(layer) newLayers.push(layer); } }); // Remove the layers that have disapeared from childrenLayers var olLayers = this.instance.getLayers(); while(olLayers.getLength() > newLayers.length) { olLayers.removeAt(newLayers.length); } this.selectionLayer=null; if(this.mapEventHandlerInstalled && itemLayers.length==1 && this.getItemlayer(itemLayers[0]).item.itemType == 'vnd.farmmaps.itemtype.shape.processed') { this.selectionLayer = this.createSelectionLayer(this.getItemlayer(itemLayers[0])); if(this.selectionLayer) olLayers.push(this.selectionLayer) } } this.installTopLayerPrerenderEventhandler(); } topLayerPrerenderEventhandler = (event) => { this.onPrerender.emit(event); } topLayerPostrenderEventhandler = (event) => { const ctx = event.context; ctx.restore(); } mapEventHandler = (event) => { // select only when having observers if(event.type === 'click' && !this.onFeatureSelected.observers.length) return; if(event.type === 'pointermove' && !this.onFeatureHover.observers.length) return; let itemLayer= this.getItemlayer(this.itemLayer); if(itemLayer && itemLayer.layer) { this.selectedFeatures = {}; if(itemLayer.layer ) { let minZoom = itemLayer.layer.getMinZoom(); let currentZoom = this.map.instance.getView().getZoom(); if(currentZoom>minZoom) { itemLayer.layer.getFeatures(event.pixel).then((features) => { if(!features.length) { this.onFeatureHover.emit(null); return; } let fid = features[0].getId(); let feature = features[0]; if(event.type === 'pointermove') { this.selectedFeatures[fid] = features[0]; this.onFeatureHover.emit({ "feature": feature,"itemCode":itemLayer.item.code }); } else { this.onFeatureSelected.emit({ "feature": feature,"itemCode":itemLayer.item.code }); } }) if(this.selectionLayer) this.selectionLayer.changed(); } } } } getItemlayer(itemLayer:IItemLayer):IItemLayer { if((itemLayer as ITemporalItemLayer).selectedItemLayer) return (itemLayer as ITemporalItemLayer).selectedItemLayer; return itemLayer; } ngOnChanges(changes: SimpleChanges) { if (this.instance && this.initialized) { if (changes['itemLayers']) { var itemLayers = changes['itemLayers'].currentValue as IItemLayer[]; this.updateLayers(itemLayers); } if (changes['itemLayer']) { var itemLayer = changes['itemLayer'].currentValue as IItemLayer; this.itemLayer = itemLayer if(itemLayer) { if(this.getItemlayer(this.itemLayer).item.itemType == 'vnd.farmmaps.itemtype.shape.processed') { this.installMapEventHandler(); } this.updateLayers([itemLayer]); } else { this.unInstallMapEventHandler(); this.updateLayers([]); } } } } ngOnDestroy() { this.unInstallMapEventHandler(); super.ngOnDestroy(); } }