From 4fc85960106922ae15698a70ed622b4be7cb976c Mon Sep 17 00:00:00 2001 From: Igor Dykhta Date: Tue, 16 Aug 2022 20:08:57 +0300 Subject: [PATCH] [Chore]: layer-utils, map-utils refactor (#1923) --- src/components/kepler-gl.tsx | 32 ++- src/components/map-container.tsx | 182 +++++++++--------- src/components/plot-container.tsx | 34 ++-- src/constants/src/default-settings.ts | 1 + src/reducers/index.ts | 2 + src/reducers/map-style-updaters.ts | 7 +- src/utils/index.ts | 9 + src/utils/layer-utils.ts | 112 ++++++++++- src/utils/map-style-utils/mapbox-utils.ts | 2 +- src/utils/map-utils.ts | 30 +++ src/utils/utils.ts | 4 + .../component/map-container-test.js | 21 +- test/node/reducers/map-style-test.js | 2 + 13 files changed, 297 insertions(+), 141 deletions(-) create mode 100644 src/utils/map-utils.ts diff --git a/src/components/kepler-gl.tsx b/src/components/kepler-gl.tsx index 1236ab4f61..045d7ba5a3 100644 --- a/src/components/kepler-gl.tsx +++ b/src/components/kepler-gl.tsx @@ -134,24 +134,17 @@ export const mapFieldsSelector = (props: KeplerGLProps) => ({ mapStateActions: props.mapStateActions, // visState - editor: props.visState.editor, - datasets: props.visState.datasets, - layers: props.visState.layers, - layerOrder: props.visState.layerOrder, - layerData: props.visState.layerData, - layerBlending: props.visState.layerBlending, - filters: props.visState.filters, - interactionConfig: props.visState.interactionConfig, - hoverInfo: props.visState.hoverInfo, - clicked: props.visState.clicked, - mousePos: props.visState.mousePos, - animationConfig: props.visState.animationConfig, + visState: props.visState, // uiState activeSidePanel: props.uiState.activeSidePanel, mapControls: props.uiState.mapControls, readOnly: props.uiState.readOnly, - locale: props.uiState.locale + locale: props.uiState.locale, + + // mapStyle + topMapContainerProps: props.topMapContainerProps, + bottomMapContainerProps: props.bottomMapContainerProps }); export function getVisibleDatasets(datasets) { @@ -294,6 +287,9 @@ type KeplerGLBasicProps = { localeMessages?: {[key: string]: {[key: string]: string}}; dispatch: Dispatch; + + topMapContainerProps?: object; + bottomMapContainerProps?: object; }; type KeplerGLProps = KeplerGlState & KeplerGlActions & KeplerGLBasicProps; @@ -451,15 +447,9 @@ function KeplerGlFactory( const notificationPanelFields = notificationPanelSelector(this.props); const mapContainers = !isSplit - ? [] + ? [] : splitMaps.map((settings, index) => ( - + )); return ( diff --git a/src/components/map-container.tsx b/src/components/map-container.tsx index 71e9741d15..20b18183c7 100644 --- a/src/components/map-container.tsx +++ b/src/components/map-container.tsx @@ -51,23 +51,25 @@ import {setLayerBlending} from 'utils/gl-utils'; import {transformRequest} from 'utils/map-style-utils/mapbox-utils'; import { getLayerHoverProp, - renderDeckGlLayer, prepareLayersToRender, prepareLayersForDeck, LayerHoverProp } from 'utils/layer-utils'; // default-settings -import {ThreeDBuildingLayer} from '@kepler.gl/deckgl-layers'; import {FILTER_TYPES, GEOCODER_LAYER_ID, THROTTLE_NOTIFICATION_TIME} from '@kepler.gl/constants'; import ErrorBoundary from 'components/common/error-boundary'; import {observeDimensions, unobserveDimensions} from '../utils/observe-dimensions'; import {LOCALE_CODES} from '@kepler.gl/localization'; +import {computeDeckLayers} from '../utils/layer-utils'; +import {getMapLayersFromSplitMaps, onViewPortChange} from 'utils/map-utils'; +import {MapView} from '@deck.gl/core'; import { Datasets, Filter, InteractionConfig, + AnimationConfig, MapControls, MapState, MapStyle, @@ -94,7 +96,6 @@ const MAP_STYLE: {[key: string]: React.CSSProperties} = { const MAPBOXGL_STYLE_UPDATE = 'style.load'; const MAPBOXGL_RENDER = 'render'; -const TRANSITION_DURATION = 0; const nop = () => {}; export const Attribution = () => ( @@ -131,17 +132,24 @@ MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory] type MapboxStyle = string | object | undefined; interface MapContainerProps { - datasets: Datasets; - interactionConfig: InteractionConfig; - layerBlending: string; - layerOrder: number[]; - layerData: any[]; - layers: Layer[]; - filters: Filter[]; + visState: { + datasets: Datasets; + interactionConfig: InteractionConfig; + animationConfig: AnimationConfig; + layerBlending: string; + layerOrder: number[]; + layerData: any[]; + layers: Layer[]; + filters: Filter[]; + mousePos: any; + clicked?: any; + hoverInfo?: any; + mapLayers?: SplitMapLayers | null; + editor: any; + }; mapState: MapState; mapControls: MapControls; mapStyle: {bottomMapStyle?: MapboxStyle; topMapStyle?: MapboxStyle} & MapStyle; - mousePos: any; mapboxApiAccessToken: string; mapboxApiUrl: string; visStateActions: typeof VisStateActions; @@ -152,9 +160,6 @@ interface MapContainerProps { primary?: boolean; // primary one will be reporting its size to appState readOnly?: boolean; isExport?: boolean; - clicked?: any; - hoverInfo?: any; - mapLayers?: SplitMapLayers | null; onMapToggleLayer?: Function; onMapStyleLoaded?: Function; onMapRender?: Function; @@ -167,6 +172,9 @@ interface MapContainerProps { deckGlProps?: any; onDeckInitialized?: (a: any, b: any) => void; onViewStateChange?: (viewport: Viewport) => void; + + topMapContainerProps: any; + bottomMapContainerProps: any; } export default function MapContainerFactory( @@ -225,10 +233,16 @@ export default function MapContainerFactory( } }; - layersSelector = props => props.layers; - layerDataSelector = props => props.layerData; - mapLayersSelector = props => props.mapLayers; - layerOrderSelector = props => props.layerOrder; + layersSelector = props => props.visState.layers; + layerDataSelector = props => props.visState.layerData; + splitMapSelector = props => props.visState.splitMaps; + splitMapIndexSelector = props => props.index; + mapLayersSelector = createSelector( + this.splitMapSelector, + this.splitMapIndexSelector, + (splitMaps, splitMapIndex) => getMapLayersFromSplitMaps(splitMaps, splitMapIndex) + ); + layerOrderSelector = props => props.visState.layerOrder; layersToRenderSelector = createSelector( this.layersSelector, this.layerDataSelector, @@ -240,7 +254,7 @@ export default function MapContainerFactory( this.layerDataSelector, prepareLayersForDeck ); - filtersSelector = props => props.filters; + filtersSelector = props => props.visState.filters; polygonFilters = createSelector(this.filtersSelector, filters => filters.filter(f => f.type === FILTER_TYPES.polygon) ); @@ -259,7 +273,7 @@ export default function MapContainerFactory( }; _onLayerSetDomain = (idx: number, colorDomain: VisualChannelDomain) => { - this.props.visStateActions.layerConfigChange(this.props.layers[idx], { + this.props.visStateActions.layerConfigChange(this.props.visState.layers[idx], { colorDomain } as Partial); }; @@ -311,7 +325,7 @@ export default function MapContainerFactory( } _onBeforeRender = ({gl}) => { - setLayerBlending(gl, this.props.layerBlending); + setLayerBlending(gl, this.props.visState.layerBlending); }; _onDeckError = (error, layer) => { @@ -342,12 +356,14 @@ export default function MapContainerFactory( // TODO: move this into reducer so it can be tested const { mapState, - hoverInfo, - clicked, - datasets, - interactionConfig, - layers, - mousePos: {mousePosition, coordinate, pinned} + visState: { + hoverInfo, + clicked, + datasets, + interactionConfig, + layers, + mousePos: {mousePosition, coordinate, pinned} + } } = this.props; const layersToRender = this.layersToRenderSelector(this.props); @@ -430,54 +446,41 @@ export default function MapContainerFactory( const { mapState, mapStyle, - layerData, - layerOrder, - layers, + visState, visStateActions, mapboxApiAccessToken, - mapboxApiUrl + mapboxApiUrl, + deckGlProps, + index } = this.props; - // initialise layers from props if exists - let deckGlLayers = this.props.deckGlProps?.layers || []; - - // wait until data is ready before render data layers - if (layerData && layerData.length) { - // last layer render first - const dataLayers = layerOrder - .slice() - .reverse() - .filter(idx => layersForDeck[layers[idx].id]) - .reduce((overlays, idx) => { - const layerCallbacks = { - onSetLayerDomain: val => this._onLayerSetDomain(idx, val) - }; - const layerOverlay = renderDeckGlLayer(this.props, layerCallbacks, idx); - return overlays.concat(layerOverlay || []); - }, []); - deckGlLayers = deckGlLayers.concat(dataLayers); - } + const deckGlLayers = computeDeckLayers( + { + visState, + mapState, + mapStyle + }, + { + mapIndex: index, + primaryMap: options.primaryMap, + mapboxApiAccessToken, + mapboxApiUrl, + layersForDeck + }, + this._onLayerSetDomain, + deckGlProps + ); - if (mapStyle.visibleLayerGroups['3d building'] && options.primaryMap) { - deckGlLayers.push( - new ThreeDBuildingLayer({ - id: '_keplergl_3d-building', - mapboxApiAccessToken, - mapboxApiUrl, - threeDBuildingColor: mapStyle.threeDBuildingColor, - updateTriggers: { - getFillColor: mapStyle.threeDBuildingColor - } - }) - ); - } + const views = deckGlProps?.views ? deckGlProps?.views() : new MapView({}); return ( { - const {width, height, ...restViewState} = viewState; - const {primary} = this.props; - // react-map-gl sends 0,0 dimensions during initialization - // after we have received proper dimensions from observeDimensions - const next = { - ...(width > 0 && height > 0 ? viewState : restViewState), - // enabling transition in two maps may lead to endless update loops - transitionDuration: primary ? TRANSITION_DURATION : 0 - }; - if (typeof this.props.onViewStateChange === 'function') { - this.props.onViewStateChange(next); - } - this.props.mapStateActions.updateMap(next); + onViewPortChange( + viewState, + this.props.mapStateActions.updateMap, + this.props.onViewStateChange, + this.props.primary + ); }; _toggleMapControl = panelId => { @@ -561,12 +558,11 @@ export default function MapContainerFactory( /* eslint-disable complexity */ _renderMap() { const { + visState, mapState, mapStyle, mapStateActions, - layers, MapComponent = MapboxGLMap, - datasets, mapboxApiAccessToken, mapboxApiUrl, mapControls, @@ -574,12 +570,14 @@ export default function MapContainerFactory( locale, uiStateActions, visStateActions, - interactionConfig, - editor, index, - primary + primary, + bottomMapContainerProps, + topMapContainerProps } = this.props; + const {layers, datasets, editor, interactionConfig, hoverInfo} = visState; + const layersToRender = this.layersToRenderSelector(this.props); const layersForDeck = this.layersForDeckSelector(this.props); @@ -596,7 +594,7 @@ export default function MapContainerFactory( transformRequest }; - const hasGeocoderLayer = layers.find(l => l.id === GEOCODER_LAYER_ID); + const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID)); const isSplit = Boolean(mapState.isSplit); return ( @@ -627,11 +625,12 @@ export default function MapContainerFactory( mapHeight={mapState.height} /> 'pointer' : undefined} + {...bottomMapContainerProps} + ref={this._setMapboxMap} + getCursor={hoverInfo ? () => 'pointer' : undefined} onMouseMove={this.props.visStateActions.onMouseMove} > {this._renderDeckOverlay(layersForDeck, {primaryMap: true})} @@ -640,7 +639,12 @@ export default function MapContainerFactory( {mapStyle.topMapStyle || hasGeocoderLayer ? (
- + {this._renderDeckOverlay({[GEOCODER_LAYER_ID]: hasGeocoderLayer})}
diff --git a/src/components/plot-container.tsx b/src/components/plot-container.tsx index 202d0e48d7..af85078346 100644 --- a/src/components/plot-container.tsx +++ b/src/components/plot-container.tsx @@ -67,13 +67,6 @@ const StyledMapContainer = styled.div` display: flex; `; -const deckGlProps = { - glOptions: { - preserveDrawingBuffer: true, - useDevicePixels: false - } -}; - interface PlotContainerProps { width?: number; height?: number; @@ -165,8 +158,10 @@ export default function PlotContainerFactory( render() { const {exportImageSetting, mapFields, splitMaps = []} = this.props; + const {mapState, visState} = mapFields; + const {layers, layerData} = visState; const {imageSize, legend} = exportImageSetting; - const {mapState} = mapFields; + const isSplit = splitMaps && splitMaps.length > 1; const size = { @@ -185,9 +180,8 @@ export default function PlotContainerFactory( // center and all layer bounds if (exportImageSetting.center) { - const renderedLayers = mapFields.layers.filter( - (layer, idx) => - layer.id !== GEOCODER_LAYER_ID && layer.shouldRenderLayer(mapFields.layerData[idx]) + const renderedLayers = layers.filter( + (layer, idx) => layer.id !== GEOCODER_LAYER_ID && layer.shouldRenderLayer(layerData[idx]) ); const bounds = findMapBounds(renderedLayers); const centerAndZoom = getCenterAndZoomFromBounds(bounds, {width, height}); @@ -216,21 +210,21 @@ export default function PlotContainerFactory( MapComponent: StaticMap, onMapRender: this._onMapRender, isExport: true, - deckGlProps + deckGlProps: { + ...mapFields.deckGlProps, + glOptions: { + preserveDrawingBuffer: true, + useDevicePixels: false + } + } }; const mapContainers = !isSplit ? ( ) : ( - + {splitMaps.map((settings, index) => ( - + ))} ); diff --git a/src/constants/src/default-settings.ts b/src/constants/src/default-settings.ts index 5adf61ddfd..c54c3e0d3a 100644 --- a/src/constants/src/default-settings.ts +++ b/src/constants/src/default-settings.ts @@ -37,6 +37,7 @@ export const ACTION_PREFIX = '@@kepler.gl/'; export const CLOUDFRONT = 'https://d1a3f4spazzrp4.cloudfront.net/kepler.gl'; export const ICON_PREFIX = `${CLOUDFRONT}/geodude`; export const DEFAULT_MAPBOX_API_URL = 'https://api.mapbox.com'; +export const TRANSITION_DURATION = 0; // Modal Ids /** diff --git a/src/reducers/index.ts b/src/reducers/index.ts index a8806c9ca0..ce85fca1c4 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -117,3 +117,5 @@ export type { Notifications, UiState } from './ui-state-updaters'; + +export * as providerStateUpdaters from './provider-state-updaters'; \ No newline at end of file diff --git a/src/reducers/map-style-updaters.ts b/src/reducers/map-style-updaters.ts index 3e039a1cc1..86119cc745 100644 --- a/src/reducers/map-style-updaters.ts +++ b/src/reducers/map-style-updaters.ts @@ -97,7 +97,8 @@ export type MapStyle = { inputStyle: InputStyle; threeDBuildingColor: RGBColor; custom3DBuildingColor: boolean; - + bottomMapStyle: any; + topMapStyle: any; initialState?: MapStyle; }; @@ -125,7 +126,9 @@ const getDefaultState = (): MapStyle => { mapStylesReplaceDefault: false, inputStyle: getInitialInputStyle(), threeDBuildingColor: hexToRgb(DEFAULT_BLDG_COLOR), - custom3DBuildingColor: false + custom3DBuildingColor: false, + bottomMapStyle: undefined, + topMapStyle: undefined }; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 62a001ab0e..8610371a55 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -73,3 +73,12 @@ export * from './gpu-filter-utils'; export * from './interaction-utils'; export * from './layer-utils'; export * from './observe-dimensions'; + +// Layers +export {computeDeckLayers} from './layer-utils'; + +// Mapbox +export {transformRequest} from './map-style-utils/mapbox-utils'; + +// Map +export {onViewPortChange} from './map-utils'; diff --git a/src/utils/layer-utils.ts b/src/utils/layer-utils.ts index 1b64d3b3c7..48a87f683c 100644 --- a/src/utils/layer-utils.ts +++ b/src/utils/layer-utils.ts @@ -25,6 +25,9 @@ import { OVERLAY_TYPE_CONST } from '@kepler.gl/layers'; import {GEOCODER_LAYER_ID} from '@kepler.gl/constants'; +import {ThreeDBuildingLayer} from '../deckgl-layers'; +import {getMapLayersFromSplitMaps} from './map-utils'; +import {isFunction} from 'utils/utils'; import {VisState, TooltipField, CompareType, SplitMapLayers} from 'reducers/vis-state-updaters'; import KeplerTable, {Field} from './table-utils/kepler-table'; @@ -220,7 +223,7 @@ export function prepareLayersForDeck( export function prepareLayersToRender( layers: Layer[], layerData: VisState['layerData'], - mapLayers?: SplitMapLayers + mapLayers?: SplitMapLayers | null ): { [key: string]: boolean; } { @@ -232,3 +235,110 @@ export function prepareLayersToRender( {} ); } + +export function getCustomDeckLayers(deckGlProps) { + const bottomDeckLayers = Array.isArray(deckGlProps?.layers) + ? deckGlProps?.layers + : isFunction(deckGlProps?.layers) + ? deckGlProps?.layers() + : []; + const topDeckLayers = Array.isArray(deckGlProps?.topLayers) + ? deckGlProps?.topLayers + : isFunction(deckGlProps?.topLayers) + ? deckGlProps?.topLayers() + : []; + + return [bottomDeckLayers, topDeckLayers]; +} + +export type ComputeDeckLayersProps = { + mapIndex?: number; + mapboxApiAccessToken?: string; + mapboxApiUrl?: string; + primaryMap?: boolean; + layersForDeck?: {[key: string]: boolean}; +}; + +export function computeDeckLayers( + {visState, mapState, mapStyle}: any, + options?: ComputeDeckLayersProps, + onSetLayerDomain?: (idx: number, value: any) => void, + deckGlProps?: any +): Layer[] { + const { + datasets, + layers, + layerOrder, + layerData, + hoverInfo, + clicked, + interactionConfig, + animationConfig, + splitMaps + } = visState; + + const {mapIndex, mapboxApiAccessToken, mapboxApiUrl, primaryMap, layersForDeck} = options || {}; + + if (!layerData || !layerData.length) { + return []; + } + + const mapLayers = getMapLayersFromSplitMaps(splitMaps, mapIndex || 0); + + const currentLayersForDeck = layersForDeck || prepareLayersForDeck(layers, layerData); + + const dataLayers = layerOrder + .slice() + .reverse() + .filter(idx => currentLayersForDeck[layers[idx].id]) + .reduce((overlays, idx) => { + const layerCallbacks = onSetLayerDomain + ? { + onSetLayerDomain: val => onSetLayerDomain(idx, val) + } + : {}; + const layerOverlay = renderDeckGlLayer( + { + datasets, + layers, + layerData, + hoverInfo, + clicked, + mapState, + interactionConfig, + animationConfig, + mapLayers + }, + layerCallbacks, + idx + ); + return overlays.concat(layerOverlay || []); + }, []); + + if (!primaryMap) { + return dataLayers; + } + + if ( + mapStyle?.visibleLayerGroups['3d building'] && + primaryMap && + mapboxApiAccessToken && + mapboxApiUrl + ) { + dataLayers.push( + new ThreeDBuildingLayer({ + id: '_keplergl_3d-building', + mapboxApiAccessToken, + mapboxApiUrl, + threeDBuildingColor: mapStyle.threeDBuildingColor, + updateTriggers: { + getFillColor: mapStyle.threeDBuildingColor + } + }) + ); + } + + const [customBottomDeckLayers, customTopDeckLayers] = getCustomDeckLayers(deckGlProps); + + return [...customBottomDeckLayers, ...dataLayers, ...customTopDeckLayers]; +} diff --git a/src/utils/map-style-utils/mapbox-utils.ts b/src/utils/map-style-utils/mapbox-utils.ts index 3595384806..9651bb9d8c 100644 --- a/src/utils/map-style-utils/mapbox-utils.ts +++ b/src/utils/map-style-utils/mapbox-utils.ts @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -export const transformRequest = url => { +export const transformRequest = (url: string): {url: string} => { const isMapboxRequest = url.slice(8, 22) === 'api.mapbox.com' || url.slice(10, 26) === 'tiles.mapbox.com'; diff --git a/src/utils/map-utils.ts b/src/utils/map-utils.ts new file mode 100644 index 0000000000..a6735f3374 --- /dev/null +++ b/src/utils/map-utils.ts @@ -0,0 +1,30 @@ +import {TRANSITION_DURATION} from '@kepler.gl/constants'; +import {SplitMapLayers, SplitMap} from 'reducers/vis-state-updaters'; + +export const onViewPortChange = ( + viewState: any, + onUpdateMap: (next: any) => any, + onViewStateChange?: (next: any) => void | null, + primary: boolean = false +): void => { + const {width, height, ...restViewState} = viewState; + // react-map-gl sends 0,0 dimensions during initialization + // after we have received proper dimensions from observeDimensions + const next = { + ...(width > 0 && height > 0 ? viewState : restViewState), + // enabling transition in two maps may lead to endless update loops + transitionDuration: primary ? TRANSITION_DURATION : 0 + }; + if (onViewStateChange && typeof onViewStateChange === 'function') { + onViewStateChange(next); + } + + onUpdateMap(next); +}; + +export const getMapLayersFromSplitMaps = ( + splitMaps: SplitMap[], + mapIndex: number +): SplitMapLayers | null => { + return splitMaps[mapIndex]?.layers; +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 82de97975b..45e167ee15 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -224,3 +224,7 @@ export function filterObjectByPredicate(obj, predicate) { {} ); } + +export function isFunction(func) { + return typeof func === 'function'; +} diff --git a/test/browser-headless/component/map-container-test.js b/test/browser-headless/component/map-container-test.js index 7cfbc3e41b..b5f9672dbb 100644 --- a/test/browser-headless/component/map-container-test.js +++ b/test/browser-headless/component/map-container-test.js @@ -176,19 +176,26 @@ test('MapContainerFactory - _renderDeckOverlay', t => { t.ok(hoverEvents[0].info.object, 'should have info.object'); t.deepEqual( hoverEvents[0].info.object, - props.layerData[0].data[15], + props.visState.layerData[0].data[15], 'object should be layer data' ); - t.is(hoverEvents[0].info.layer.id, props.layers[0].id, 'layer id should be correct'); + t.is( + hoverEvents[0].info.layer.id, + props.visState.layers[0].id, + 'layer id should be correct' + ); // asign info object to to state and test map popover const propsWithHoverInfo = { ...initialProps, - hoverInfo: hoverEvents[0].info, - mousePos: { - ...initialProps.mousePos, - coordinate: expectedCoordinate, - mousePosition: [200, 200] + visState: { + ...initialProps.visState, + hoverInfo: hoverEvents[0].info, + mousePos: { + ...initialProps.visState.mousePos, + coordinate: expectedCoordinate, + mousePosition: [200, 200] + } } }; diff --git a/test/node/reducers/map-style-test.js b/test/node/reducers/map-style-test.js index 66f3f6b48a..0ab762c139 100644 --- a/test/node/reducers/map-style-test.js +++ b/test/node/reducers/map-style-test.js @@ -173,6 +173,8 @@ test('#mapStyleReducer -> RECEIVE_MAP_CONFIG', t => { inputStyle: getInitialInputStyle(), threeDBuildingColor: [1, 2, 3], custom3DBuildingColor: true, + bottomMapStyle: undefined, + topMapStyle: undefined, initialState: {} };