import { AnyAction , Dispatch } from 'redux' ; import { ThunkDispatch } from 'redux-thunk' ; import bbox from '@turf/bbox' ; import uuid from 'uuid/v4' ; import { multiPoint } from '@turf/helpers' ; import { FeatureCollection } from 'geojson' ; import { MapStoreState } from '../reducers/store' ; import { KBN_IS_CENTROID_FEATURE , LAYER_TYPE , SOURCE_DATA_REQUEST_ID , } from '../../common/constants' ; import { getDataFilters , getDataRequestDescriptor , getLayerById , getLayerList , } from '../selectors/map_selectors' ; import { cancelRequest , registerCancelCallback , unregisterCancelCallback , getEventHandlers , ResultMeta , } from '../reducers/non_serializable_instances' ; import { updateTooltipStateForLayer } from './tooltip_actions' ; import { LAYER_DATA_LOAD_ENDED , LAYER_DATA_LOAD_ERROR , LAYER_DATA_LOAD_STARTED , SET_GOTO , SET_LAYER_ERROR_STATUS , SET_LAYER_STYLE_META , UPDATE_LAYER_PROP , UPDATE_SOURCE_DATA_REQUEST , } from './map_action_constants' ; import { ILayer } from '../classes/layers/layer' ; import { IVectorLayer } from '../classes/layers/vector_layer' ; import { DataRequestMeta , MapExtent , DataFilters } from '../../common/descriptor_types' ; import { DataRequestAbortError } from '../classes/util/data_request' ; import { scaleBounds , turfBboxToBounds } from '../../common/elasticsearch_util' ; const FIT_TO_BOUNDS_SCALE_FACTOR = 0.1 ; export type DataRequestContext = { startLoading ( dataId , requestToken , requestMeta ? ) : void ; stopLoading ( dataId , requestToken , data , resultsMeta ? ) : void ; onLoadError ( dataId , requestToken , errorMessage ) : void ; onJoinError ( errorMessage ) : void ; updateSourceData ( newData ) : void ; isRequestStillActive ( dataId , requestToken ) : boolean ; registerCancelCallback ( requestToken , callback ) : void ; dataFilters : DataFilters ; forceRefreshDueToDrawing : boolean ; isForceRefresh : boolean ; } ; export function clearDataRequests ( layer ) { return ( dispatch ) => { layer . getInFlightRequestTokens ( ) . forEach ( ( requestToken ) => { dispatch ( cancelRequest ( requestToken ) ) ; } ) ; dispatch ( { type : UPDATE_LAYER_PROP , id : layer . getId ( ) , propName : '__dataRequests' , newValue : [ ] , } ) ; } ; } export function cancelAllInFlightRequests ( ) { return ( dispatch , getState ) => { getLayerList ( getState ( ) ) . forEach ( ( layer ) => { dispatch ( clearDataRequests ( layer ) ) ; } ) ; } ; } export function updateStyleMeta ( layerId ) { return async ( dispatch , getState ) => { const layer = getLayerById ( layerId , getState ( ) ) ; if ( ! layer ) { return ; } const styleMeta = await layer . getStyleMetaDescriptorFromLocalFeatures ( ) ; if ( ! styleMeta ) { return ; } dispatch ( { type : SET_LAYER_STYLE_META , layerId , styleMeta , } ) ; } ; } function getDataRequestContext ( dispatch , getState , layerId , forceRefreshDueToDrawing , isForceRefresh ) { return { dataFilters : getDataFilters ( getState ( ) ) , startLoading : ( dataId , requestToken , meta ) => dispatch ( startDataLoad ( layerId , dataId , requestToken , meta ) ) , stopLoading : ( dataId , requestToken , data , meta ) => dispatch ( endDataLoad ( layerId , dataId , requestToken , data , meta ) ) , onLoadError : ( dataId , requestToken , errorMessage ) => dispatch ( onDataLoadError ( layerId , dataId , requestToken , errorMessage ) ) , onJoinError : ( errorMessage ) => dispatch ( setLayerDataLoadErrorStatus ( layerId , errorMessage ) ) , updateSourceData : ( newData ) => { dispatch ( updateSourceDataRequest ( layerId , newData ) ) ; } , isRequestStillActive : ( dataId , requestToken ) => { const dataRequest = getDataRequestDescriptor ( getState ( ) , layerId , dataId ) ; if ( ! dataRequest ) { return false ; } return dataRequest . dataRequestToken === requestToken ; } , registerCancelCallback : ( requestToken , callback ) => dispatch ( registerCancelCallback ( requestToken , callback ) ) , forceRefreshDueToDrawing , isForceRefresh , } ; } export function syncDataForAllLayers ( isForceRefresh ) { return async ( dispatch , getState ) => { const syncPromises = getLayerList ( getState ( ) ) . map ( ( layer ) => { return dispatch ( syncDataForLayer ( layer , isForceRefresh ) ) ; } ) ; await Promise . all ( syncPromises ) ; } ; } function syncDataForAllJoinLayers ( ) { return async ( dispatch , getState ) => { const syncPromises = getLayerList ( getState ( ) ) . filter ( ( layer ) => { return 'hasJoins' in layer ? ( layer as IVectorLayer ) . hasJoins ( ) : false ; } ) . map ( ( layer ) => { return dispatch ( syncDataForLayer ( layer , false ) ) ; } ) ; await Promise . all ( syncPromises ) ; } ; } export function syncDataForLayerDueToDrawing ( layer ) { return async ( dispatch , getState ) => { const dataRequestContext = getDataRequestContext ( dispatch , getState , layer . getId ( ) , true , false ) ; if ( ! layer . isVisible ( ) || ! layer . showAtZoomLevel ( dataRequestContext . dataFilters . zoom ) ) { return ; } await layer . syncData ( dataRequestContext ) ; } ; } export function syncDataForLayer ( layer , isForceRefresh ) { return async ( dispatch , getState ) => { const dataRequestContext = getDataRequestContext ( dispatch , getState , layer . getId ( ) , false , isForceRefresh ) ; if ( ! layer . isVisible ( ) || ! layer . showAtZoomLevel ( dataRequestContext . dataFilters . zoom ) ) { return ; } await layer . syncData ( dataRequestContext ) ; } ; } export function syncDataForLayerId ( layerId , isForceRefresh ) { return async ( dispatch , getState ) => { const layer = getLayerById ( layerId , getState ( ) ) ; if ( layer ) { dispatch ( syncDataForLayer ( layer , isForceRefresh ) ) ; } } ; } function setLayerDataLoadErrorStatus ( layerId , errorMessage ) { return { type : SET_LAYER_ERROR_STATUS , isInErrorState : errorMessage !== null , layerId , errorMessage , } ; } function startDataLoad ( layerId , dataId , requestToken , meta ) { return ( dispatch , getState ) => { const layer = getLayerById ( layerId , getState ( ) ) ; if ( layer ) { dispatch ( cancelRequest ( layer . getPrevRequestToken ( dataId ) ) ) ; } const eventHandlers = getEventHandlers ( getState ( ) ) ; if ( eventHandlers && eventHandlers . onDataLoad ) { eventHandlers . onDataLoad ( { layerId , dataId , } ) ; } dispatch ( { meta , type : LAYER_DATA_LOAD_STARTED , layerId , dataId , requestToken , } ) ; } ; } function endDataLoad ( layerId , dataId , requestToken , data , meta ) { return ( dispatch , getState ) => { dispatch ( unregisterCancelCallback ( requestToken ) ) ; const dataRequest = getDataRequestDescriptor ( getState ( ) , layerId , dataId ) ; if ( dataRequest && dataRequest . dataRequestToken !== requestToken ) { throw new DataRequestAbortError ( ) ; } if ( dataId === SOURCE_DATA_REQUEST_ID ) { const features = data && 'features' in data ? ( data as FeatureCollection ) . features : [ ] ; const layer = getLayerById ( layerId , getState ( ) ) ; const eventHandlers = getEventHandlers ( getState ( ) ) ; if ( eventHandlers && eventHandlers . onDataLoadEnd ) { const resultMeta = { } ; if ( layer && layer . getType ( ) === LAYER_TYPE . GEOJSON_VECTOR ) { const featuresWithoutCentroids = features . filter ( ( feature ) => { return feature . properties ? ! feature . properties [ KBN_IS_CENTROID_FEATURE ] : true ; } ) ; resultMeta . featuresCount = featuresWithoutCentroids . length ; } eventHandlers . onDataLoadEnd ( { layerId , dataId , resultMeta , } ) ; } if ( layer ) { dispatch ( updateTooltipStateForLayer ( layer , features ) ) ; } } dispatch ( { type : LAYER_DATA_LOAD_ENDED , layerId , dataId , data , meta , requestToken , } ) ; dispatch ( setLayerDataLoadErrorStatus ( layerId , null ) ) ; dispatch ( updateStyleMeta ( layerId ) ) ; } ; } function onDataLoadError ( layerId , dataId , requestToken , errorMessage ) { return async ( dispatch , getState ) => { dispatch ( unregisterCancelCallback ( requestToken ) ) ; if ( dataId === SOURCE_DATA_REQUEST_ID ) { const eventHandlers = getEventHandlers ( getState ( ) ) ; if ( eventHandlers && eventHandlers . onDataLoadError ) { eventHandlers . onDataLoadError ( { layerId , dataId , errorMessage , } ) ; } const layer = getLayerById ( layerId , getState ( ) ) ; if ( layer ) { dispatch ( updateTooltipStateForLayer ( layer ) ) ; } } dispatch ( { type : LAYER_DATA_LOAD_ERROR , layerId , dataId , requestToken , } ) ; dispatch ( setLayerDataLoadErrorStatus ( layerId , errorMessage ) ) ; } ; } export function updateSourceDataRequest ( layerId , newData ) { return ( dispatch , getState ) => { dispatch ( { type : UPDATE_SOURCE_DATA_REQUEST , dataId : SOURCE_DATA_REQUEST_ID , layerId , newData , } ) ; if ( 'features' in newData ) { const layer = getLayerById ( layerId , getState ( ) ) ; if ( layer ) { dispatch ( updateTooltipStateForLayer ( layer , ( newData as FeatureCollection ) . features ) ) ; } } dispatch ( updateStyleMeta ( layerId ) ) ; } ; } export function fitToLayerExtent ( layerId ) { return async ( dispatch , getState ) => { const targetLayer = getLayerById ( layerId , getState ( ) ) ; if ( targetLayer ) { try { const bounds = await targetLayer . getBounds ( getDataRequestContext ( dispatch , getState , layerId , false , false ) ) ; if ( bounds ) { await dispatch ( setGotoWithBounds ( scaleBounds ( bounds , FIT_TO_BOUNDS_SCALE_FACTOR ) ) ) ; } } catch ( error ) { if ( ! ( error instanceof DataRequestAbortError ) ) { console . warn ( 'Unhandled getBounds error for layer. Only DataRequestAbortError should be surfaced' , error ) ; } return ; } } } ; } export function fitToDataBounds ( onNoBounds ? ) { return async ( dispatch , getState ) => { const layerList = getLayerList ( getState ( ) ) ; if ( ! layerList . length ) { return ; } const boundsPromises = layerList . map ( async ( layer ) => { if ( ! ( await layer . isFittable ( ) ) ) { return null ; } return layer . getBounds ( getDataRequestContext ( dispatch , getState , layer . getId ( ) , false , false ) ) ; } ) ; let bounds ; try { bounds = await Promise . all ( boundsPromises ) ; } catch ( error ) { if ( ! ( error instanceof DataRequestAbortError ) ) { console . warn ( 'Unhandled getBounds error for layer. Only DataRequestAbortError should be surfaced' , error ) ; } return ; } const corners = [ ] ; for ( let i = 0 ; i < bounds . length ; i ++ ) { const b = bounds [ i ] ; if ( b === null || b . minLon === Infinity || b . maxLon === Infinity || b . minLat === - Infinity || b . maxLat === - Infinity ) { continue ; } corners . push ( [ b . minLon , b . minLat ] ) ; corners . push ( [ b . maxLon , b . maxLat ] ) ; } if ( ! corners . length ) { if ( onNoBounds ) { onNoBounds ( ) ; } return ; } const dataBounds = turfBboxToBounds ( bbox ( multiPoint ( corners ) ) ) ; dispatch ( setGotoWithBounds ( scaleBounds ( dataBounds , FIT_TO_BOUNDS_SCALE_FACTOR ) ) ) ; } ; } let lastSetQueryCallId = '' ; export function autoFitToBounds ( ) { return async ( dispatch ) => { const localSetQueryCallId = uuid ( ) ; lastSetQueryCallId = localSetQueryCallId ; await dispatch ( syncDataForAllJoinLayers ( ) ) ; if ( localSetQueryCallId === lastSetQueryCallId ) { function onNoBounds ( ) { if ( localSetQueryCallId === lastSetQueryCallId ) { dispatch ( syncDataForAllLayers ( false ) ) ; } } dispatch ( fitToDataBounds ( onNoBounds ) ) ; } } ; } function setGotoWithBounds ( bounds ) { return { type : SET_GOTO , bounds , } ; }