import { Component, OnInit, OnDestroy, HostListener, ViewChild, AfterViewInit,NgZone,ElementRef } from '@angular/core'; import { Location } from '@angular/common'; import { Observable, Subject, Subscription, from,of ,EMPTY } from 'rxjs'; import { withLatestFrom, switchMap,skip } from 'rxjs/operators'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { Store } from '@ngrx/store'; import { DeviceService } from '@farmmaps/common'; import {getRenderPixel} from 'ol/render'; // Map import * as mapReducers from '../../reducers/map.reducer'; import * as mapActions from '../../actions/map.actions'; import { IMapState} from '../../models/map.state'; import { IClickedFeature} from '../../models/clicked.feature'; import { IQuery } from '../../reducers/map.reducer' import { ISelectedFeatures } from '../../models/selected.features'; import { IItemLayer } from '../../models/item.layer'; import { IListItem, IQueryState } from '@farmmaps/common'; import { IPeriodState } from '../../models/period.state'; import {IStyles} from '../../models/style.cache'; import { IDroppedFile } from '../aol/file-drop-target/file-drop-target.component'; import { StateSerializerService } from '@farmmaps/common'; import { GeolocationService} from '../../services/geolocation.service'; import { GeolocatorService } from '@farmmaps/common'; import {DeviceOrientationService} from '../../services/device-orientation.service'; // AppCommon import { ResumableFileUploadService, ItemTypeService } from '@farmmaps/common'; import { IItemType, IItem } from '@farmmaps/common'; import {commonReducers} from '@farmmaps/common'; import {commonActions} from '@farmmaps/common'; import {Feature} from 'ol'; import {Geometry,Point,Circle} from 'ol/geom'; import {Extent,createEmpty,extend } from 'ol/extent'; import {transform} from 'ol/proj'; import { tassign } from 'tassign'; import * as style from 'ol/style'; @Component({ selector: 'fm-map-map', templateUrl: './map.component.html', styleUrls: ['./map.component.scss'] }) export class MapComponent implements OnInit, OnDestroy,AfterViewInit { title = 'Map'; public openedModalName$: Observable = this.store.select(commonReducers.selectOpenedModalName); public itemTypes$: Observable<{ [id: string]: IItemType }>; public mapState$: Observable = this.store.select(mapReducers.selectGetMapState); public features$: Observable>> = this.store.select(mapReducers.selectGetFeatures); public overlayLayers$: Observable> = this.store.select(mapReducers.selectGetOverlayLayers); public selectedOverlayLayer$: Observable = this.store.select(mapReducers.selectGetSelectedOverlayLayer); public selectedItemLayer$: Observable = this.store.select(mapReducers.selectGetSelectedItemLayer); public baseLayers$: Observable> = this.store.select(mapReducers.selectGetBaseLayers); public selectedBaseLayer$: Observable = this.store.select(mapReducers.selectGetSelectedBaseLayer); public projection$: Observable = this.store.select(mapReducers.selectGetProjection); public selectedFeatures$: Subject = new Subject(); public droppedFile$: Subject = new Subject(); private paramSub: Subscription; private itemTypeSub: Subscription; private stateSub: Subscription; private queryStateSub: Subscription; private querySub: Subscription; public parentCode$: Observable =this.store.select(mapReducers.selectGetParentCode); public panelVisible$: Observable = this.store.select(mapReducers.selectGetPanelVisible); public panelCollapsed$: Observable = this.store.select(mapReducers.selectGetPanelCollapsed); public panelExtraWide$: Observable = this.store.select(mapReducers.selectGetPanelExtraWide); public selectedFeature$: Observable> = this.store.select(mapReducers.selectGetSelectedFeature); public clickedFeature: Subject> = new Subject>(); public selectedItem$: Observable = this.store.select(mapReducers.selectGetSelectedItem); public parentItem$: Observable =this.store.select(mapReducers.selectGetParentItem); public queryState$: Observable = this.store.select(mapReducers.selectGetQueryState); public state$:Observable<{mapState:IMapState,queryState:IQueryState}> = this.store.select(mapReducers.selectGetState); public period$: Observable = this.store.select(mapReducers.selectGetPeriod); public clearEnabled$: Observable = this.store.select(mapReducers.selectGetClearEnabled); public searchCollapsed$: Observable = this.store.select(mapReducers.selectGetSearchCollapsed); public searchMinified$: Observable = this.store.select(mapReducers.selectGetSearchMinified); public showDataLayerSlide$: Observable = this.store.select(mapReducers.selectGetShowdataLayerSlide); public menuVisible$: Observable; public query$: Observable = this.store.select(mapReducers.selectGetQuery); public position$: Observable = this.geolocationService.getCurrentPosition(); public compassHeading$: Observable = this.deviceorientationService.getCurrentCompassHeading(); public baseLayersCollapsed = true; public overlayLayersCollapsed = true; public extent$: Observable = this.store.select(mapReducers.selectGetExtent); public styles$:Observable = this.store.select(mapReducers.selectGetStyles); public fullscreen$: Observable = this.store.select(commonReducers.selectGetFullScreen); private lastUrl = ""; private initialized = false; public noContent = false; public overrideSelectedItemLayer = false; public overrideOverlayLayers = false; public dataLayerSlideValue = 50; public dataLayerSlideEnabled = false; private visibleAreaBottom = 0; private viewEnabled = true; @ViewChild('map') map; @ViewChild('contentDiv') contentDiv: ElementRef; constructor(private store: Store, private route: ActivatedRoute, private router: Router, private uploadService: ResumableFileUploadService, private serializeService: StateSerializerService, public itemTypeService: ItemTypeService, private location: Location, private geolocationService: GeolocationService, private geolocaterService: GeolocatorService, private zone: NgZone, private deviceorientationService:DeviceOrientationService, public devicesService:DeviceService) { if(route && route.snapshot && route.snapshot.data && route.snapshot.data["fm-map-map"]) { const params = route.snapshot.data["fm-map-map"]; this.overrideSelectedItemLayer = params["overrideSelectedItemlayer"] ? params["overrideSelectedItemlayer"] : false; this.overrideOverlayLayers = params["overrideOverlayLayers"] ? params["overrideOverlayLayers"] : false; } this.querySub = this.query$.pipe(skip(1), withLatestFrom(this.mapState$)).subscribe(([query,mapState]) =>{ if(query && query.querystate) { let newQueryState = tassign(mapReducers.initialQueryState); //console.debug(`Do Query`); const urlparts=[]; if (query.querystate.itemCode && query.querystate.itemCode != "") { if(query.querystate.itemType && query.querystate.itemType!= "") { const itemType = this.itemTypeService.itemTypes[query.querystate.itemType]; if (itemType && itemType.viewer && itemType.viewer == "edit_in_editor" && itemType.editor) { urlparts.push('/editor'); urlparts.push(itemType.editor); urlparts.push('item'); urlparts.push(query.querystate.itemCode); } } } else { newQueryState= query.querystate; } if(urlparts.length==0 ) { newQueryState.itemCode = query.querystate.itemCode; this.zone.run(() => { this.replaceUrl(mapState,newQueryState,query.replace); }) } else { this.router.navigate(urlparts); } } }); this.store.dispatch(new mapActions.Init()); // this.store.select(commonReducers.getRootItems).subscribe((l) => { // if(l && l.length>0) { // this.store.dispatch(new mapActions.Init()); // } // }); } @HostListener('document:keyup', ['$event']) escapeClose(event: KeyboardEvent) { const x = event.keyCode; if (x === 27) { this.handleCloseModal() } } handlePanelResize(resizeTop:number) { if(resizeTop==100 || !this.devicesService.IsMobile() ) { this.visibleAreaBottom=0; } else { this.visibleAreaBottom=100-resizeTop; if(this.visibleAreaBottom>60) { this.visibleAreaBottom=60; } } } bottom(panelVisible:boolean) { if(panelVisible) { return this.visibleAreaBottom + '%'; } else { return "0%"; } } handleOpenModal(modalName: string) { this.store.dispatch(new commonActions.OpenModal(modalName)); } handleCloseModal() { this.store.dispatch(new commonActions.CloseModal()); } handleFileDropped(droppedFile: IDroppedFile) { this.uploadService.addFiles(droppedFile.files, droppedFile.event, { parentCode:droppedFile.parentCode, geometry:droppedFile.geometry }); } handleFeatureClick(feature: Feature) { this.store.dispatch(new mapActions.ClickFeature(feature)); this.clickedFeature.next(feature); } handleFeatureHover(feature: Feature) { this.store.dispatch(new mapActions.SelectFeature(feature)); } handleSearch(queryState: IQueryState) { this.store.dispatch(new mapActions.DoQuery(queryState)); } handleSidepaneloutletActivate(component:any) { if(component && component.hasOwnProperty('clickedFeature')) { (component as IClickedFeature).clickedFeature = this.clickedFeature; } if(component && component.hasOwnProperty('extrawide')) { this.store.dispatch(new mapActions.SetPanelExtraWide(true)); } } handleSidepaneloutletDeactivate(component:any) { if(component && component.hasOwnProperty('clickedFeature')) { (component as IClickedFeature).clickedFeature = null; } if(component && component.hasOwnProperty('extrawide')) { this.store.dispatch(new mapActions.SetPanelExtraWide(false)); } } handlePrerender(event:any) { if(!this.dataLayerSlideEnabled) return; const ctx = event.context; const mapSize = this.map.instance.getSize(); const width = mapSize[0] * (this.dataLayerSlideValue / 100); const tl = getRenderPixel(event, [width, 0]); const tr = getRenderPixel(event, [mapSize[0], 0]); const bl = getRenderPixel(event, [width, mapSize[1]]); const br = getRenderPixel(event, mapSize); ctx.save(); ctx.beginPath(); ctx.moveTo(tl[0], tl[1]); ctx.lineTo(bl[0], bl[1]); ctx.lineTo(br[0], br[1]); ctx.lineTo(tr[0], tr[1]); ctx.closePath(); ctx.clip(); } handleSlideChange(event:any) { this.dataLayerSlideValue = event.target.value; this.map.instance.render(); } ngOnInit() { this.initialized = false; //console.debug("Init"); this.store.dispatch(new mapActions.Clear()); this.selectedFeatures$.next({x:0,y:0,features:[]}); this.selectedFeatures$.next(null); } initCustomStyles() { this.store.dispatch(new mapActions.SetStyle('vnd.farmmaps.itemtype.layer',new style.Style({ stroke: new style.Stroke({ color: 'red', lineDash: [ 5,5], width: 1 }), geometry:(feature) =>feature.getGeometry() }))); this.store.dispatch(new mapActions.SetStyle('vnd.farmmaps.itemtype.layer_selected',new style.Style({ stroke: new style.Stroke({ color: 'red', lineDash: [ 5,5], width: 3 }), geometry:(feature) =>feature.getGeometry() }))); } round(value:number,decimals:number):number { const d = Math.pow(10, decimals); return Math.round((value + Number.EPSILON)*d)/d; } getMapStateFromUrl(params:ParamMap):IMapState { const hasUrlmapState = params.has("xCenter") && params.has("yCenter"); if (hasUrlmapState) { const xCenter = parseFloat(params.get("xCenter")); const yCenter = parseFloat(params.get("yCenter")); const zoom = parseFloat(params.get("zoom")); const rotation = parseFloat(params.get("rotation")); const baseLayer = params.get("baseLayer")?params.get("baseLayer"):""; const newMapState = {zoom: zoom, rotation: rotation, xCenter: xCenter, yCenter: yCenter, baseLayerCode: baseLayer }; return newMapState; } else { return null; } } normalizeMapState(mapState:IMapState):IMapState { if(!mapState) return null; return {zoom: this.round(mapState.zoom,0), rotation: this.round(mapState.rotation,2), xCenter: this.round(mapState.xCenter,5), yCenter: this.round(mapState.yCenter,5), baseLayerCode: mapState.baseLayerCode }; } serializeMapState(mapState:IMapState):string { return JSON.stringify(this.normalizeMapState(mapState)); } getQueryStateFromUrl(params:ParamMap):IQueryState { if (params.has("queryState")) { const queryState = params.get("queryState"); let newQueryState = tassign(mapReducers.initialQueryState); if (queryState != "") { newQueryState = this.serializeService.deserialize(queryState); } return newQueryState; } else { return null; } } ngAfterViewInit() { //console.debug("View init"); this.noContent=true; this.route.children.forEach((entry) => { if(entry.outlet=="primary") { this.noContent=false; } }); this.initCustomStyles(); // url to state const urlMapState = this.getMapStateFromUrl(this.route.snapshot.paramMap); const urlQueryState = this.getQueryStateFromUrl(this.route.snapshot.paramMap); if(urlQueryState && urlMapState && this.noContent) { this.store.dispatch(new mapActions.SetState(urlMapState,urlQueryState)); window.localStorage.setItem("FarmMapsCommonMap_mapState",this.serializeMapState(urlMapState)); } else if(urlQueryState && this.noContent) { this.store.dispatch(new mapActions.SetQueryState(urlQueryState)); } else { this.store.dispatch(new mapActions.SetReplaceUrl(true)); } this.paramSub = this.route.paramMap.pipe(withLatestFrom(this.state$),switchMap(([params,state]) => { if(this.initialized && this.noContent) { const urlQueryState = this.getQueryStateFromUrl(params); if( this.serializeService.serialize(state.queryState) != this.serializeService.serialize(urlQueryState)) { return of(new mapActions.SetState(state.mapState,urlQueryState)); } } return EMPTY; })).subscribe((action) => { if(action) { this.zone.run(() => { //console.debug("Url to state"); this.store.dispatch(action); }); } }); // state to url this.stateSub = this.state$.pipe(switchMap((state) => { const newUrl = this.serializeMapState(state.mapState) + "_" + this.serializeService.serialize(state.queryState); if(this.lastUrl!=newUrl) { this.lastUrl=newUrl; return of(state); } else { return of(null); } })).subscribe((newUrlState: any) => { if(newUrlState) { //console.debug(`State to url`); this.replaceUrl(newUrlState.mapState,newUrlState.queryState,newUrlState.replaceUrl); } }); this.initialized = true; this.showDataLayerSlide$.subscribe((v) => { this.dataLayerSlideEnabled=v; this.map.instance.render(); }); this.store.select(mapReducers.selectGetViewEnabled).subscribe((v) => { this.viewEnabled = v; }); } handleSearchCollapse(event) { this.store.dispatch(new mapActions.CollapseSearch()); } handleSearchExpand(event) { this.store.dispatch(new mapActions.ExpandSearch()); } handleToggleMenu(event) { this.store.dispatch(new commonActions.ToggleMenu()); } handleToggleBaseLayers(event:MouseEvent) { this.baseLayersCollapsed = !this.baseLayersCollapsed; event.preventDefault(); } handleToggleOverlayLayers(event: MouseEvent) { this.overlayLayersCollapsed = !this.overlayLayersCollapsed; event.preventDefault(); } handlePredefinedQuery(event: MouseEvent, query: any) { event.preventDefault(); const queryState = tassign(mapReducers.initialQueryState, query); this.store.dispatch(new mapActions.DoQuery(queryState)); } replaceUrl(mapState: IMapState, queryState: IQueryState, replace = true) { if(this.noContent) { const newMapState = this.serializeMapState(mapState); const newQueryState = this.serializeService.serialize(queryState); const currentMapState = this.serializeMapState(this.getMapStateFromUrl(this.route.snapshot.paramMap)); const urlQueryState = this.getQueryStateFromUrl(this.route.snapshot.paramMap); const currentQueryState = urlQueryState==null?"":this.serializeService.serialize(urlQueryState); if(mapState.baseLayerCode!="" && ((newMapState!= currentMapState) || (newQueryState!=currentQueryState))) { const parts =["."]; parts.push(mapState.xCenter.toFixed(5)); parts.push(mapState.yCenter.toFixed(5)); parts.push( mapState.zoom.toFixed(0)); parts.push( mapState.rotation.toFixed(2)); parts.push(mapState.baseLayerCode); parts.push( this.serializeService.serialize(queryState)); //console.debug("Replace url",parts); this.router.navigate(parts, { replaceUrl: replace,relativeTo:this.route.parent }); } } } handleOnMoveEnd(event) { if(this.initialized && this.viewEnabled) { this.zone.run(() =>{ //console.debug("Move end"); const map = event.map; const view = map.getView(); const rotation = view.getRotation(); const zoom = view.getZoom(); const center = transform(view.getCenter(), view.getProjection(), "EPSG:4326"); const viewExtent = view.calculateExtent(this.map.instance.getSize()); const mapState: IMapState = { xCenter: center[0], yCenter: center[1], zoom: zoom, rotation: rotation, baseLayerCode: null }; const state = { mapState: mapState, viewExtent: viewExtent }; //console.debug("Center: ",center[0],center[1] ); const source = from([state]); source.pipe(withLatestFrom(this.selectedBaseLayer$)).subscribe(([state, baselayer]) => { if (mapState && baselayer) { // do not react on first move const newMapState = tassign(state.mapState, { baseLayerCode: baselayer.item.code }); this.store.dispatch(new mapActions.SetMapState(newMapState)); this.store.dispatch(new mapActions.SetViewExtent(state.viewExtent)); } }); }); } } handleOnMouseDown(event: MouseEvent) { event.stopPropagation(); this.zone.run(() =>{ this.store.dispatch(new commonActions.CloseAll()); }); } handleShowLayerValues(event: MouseEvent) { event.stopPropagation(); this.zone.run(() =>{ this.store.dispatch(new mapActions.ToggleLayerValuesEnabled()); }); } handleOnDownload(event) { } handleClearSearch(event) { this.store.dispatch(new mapActions.Clear()); } handleOnDelete(itemLayer: IItemLayer) { this.store.dispatch(new mapActions.RemoveLayer(itemLayer)); } handleOnToggleVisibility(itemLayer: IItemLayer) { this.store.dispatch(new mapActions.SetVisibility(itemLayer,!itemLayer.visible)); } handleOnSetOpacity(event:{ layer: IItemLayer,opacity:number }) { this.store.dispatch(new mapActions.SetOpacity(event.layer, event.opacity)); } handleZoomToExtent(itemLayer: IItemLayer) { const extent = createEmpty(); extend(extent, itemLayer.layer.getExtent()); if (extent) { this.store.dispatch(new mapActions.SetExtent(extent)); } } handleSelectBaseLayer(itemLayer: IItemLayer) { this.store.dispatch(new mapActions.SelectBaseLayer(itemLayer)); } handleSelectOverlayLayer(itemLayer: IItemLayer) { this.store.dispatch(new mapActions.SelectOverlayLayer(itemLayer)); } handlePeriodChange(period:IPeriodState) { this.store.dispatch(new mapActions.SetPeriod(period)); } handleCitySearch(location:string) { this.geolocaterService.geocode(location).subscribe(locations => { if( locations.length > 0) { const point = new Point([locations[0].coordinates.lon,locations[0].coordinates.lat]); point.transform('EPSG:4326', 'EPSG:3857'); const circle = new Circle(point.getCoordinates(),5000);// const extent = createEmpty(); extend(extent, circle.getExtent()); this.store.dispatch(new mapActions.SetExtent(extent)) } }); } ngOnDestroy() { if (this.paramSub) this.paramSub.unsubscribe(); if (this.itemTypeSub) this.itemTypeSub.unsubscribe(); if (this.stateSub) this.stateSub.unsubscribe(); if (this.queryStateSub) this.queryStateSub.unsubscribe(); if (this.querySub) this.querySub.unsubscribe(); } }