Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Unlocked split map viewports #2170

Merged
merged 2 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/actions/src/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ export const ActionTypes = {
UPDATE_MAP: `${ACTION_PREFIX}UPDATE_MAP`,
FIT_BOUNDS: `${ACTION_PREFIX}FIT_BOUNDS`,
TOGGLE_PERSPECTIVE: `${ACTION_PREFIX}TOGGLE_PERSPECTIVE`,
TOGGLE_SPLIT_MAP: `${ACTION_PREFIX}TOGGLE_SPLIT_MAP`,
TOGGLE_FULLSCREEN: `${ACTION_PREFIX}TOGGLE_FULLSCREEN`,
TOGGLE_SPLIT_MAP: `${ACTION_PREFIX}TOGGLE_SPLIT_MAP`,
TOGGLE_SPLIT_MAP_VIEWPORT: `${ACTION_PREFIX}TOGGLE_SPLIT_MAP_VIEWPORT`,

// mapStyle
MAP_CONFIG_CHANGE: `${ACTION_PREFIX}MAP_CONFIG_CHANGE`,
Expand Down
46 changes: 39 additions & 7 deletions src/actions/src/map-state-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const fitBounds: (
{type: typeof ActionTypes.FIT_BOUNDS}
> = createAction(ActionTypes.FIT_BOUNDS, (bounds: Bounds) => ({payload: bounds}));

export type UpdateMapUpdaterAction = {payload: Viewport};
export type UpdateMapUpdaterAction = {payload: {viewport: Viewport; mapIndex?: number}};
/**
* Update map viewport
* @memberof mapStateActions
Expand All @@ -67,18 +67,25 @@ export type UpdateMapUpdaterAction = {payload: Viewport};
* @param {Number} [viewport.latitude] Latitude center of viewport on map in mercator projection
* @param {Number} [viewport.longitude] Longitude Center of viewport on map in mercator projection
* @param {boolean} [viewport.dragRotate] Whether to enable drag and rotate map into perspective viewport
* @param {number} mapIndex Index of which map to update the viewport of
* @public
* @example
* import {updateMap} from 'kepler.gl/actions';
* this.props.dispatch(updateMap({latitude: 37.75043, longitude: -122.34679, width: 800, height: 1200}));
* this.props.dispatch(updateMap({latitude: 37.75043, longitude: -122.34679, width: 800, height: 1200}, 0));
*/

export const updateMap: (
payload: Viewport
) => Merge<
UpdateMapUpdaterAction,
{type: typeof ActionTypes.UPDATE_MAP}
> = createAction(ActionTypes.UPDATE_MAP, (viewport: Viewport) => ({payload: viewport}));
viewport: Viewport,
mapIndex?: number
) => Merge<UpdateMapUpdaterAction, {type: typeof ActionTypes.UPDATE_MAP}> = createAction(
ActionTypes.UPDATE_MAP,
(viewport: Viewport, mapIndex?: number) => ({
payload: {
viewport,
mapIndex
}
})
);

export type ToggleSplitMapUpdaterAction = {
payload: number;
Expand All @@ -99,6 +106,31 @@ export const toggleSplitMap: (
{type: typeof ActionTypes.TOGGLE_SPLIT_MAP}
> = createAction(ActionTypes.TOGGLE_SPLIT_MAP, (index: number) => ({payload: index}));

export type ToggleSplitMapViewportUpdaterAction = {
payload: {
isViewportSynced?: boolean;
isZoomLocked?: boolean;
};
};

/**
* For split maps, toggle between having (un)synced viewports and (un)locked zooms
* @memberof mapStateActions
* @param {Object} syncInfo
* @param {boolean} [syncInfo.isViewportSynced] Are the 2 split maps having synced viewports?
* @param {boolean} [syncInfo.isZoomLocked] If split, are the zooms locked to each other or independent?
*/
export const toggleSplitMapViewport: (payload: {
isViewportSynced?: boolean;
isZoomLocked?: boolean;
}) => Merge<
ToggleSplitMapViewportUpdaterAction,
{type: typeof ActionTypes.TOGGLE_SPLIT_MAP_VIEWPORT}
> = createAction(
ActionTypes.TOGGLE_SPLIT_MAP_VIEWPORT,
(syncInfo: ToggleSplitMapViewportUpdaterAction['payload']) => ({payload: syncInfo})
);

/**
* This declaration is needed to group actions in docs
*/
Expand Down
6 changes: 3 additions & 3 deletions src/actions/src/vis-state-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,17 +876,17 @@ export function toggleFilterFeature(
}

export type OnLayerHoverUpdaterAction = {
info: PickInfo<any> | null;
info: (PickInfo<any> & {mapIndex?: number}) | null;
};
/**
* Trigger layer hover event with hovered object
* @memberof visStateActions
* @param info - Object hovered, returned by deck.gl
* @param info - Object hovered, returned by deck.gl. Includes an optional `mapIndex` property for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with.
* @returns action
* @public
*/
export function onLayerHover(
info: PickInfo<any> | null
info: (PickInfo<any> & {mapIndex?: number}) | null
): Merge<OnLayerHoverUpdaterAction, {type: typeof ActionTypes.LAYER_HOVER}> {
return {
type: ActionTypes.LAYER_HOVER,
Expand Down
1 change: 1 addition & 0 deletions src/components/src/common/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface SwitchProps {
onFocus?: FocusEventHandler<HTMLInputElement>;
value?: string;
secondary?: boolean;
disabled?: boolean;
}

const Switch = (props: SwitchProps) => {
Expand Down
49 changes: 36 additions & 13 deletions src/components/src/geocoder-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,21 @@ const PARSED_CONFIG = KeplerGlSchema.parseSavedConfig({

interface StyledGeocoderPanelProps {
width?: number;
unsyncedViewports: any;
index: any;
}

const StyledGeocoderPanel = styled.div<StyledGeocoderPanelProps>`
position: absolute;
top: ${props => props.theme.geocoderTop}px;
right: ${props => props.theme.geocoderRight}px;
right: ${props =>
props.unsyncedViewports
? // 2 geocoders: split mode and unsynced viewports
Number.isFinite(props.index) && props.index === 0
? `calc(50% + ${props.theme.geocoderRight}px)` // unsynced left geocoder (index 0)
: `${props.theme.geocoderRight}px` // unsynced right geocoder (index 1)
: // 1 geocoder: single mode OR split mode and synced viewports
`${props.theme.geocoderRight}px`};
width: ${props => (Number.isFinite(props.width) ? props.width : props.theme.geocoderWidth)}px;
box-shadow: ${props => props.theme.boxShadow};
z-index: 100;
Expand Down Expand Up @@ -125,6 +134,8 @@ interface GeocoderPanelProps {
width?: number;
appWidth: number;
className?: string;
index: number;
unsyncedViewports: boolean;
}

export default function GeocoderPanelFactory(): ComponentType<GeocoderPanelProps> {
Expand Down Expand Up @@ -161,29 +172,41 @@ export default function GeocoderPanelFactory(): ComponentType<GeocoderPanelProps
return;
}

this.props.updateMap({
latitude: centerAndZoom.center[1],
longitude: centerAndZoom.center[0],
// For marginal or invalid bounds, zoom may be NaN. Make sure to provide a valid value in order
// to avoid corrupt state and potential crashes as zoom is expected to be a number
...(Number.isFinite(centerAndZoom.zoom) ? {zoom: centerAndZoom.zoom} : {}),
pitch: 0,
bearing: 0,
transitionDuration: this.props.transitionDuration,
transitionInterpolator: new FlyToInterpolator()
});
this.props.updateMap(
{
latitude: centerAndZoom.center[1],
longitude: centerAndZoom.center[0],
// For marginal or invalid bounds, zoom may be NaN. Make sure to provide a valid value in order
// to avoid corrupt state and potential crashes as zoom is expected to be a number
...(Number.isFinite(centerAndZoom.zoom) ? {zoom: centerAndZoom.zoom} : {}),
pitch: 0,
bearing: 0,
transitionDuration: this.props.transitionDuration,
transitionInterpolator: new FlyToInterpolator()
},
this.props.index
);
};

removeMarker = () => {
this.removeGeocoderDataset();
};

render() {
const {isGeocoderEnabled, mapboxApiAccessToken, width, className} = this.props;
const {
className,
isGeocoderEnabled,
mapboxApiAccessToken,
width,
index,
unsyncedViewports
} = this.props;
return (
<StyledGeocoderPanel
className={classnames('geocoder-panel', className)}
width={width}
index={index}
unsyncedViewports={unsyncedViewports}
style={{display: isGeocoderEnabled ? 'block' : 'none'}}
>
{isValid(mapboxApiAccessToken) && (
Expand Down
53 changes: 47 additions & 6 deletions src/components/src/kepler-gl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,34 @@ const BottomWidgetOuter = styled.div<BottomWidgetOuterProps>(
}`
);

export const mapFieldsSelector = (props: KeplerGLProps) => ({
export const isViewportDisjointed = props => {
return (
props.mapState.isSplit &&
!props.mapState.isViewportSynced &&
props.mapState.splitMapViewports.length > 1
);
};

export const mapStateSelector = (props, index) => {
if (!Number.isFinite(index)) {
// either no index arg or an invalid index was provided
// it is expected to be either 0 or 1 when in split mode
// only use the mapState
return props.mapState;
}

return isViewportDisjointed(props)
? // mix together the viewport properties intended for this disjointed <MapContainer> with the other necessary mapState properties
{...props.mapState, ...props.mapState.splitMapViewports[index]}
: // otherwise only use the mapState
props.mapState;
};

export const mapFieldsSelector = (props: KeplerGLProps, index: number = 0) => ({
getMapboxRef: props.getMapboxRef,
mapboxApiAccessToken: props.mapboxApiAccessToken,
mapboxApiUrl: props.mapboxApiUrl ? props.mapboxApiUrl : DEFAULT_KEPLER_GL_PROPS.mapboxApiUrl,
mapState: props.mapState,
mapState: mapStateSelector(props, index),
mapStyle: props.mapStyle,
onDeckInitialized: props.onDeckInitialized,
onViewStateChange: props.onViewStateChange,
Expand Down Expand Up @@ -449,7 +472,6 @@ function KeplerGlFactory(
const isExportingImage = uiState.exportImage.exporting;
const availableProviders = this.availableProviders(this.props);

const mapFields = mapFieldsSelector(this.props);
const filteredDatasets = this.filteredDatasetsSelector(this.props);
const sideFields = sidePanelSelector(this.props, availableProviders, filteredDatasets);
const plotContainerFields = plotContainerSelector(this.props);
Expand All @@ -459,9 +481,14 @@ function KeplerGlFactory(
const notificationPanelFields = notificationPanelSelector(this.props);

const mapContainers = !isSplit
? [<MapContainer primary={true} key={0} index={0} {...mapFields} />]
? [<MapContainer primary={true} key={0} index={0} {...mapFieldsSelector(this.props)} />]
: splitMaps.map((settings, index) => (
<MapContainer key={index} index={index} primary={index === 1} {...mapFields} />
<MapContainer
key={index}
index={index}
primary={index === 1}
{...mapFieldsSelector(this.props, index)}
/>
));

return (
Expand All @@ -485,7 +512,21 @@ function KeplerGlFactory(
{!uiState.readOnly && !readOnly && <SidePanel {...sideFields} />}
<MapsLayout className="maps">{mapContainers}</MapsLayout>
{isExportingImage && <PlotContainer {...plotContainerFields} />}
{interactionConfig.geocoder.enabled && <GeoCoderPanel {...geoCoderPanelFields} />}
{/* 1 geocoder: single mode OR split mode and synced viewports */}
{!isViewportDisjointed(this.props) && interactionConfig.geocoder.enabled && (
<GeoCoderPanel {...geoCoderPanelFields} index={0} unsyncedViewports={false} />
)}
{/* 2 geocoders: split mode and unsynced viewports */}
{isViewportDisjointed(this.props) &&
interactionConfig.geocoder.enabled &&
mapContainers.map((_mapContainer, index) => (
<GeoCoderPanel
key={index}
{...geoCoderPanelFields}
index={index}
unsyncedViewports={true}
/>
))}
<BottomWidgetOuter absolute={!hasPortableWidth(breakPointValues)}>
<BottomWidget
rootRef={this.bottomWidgetRef}
Expand Down
19 changes: 16 additions & 3 deletions src/components/src/map-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,11 +330,11 @@ export default function MapContainerFactory(
};

_handleResize = dimensions => {
const {primary} = this.props;
const {primary, index} = this.props;
if (primary) {
const {mapStateActions} = this.props;
if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
mapStateActions.updateMap(dimensions);
mapStateActions.updateMap(dimensions, index);
}
}
};
Expand Down Expand Up @@ -521,6 +521,12 @@ export default function MapContainerFactory(

/* eslint-disable complexity */
_renderMapPopover() {
// this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
// the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
return null;
}

// TODO: move this into reducer so it can be tested
const {
mapState,
Expand Down Expand Up @@ -733,6 +739,11 @@ export default function MapContainerFactory(
});
if (res) return;

// add `mapIndex` property which will end up in the the `hoverInfo` object of `visState`
// this is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
// @ts-ignore (does not fail with local yarn-test)
data.mapIndex = index;

visStateActions.onLayerHover(data);
}}
onClick={(data, event) => {
Expand Down Expand Up @@ -790,7 +801,8 @@ export default function MapContainerFactory(
viewState,
this.props.mapStateActions.updateMap,
this.props.onViewStateChange,
this.props.primary
this.props.primary,
this.props.index
);
};

Expand Down Expand Up @@ -869,6 +881,7 @@ export default function MapContainerFactory(
onToggleSplitMap={mapStateActions.toggleSplitMap}
onMapToggleLayer={this._handleMapToggleLayer}
onToggleMapControl={this._toggleMapControl}
onToggleSplitMapViewport={mapStateActions.toggleSplitMapViewport}
onSetEditorMode={visStateActions.setEditorMode}
onSetLocale={uiStateActions.setLocale}
onToggleEditorVisibility={visStateActions.toggleEditorVisibility}
Expand Down