import { Injectable } from '@angular/core'; import { Store, Action, createFeatureSelector } from '@ngrx/store'; import { ROUTER_NAVIGATED, RouterReducerState } from '@ngrx/router-store'; import * as fromRouter from '@ngrx/router-store'; import { createEffect, Actions, ofType } from '@ngrx/effects'; import { EMPTY, Observable, of } from 'rxjs'; import { withLatestFrom, switchMap, map, catchError, mergeMap } from 'rxjs/operators'; import { GeoJSON, WKT } from 'ol/format'; import { Feature } from 'ol'; import { getCenter, createEmpty, extend } from 'ol/extent'; import { Point, Geometry } from 'ol/geom' import * as mapActions from '../actions/map.actions'; import * as mapReducers from '../reducers/map.reducer'; import { commonReducers } from '@farmmaps/common'; import { commonActions } from '@farmmaps/common'; import { IItem } from '@farmmaps/common'; import { FolderService, ItemService } from '@farmmaps/common'; import { tassign } from 'tassign'; import { FeatureIconService } from '../services/feature-icon.service'; import * as style from 'ol/style'; import { ItemTypeService, IQueryState } from '@farmmaps/common'; import { TemporalItemLayer } from '../models/item.layer' export const getRouterState = createFeatureSelector('router'); export const { selectCurrentRoute, // select the current route selectQueryParams, // select the current route query params selectQueryParam, // factory function to select a query param selectRouteParams, // select the current route params selectRouteParam, // factory function to select a route param selectRouteData, // select the current route data selectUrl, // select the current url } = fromRouter.getSelectors(getRouterState); @Injectable() export class MapEffects { private _geojsonFormat: GeoJSON; private _wktFormat: WKT; private overrideSelectedItemLayer = false; private updateFeatureGeometry(feature: Feature, updateEvent: commonActions.DeviceUpdateEvent): Feature { const newFeature = feature.clone(); const f = this._wktFormat.readFeature(updateEvent.attributes["geometry"], { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }); const centroid = getCenter(f.getGeometry().getExtent()); newFeature.setId(feature.getId()); newFeature.setGeometry(new Point(centroid)); return newFeature; } init$ = createEffect(() => this.actions$.pipe( ofType(mapActions.INIT), withLatestFrom(this.store$.select(commonReducers.selectGetRootItems)), switchMap(([action, rootItems]) => { const actions = []; for (const rootItem of rootItems) { if (rootItem.itemType == "UPLOADS_FOLDER") actions.push(new mapActions.SetParent(rootItem.code)); } // initialize default feature styles actions.push(new mapActions.SetStyle('file', new style.Style({ image: new style.Icon({ anchor: [0.5, 1], scale: 0.05, src: this.featureIconService$.getIconImageDataUrl("fal fa-file") }), stroke: new style.Stroke({ color: 'red', width: 1 }), fill: new style.Fill({ color: 'rgba(0, 0, 0,0)' }) }))); actions.push(new mapActions.SetStyle('selected', new style.Style({ image: new style.Icon({ anchor: [0.5, 1], scale: 0.08, src: this.featureIconService$.getIconImageDataUrl(null) }), stroke: new style.Stroke({ color: 'red', width: 3 }), fill: new style.Fill({ color: 'rgba(0, 0, 0, 0)' }) }))); return actions; } ))); initBaseLayers$ = createEffect(() => this.actions$.pipe( ofType(mapActions.INIT), withLatestFrom(this.store$.select(mapReducers.selectGetProjection)), map(([action, projection]) => new mapActions.LoadBaseLayers(projection))) ); loadBaseLayers$ = createEffect(() => this.actions$.pipe( ofType(mapActions.LOADBASELAYERS), switchMap((action: mapActions.LoadBaseLayers) => { return this.itemService$.getItemList("vnd.farmmaps.itemtype.layer", { "isBaseLayer": true }).pipe( map((items: IItem[]) => new mapActions.LoadBaseLayersSuccess(items)), catchError(error => of(new commonActions.Fail(error)))); }))); startSearch$ = createEffect(() => this.actions$.pipe( ofType(mapActions.STARTSEARCH), switchMap((action) => { const a = action as mapActions.StartSearch; const startDate = a.queryState.startDate; const endDate = a.queryState.endDate; let newAction: Observable; if (a.queryState.itemCode || a.queryState.parentCode || a.queryState.itemType || a.queryState.query || a.queryState.tags) { newAction = this.itemService$.getFeatures(a.queryState.bbox, "EPSG:3857", a.queryState.query, a.queryState.tags, startDate, endDate, a.queryState.itemType, a.queryState.parentCode, a.queryState.dataFilter, a.queryState.level).pipe( switchMap((features: any) => { for (const f of features.features) { if (f.properties && f.properties["code"]) { f.id = f.properties["code"]; } } return of(new mapActions.StartSearchSuccess(this._geojsonFormat.readFeatures(features), a.queryState)); } ), catchError(error => of(new commonActions.Fail(error)))); } else { return []; } return newAction; }))); zoomToExtent$ = createEffect(() => this.actions$.pipe( ofType(mapActions.STARTSEARCHSUCCESS), mergeMap((action: mapActions.StartSearchSuccess) => { const actions = []; actions.push(new commonActions.SetMenuVisible(false)); const extent = createEmpty(); if (!action.query.bboxFilter) { if (extent) { for (const f of action.features) { extend(extent, (f as Feature).getGeometry().getExtent()); } if (action.features && action.features.length > 0) { actions.push(new mapActions.SetExtent(extent)); } } } return actions; }))); zoomToExtent2$ = createEffect(() => this.actions$.pipe( ofType(mapActions.SETFEATURES), switchMap((action: mapActions.SetFeatures) => { const extent = createEmpty(); if (extent) { for (const f of action.features) { extend(extent, (f as Feature).getGeometry().getExtent()); } if (action.features.length > 0) return of(new mapActions.SetExtent(extent)); } return EMPTY; }))); hideMenu$ = createEffect(() => this.actions$.pipe( ofType(mapActions.STARTSEARCHSUCCESS), mergeMap((action: mapActions.StartSearchSuccess) => { return of(new commonActions.SetMenuVisible(false)); }))); selectItem$ = createEffect(() => this.actions$.pipe( ofType(mapActions.SELECTITEM), withLatestFrom(this.store$.select(mapReducers.selectGetSelectedItem)), switchMap(([action, selectedItem]) => { const a = action as mapActions.SelectItem; const itemCode = selectedItem ? selectedItem.code : ""; if (a.itemCode != itemCode) { return this.itemService$.getItem(a.itemCode).pipe( switchMap(child => { return this.itemService$.getItem(child.parentCode) .pipe(map(parent => { return { child, parent }; }), catchError(() => { const parent: IItem = null; return of({ child, parent }) }) ); }), map(data => new mapActions.SelectItemSuccess(data.child, data.parent)), catchError(error => of(new commonActions.Fail(error)))) } else { return []; } } ))); selectItemSuccessSetLayer$ = createEffect(() => this.actions$.pipe( ofType(mapActions.SELECTITEMSUCCESS), map((action: mapActions.SelectItemSuccess) => new mapActions.SetSelectedItemLayer(action.item) ) )); selectItemSuccess$ = createEffect(() => this.actions$.pipe( ofType(mapActions.SELECTITEMSUCCESS), switchMap((action: mapActions.SelectItemSuccess) => { if (!this.overrideSelectedItemLayer) { return this.itemService$.getFeature(action.item.code, "EPSG:3857").pipe( map((feature: any) => { const f = this._geojsonFormat.readFeature(feature); f.setId(action.item.code); return new mapActions.AddFeatureSuccess(f); }), catchError(error => of(new commonActions.Fail(error)))); } else { return EMPTY; } } ))); selectItemSuccessTemporal$ = createEffect(() => this.actions$.pipe( ofType(mapActions.SELECTITEMSUCCESS), switchMap((action: mapActions.SelectItemSuccess) => { if (action.item.itemType == "vnd.farmmaps.itemtype.temporal") { return this.itemService$.getChildItemList(action.item.code, null).pipe( map(items => new mapActions.SelectTemporalItemsSuccess( items.sort((a, b) => -(Date.parse(b.dataDate) - Date.parse(a.dataDate)) ) )), catchError(error => of(new commonActions.Fail(error)))); } else { return []; } } ))); uploadedItemClick$ = createEffect(() => this.actions$.pipe( ofType(commonActions.UPLOADEDFILECLICK), switchMap((action: commonActions.UploadedFileClick) => of(new mapActions.DoQuery(tassign(mapReducers.initialState.query.querystate, { itemCode: action.itemCode }))) ))); featureUpdate$ = createEffect(() => this.actions$.pipe( ofType(commonActions.DEVICEUPDATEEVENT), withLatestFrom(this.store$.select(mapReducers.selectGetFeatures)), mergeMap(([action, features]) => { const deviceUpdateEventAction = action as commonActions.DeviceUpdateEvent; let feature: Feature = null; for (const f of features) { if (f.getId() == deviceUpdateEventAction.itemCode) { feature = f; break; } } if (feature) { return of(new mapActions.UpdateFeatureSuccess(this.updateFeatureGeometry(feature, deviceUpdateEventAction))); } else { return []; } }))); itemUpdate$ = createEffect(() => this.actions$.pipe( ofType(commonActions.ITEMCHANGEDEVENT), withLatestFrom(this.store$.select(mapReducers.selectGetSelectedItem)), mergeMap(([action, selectedItem]) => { const itemChangedAction = action as commonActions.ItemChangedEvent; if (selectedItem && selectedItem.code == itemChangedAction.itemCode) { return this.itemService$.getItem(itemChangedAction.itemCode).pipe( switchMap(child => { return this.itemService$.getItem(child.parentCode) .pipe(map(parent => { return { child, parent }; }) ); }), map(data => new mapActions.SelectItemSuccess(data.child, data.parent)), catchError(error => of(new commonActions.Fail(error)))); } else { return []; } }))); getActionFromQueryState(queryState: IQueryState, inSearch: boolean): Observable | [] { if (!inSearch && (queryState.itemType || queryState.parentCode || queryState.itemCode || queryState.query || queryState.tags)) { var newAction: Action; if (queryState.itemCode && queryState.itemCode != "") { newAction = new mapActions.SelectItem(queryState.itemCode); } else { newAction = new mapActions.StartSearch(queryState); } } else { newAction = new mapActions.Clear(); } return of(newAction); } getLayerValue$ = createEffect(() => this.actions$.pipe( ofType(mapActions.GETLAYERVALUE), mergeMap((action: mapActions.GetLayerValue) => { const l = action.itemLayer.item.data["layers"][action.itemLayer.layerIndex]; const scale = l.scale ? l.scale : 1; return this.itemService$.getLayerValue(action.itemLayer.item.code, action.itemLayer.layerIndex, action.x, action.y).pipe( mergeMap((v: number) => { const a = []; if (v !== null) { if (l.renderer && l.renderer.colorMap && l.renderer.colorMap.colormapType == "manual") { l.renderer.colorMap.entries.forEach((e) => { if (e.value == v && e.label) { v = e.label; return; } }); a.push(new mapActions.GetLayerValueSuccess({ date: action.itemLayer.item.dataDate, value: v, layerName: l.name, quantity: "", unit: l.unit, scale: l.scale })); } else { a.push(new mapActions.GetLayerValueSuccess({ date: action.itemLayer.item.dataDate, value: v * scale, layerName: l.name, quantity: l.quantity, unit: l.unit, scale: l.scale })); } } return a; })) } ))); updateLayerValuesOnLayerAddedOrRemoved$ = createEffect(() => this.actions$.pipe( ofType(mapActions.ADDLAYER, mapActions.REMOVELAYER, mapActions.SELECTITEMSUCCESS, mapActions.SELECTTEMPORALITEMSSUCCESS, mapActions.NEXTTEMPORAL, mapActions.PREVIOUSTEMPORAL, mapActions.TOGGLELAYERVALUESENABLED, mapActions.SETLAYERINDEX, mapActions.SETSELECTEDITEMLAYER), withLatestFrom(this.store$.select(mapReducers.selectGetLayerValuesX)), withLatestFrom(this.store$.select(mapReducers.selectGetLayerValuesY)), map(([[action, x], y]) => new mapActions.SetLayerValuesLocation(x, y)) )); getLayerValues$ = createEffect(() => this.actions$.pipe( ofType(mapActions.SETLAYERVALUESLOCATION), withLatestFrom(this.store$.select(mapReducers.selectGetSelectedItemLayer)), withLatestFrom(this.store$.select(mapReducers.selectGetLayerValuesEnabled)), withLatestFrom(this.store$.select(mapReducers.selectGetOverlayLayers)), mergeMap(([[[action, selected], enabled], overlayLayers]) => { const layers = []; if (selected) { if (selected && (selected as TemporalItemLayer).selectedItemLayer) { selected = (selected as TemporalItemLayer).selectedItemLayer; } layers.push(selected); } overlayLayers.forEach((ol) => { if (ol != selected) layers.push(ol); }); const a = action as mapActions.SetLayerValuesLocation; const actions = []; if (enabled) { layers.forEach((ol) => { if ("vnd.farmmaps.itemtype.shape.processed,vnd.farmmaps.itemtype.geotiff.processed".indexOf(ol.item.itemType) >= 0) { actions.push(new mapActions.GetLayerValue(ol, a.x, a.y)); } }); } return actions; }))); setState$ = createEffect(() => this.actions$.pipe( ofType(mapActions.SETSTATE), withLatestFrom(this.store$.select(mapReducers.selectGetInSearch)), switchMap(([action, inSearch]) => { const a = action as mapActions.SetState; return this.getActionFromQueryState(a.queryState, inSearch); }))); escape$ = createEffect(() => this.actions$.pipe( ofType(commonActions.ESCAPE), switchMap((action) => { const a = action as commonActions.Escape; if (a.escapeKey) { return of(new mapActions.Clear()); } else { return EMPTY; } }))); setOverride$ = createEffect(() => this.actions$.pipe( ofType(ROUTER_NAVIGATED), switchMap(() => this.store$.select(selectRouteData as any)), switchMap((data: any) => { if (data && data["fm-map-map"]) { const params = data["fm-map-map"]; this.overrideSelectedItemLayer = params["overrideSelectedItemlayer"] ? params["overrideSelectedItemlayer"] : false; } else { this.overrideSelectedItemLayer = false; } return []; }) )); constructor(private actions$: Actions, private store$: Store, private folderService$: FolderService, private itemService$: ItemService, private featureIconService$: FeatureIconService, private itemTypeService$: ItemTypeService) { this._geojsonFormat = new GeoJSON(); this._wktFormat = new WKT(); } }