FarmMapsLib/projects/common-map/src/fm-map/components/map/map.component.ts

530 lines
21 KiB
TypeScript

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: string = 'Map';
public openedModalName$: Observable<string> = this.store.select(commonReducers.selectOpenedModalName);
public itemTypes$: Observable<{ [id: string]: IItemType }>;
public mapState$: Observable<IMapState> = this.store.select(mapReducers.selectGetMapState);
public features$: Observable<Array<Feature<Geometry>>> = this.store.select(mapReducers.selectGetFeatures);
public overlayLayers$: Observable<Array<IItemLayer>> = this.store.select(mapReducers.selectGetOverlayLayers);
public selectedOverlayLayer$: Observable<IItemLayer> = this.store.select(mapReducers.selectGetSelectedOverlayLayer);
public selectedItemLayer$: Observable<IItemLayer> = this.store.select(mapReducers.selectGetSelectedItemLayer);
public baseLayers$: Observable<Array<IItemLayer>> = this.store.select(mapReducers.selectGetBaseLayers);
public selectedBaseLayer$: Observable<IItemLayer> = this.store.select(mapReducers.selectGetSelectedBaseLayer);
public projection$: Observable<string> = this.store.select(mapReducers.selectGetProjection);
public selectedFeatures$: Subject<ISelectedFeatures> = new Subject<ISelectedFeatures>();
public droppedFile$: Subject<IDroppedFile> = new Subject<IDroppedFile>();
private paramSub: Subscription;
private itemTypeSub: Subscription;
private stateSub: Subscription;
private queryStateSub: Subscription;
private querySub: Subscription;
public parentCode$: Observable<string> =this.store.select(mapReducers.selectGetParentCode);
public panelVisible$: Observable<boolean> = this.store.select(mapReducers.selectGetPanelVisible);
public panelCollapsed$: Observable<boolean> = this.store.select(mapReducers.selectGetPanelCollapsed);
public selectedFeature$: Observable<Feature<Geometry>> = this.store.select(mapReducers.selectGetSelectedFeature);
public clickedFeature: Subject<Feature<Geometry>> = new Subject<Feature<Geometry>>();
public selectedItem$: Observable<IItem> = this.store.select(mapReducers.selectGetSelectedItem);
public parentItem$: Observable<IItem> =this.store.select(mapReducers.selectGetParentItem);
public queryState$: Observable<IQueryState> = this.store.select(mapReducers.selectGetQueryState);
public state$:Observable<{mapState:IMapState,queryState:IQueryState}> = this.store.select(mapReducers.selectGetState);
public period$: Observable<IPeriodState> = this.store.select(mapReducers.selectGetPeriod);
public clearEnabled$: Observable<boolean> = this.store.select(mapReducers.selectGetClearEnabled);
public searchCollapsed$: Observable<boolean> = this.store.select(mapReducers.selectGetSearchCollapsed);
public searchMinified$: Observable<boolean> = this.store.select(mapReducers.selectGetSearchMinified);
public showDataLayerSlide$: Observable<boolean> = this.store.select(mapReducers.selectGetShowdataLayerSlide);
public menuVisible$: Observable<boolean>;
public query$: Observable<IQuery> = this.store.select(mapReducers.selectGetQuery);
public position$: Observable<GeolocationPosition> = this.geolocationService.getCurrentPosition();
public compassHeading$: Observable<number> = this.deviceorientationService.getCurrentCompassHeading();
public baseLayersCollapsed:boolean = true;
public overlayLayersCollapsed: boolean = true;
public extent$: Observable<Extent> = this.store.select(mapReducers.selectGetExtent);
public styles$:Observable<IStyles> = this.store.select(mapReducers.selectGetStyles);
public fullscreen$: Observable<boolean> = this.store.select(commonReducers.selectGetFullScreen);
private lastUrl = "";
private initialized: boolean = false;
public noContent: boolean = false;
public overrideSelectedItemLayer: boolean = false;
public overrideOverlayLayers: boolean = false;
public dataLayerSlideValue:number = 50;
public dataLayerSlideEnabled = false;
private visibleAreaBottom = 0;
@ViewChild('map') map;
@ViewChild('contentDiv') contentDiv: ElementRef;
constructor(private store: Store<mapReducers.State | commonReducers.State>,
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"]) {
let 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`);
let urlparts=[];
if (query.querystate.itemCode && query.querystate.itemCode != "") {
if(query.querystate.itemType && query.querystate.itemType!= "") {
let 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) {
let 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<Geometry>) {
this.clickedFeature.next(feature);
}
handleFeatureHover(feature: Feature<Geometry>) {
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;
}
}
handleSidepaneloutletDeactivate(component:any) {
if(component && component.hasOwnProperty('clickedFeature')) {
(component as IClickedFeature).clickedFeature = null;
}
}
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 {
let d = Math.pow(10, decimals);
return Math.round((value + Number.EPSILON)*d)/d;
}
getMapStateFromUrl(params:ParamMap):IMapState {
var hasUrlmapState = params.has("xCenter") && params.has("yCenter");
if (hasUrlmapState) {
let xCenter = parseFloat(params.get("xCenter"));
let yCenter = parseFloat(params.get("yCenter"));
let zoom = parseFloat(params.get("zoom"));
let rotation = parseFloat(params.get("rotation"));
let baseLayer = params.get("baseLayer")?params.get("baseLayer"):"";
var 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")) {
let queryState = params.get("queryState");
var 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
let urlMapState = this.getMapStateFromUrl(this.route.snapshot.paramMap);
let 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) {
let 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) => {
let 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) =>{
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();
});
}
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();
var queryState = tassign(mapReducers.initialQueryState, query);
this.store.dispatch(new mapActions.DoQuery(queryState));
}
replaceUrl(mapState: IMapState, queryState: IQueryState, replace: boolean = true) {
if(this.noContent) {
let newMapState = this.serializeMapState(mapState);
let newQueryState = this.serializeService.serialize(queryState);
let currentMapState = this.serializeMapState(this.getMapStateFromUrl(this.route.snapshot.paramMap));
let urlQueryState = this.getQueryStateFromUrl(this.route.snapshot.paramMap);
let currentQueryState = urlQueryState==null?"":this.serializeService.serialize(urlQueryState);
if(mapState.baseLayerCode!="" && ((newMapState!= currentMapState) || (newQueryState!=currentQueryState))) {
let 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.zone.run(() =>{
console.debug("Move end");
var map = event.map;
var view = map.getView();
var rotation = view.getRotation();
var zoom = view.getZoom();
var center = transform(view.getCenter(), view.getProjection(), "EPSG:4326");
var viewExtent = view.calculateExtent(this.map.instance.getSize());
let mapState: IMapState = { xCenter: center[0], yCenter: center[1], zoom: zoom, rotation: rotation, baseLayerCode: null };
let state = { mapState: mapState, viewExtent: viewExtent };
console.debug("Center: ",center[0],center[1] );
let source = from([state]);
source.pipe(withLatestFrom(this.selectedBaseLayer$)).subscribe(([state, baselayer]) => {
if (mapState && baselayer) { // do not react on first move
let 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) {
var 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) {
let point = new Point([locations[0].coordinates.lon,locations[0].coordinates.lat]);
point.transform('EPSG:4326', 'EPSG:3857');
let circle = new Circle(point.getCoordinates(),5000);//
let 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();
}
}