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 { VectorImage as VectorImageLayer } 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 = 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) { const 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 { const gradient: IGradientstop[] = layer.renderer.colorMap.gradient; const histogram: IHistogram = layer.renderer.band.histogram; const index = (value - histogram.min) / histogram.max; let min = gradient[0]; let max = gradient[gradient.length - 1]; for (let n = 0; n < gradient.length; n++) { const 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; } const i = index - min.relativestop; const size = max.relativestop - min.relativestop; const alpha = Math.round(min.color.alpha + ((max.color.alpha - min.color.alpha) * i / size)); const red = Math.round(min.color.red + ((max.color.red - min.color.red) * i / size)); const green = Math.round(min.color.green + ((max.color.green - min.color.green) * i / size)); const 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 { let 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 { const value = layer.indexKey ? feature.get(layer.indexKey) : feature.get(layer.name); const key = item.code + "_" + value; if (!this.styleCache[key]) { let 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 { let layerIndex = -1; let layer: Layer = null; layerIndex = itemLayer.layerIndex != -1 ? itemLayer.layerIndex : item.data.layers[0].index; const 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 }); const data = item.data; const l = (data && data.layers && data.layers.length > 0) ? data.layers[0] : null; if (l && l.rendering && l.rendering.renderoutputType == "Tiles") { const rt = l.rendering as IRenderoutputTiles; const 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") { const ri = l.rendering as IRenderoutputImage; const source = new ImageStatic({ imageExtent:ri.extent,projection:'EPSG:3857', 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 { let layerIndex = -1; let layer: Layer = null; layerIndex = itemLayer.layerIndex != -1 ? itemLayer.layerIndex : item.data.layers[0].index; const data = item.data; const 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 { const __this = this; const format = new GeoJSON(); const source = new VectorSource({ strategy: loadingstrategy.bbox, loader: function (extent: Extent, resolution: number, projection: Projection) { const source = this as VectorSource; __this.itemService.getItemFeatures(item.code, extent, projection.getCode(), layerIndex).subscribe(function (data) { const features = format.readFeatures(data); for (const f of features) { if (f.get("code")) { f.setId(f.get("code")); } } source.addFeatures(features); }); } }); layer = new VectorImageLayer({ declutter: true, source: source, style: (feature) => { const key =feature.get("code") + "_" + feature.get("color"); if (!this.styleCache[key]) { const 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 { let layerIndex = -1; const layer: Layer = null; layerIndex = itemLayer.layerIndex != -1 ? itemLayer.layerIndex : itemLayer.item.data.layers[0].index; const data = itemLayer.item.data; const 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 { const data = item.data as ILayerData; let layer: Layer = null; switch (data.interfaceType) { case 'OSM': { const source = new OSM(); layer = new Tile({ source: source }); break; } case 'BingMaps': { const source = new BingMaps(data.options); layer = new Tile({ source: source }); break; } case 'TileWMS': { const source = new TileWMS(data.options); layer = new Tile({ source: source }); break; } case 'TileJSON': { const source = new TileJSON(data.options); layer = new Tile({ source: source }); break; } case 'TileArcGISRest': { const source = new TileArcGISRest(data.options); layer = new Tile({ source: source }); break; } case 'VectorWFSJson': { const 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 { let layer: Layer = null; const 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) { const geometry = new GeoJSON().readGeometry(itemLayer.item.geometry); const 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(olLayer : Layer) { if(!this.topLayerPrerenderEventhandlerInstalled && this.onPrerender.observers.length > 0 && olLayer) { if(this.instance.getVisible()) { olLayer.on('prerender',this.topLayerPrerenderEventhandler); olLayer.on('postrender',this.topLayerPostrenderEventhandler); this.topLayerPrerenderEventhandlerInstalled = true; } } } unInstallTopLayerPrerenderEventhandler() { if(this.topLayerPrerenderEventhandlerInstalled && this.onPrerender.observers.length > 0 ) { if(this.instance.getVisible()) { const olLayers = this.instance.getLayers().getArray().forEach((l:any) => { l.un('prerender',this.topLayerPrerenderEventhandler); l.un('postrender',this.topLayerPostrenderEventhandler); }); this.topLayerPrerenderEventhandlerInstalled = false; } } } addOrUpdateOlLayer(itemLayer:IItemLayer,index:number):Layer { if(!itemLayer) return null; const olLayers = this.instance.getLayers(); let layer = itemLayer.layer; const 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[] | IItemLayer) { this.unInstallTopLayerPrerenderEventhandler(); let dataLayer = false; let ils:IItemLayer[] = []; if(Array.isArray(itemLayers)) { ils = itemLayers; } else { dataLayer=true; ils=[itemLayers]; } const newLayers: Layer[] = []; if (ils) { ils.forEach((itemLayer, index) => { if(itemLayer.item.itemType == 'vnd.farmmaps.itemtype.temporal') { const il = itemLayer as ITemporalItemLayer; const previousLayer = this.addOrUpdateOlLayer(il.previousItemLayer,newLayers.length); if(previousLayer) newLayers.push(previousLayer); const selectedLayer = this.addOrUpdateOlLayer(il.selectedItemLayer,newLayers.length); if(selectedLayer) newLayers.push(selectedLayer); const nextLayer = this.addOrUpdateOlLayer(il.nextItemLayer,newLayers.length); if(nextLayer) newLayers.push(nextLayer); this.installTopLayerPrerenderEventhandler(selectedLayer); } else { const layer = this.addOrUpdateOlLayer(itemLayer,newLayers.length); if(layer) newLayers.push(layer); this.installTopLayerPrerenderEventhandler(layer); } }); // Remove the layers that have disapeared from childrenLayers const olLayers = this.instance.getLayers(); while(olLayers.getLength() > newLayers.length) { olLayers.removeAt(newLayers.length); } this.selectionLayer=null; if(this.mapEventHandlerInstalled && ils.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) } } } 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; const itemLayer= this.getItemlayer(this.itemLayer); if(itemLayer && itemLayer.layer) { this.selectedFeatures = {}; if(itemLayer.layer ) { const minZoom = itemLayer.layer.getMinZoom(); const currentZoom = this.map.instance.getView().getZoom(); if(currentZoom>minZoom) { itemLayer.layer.getFeatures(event.pixel).then((features) => { if(!features.length) { this.onFeatureHover.emit(null); return; } const fid = features[0].getId(); const 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']) { const itemLayers = changes['itemLayers'].currentValue as IItemLayer[]; this.updateLayers(itemLayers); } if (changes['itemLayer']) { const 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(); } }