Skip to content

Commit

Permalink
Resolver zoom, pan, and center controls (#55221)
Browse files Browse the repository at this point in the history
* Resolver zoom, pan, and center controls

* add tests, fix north panning

* fix type issue

* update west and east panning to behave like google maps
  • Loading branch information
peluja1012 committed Jan 21, 2020
1 parent 0cd1733 commit 7eb934e
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Vector2 } from '../../types';
import { Vector2, PanDirection } from '../../types';

interface UserSetZoomLevel {
readonly type: 'userSetZoomLevel';
Expand All @@ -14,6 +14,14 @@ interface UserSetZoomLevel {
readonly payload: number;
}

interface UserClickedZoomOut {
readonly type: 'userClickedZoomOut';
}

interface UserClickedZoomIn {
readonly type: 'userClickedZoomIn';
}

interface UserZoomed {
readonly type: 'userZoomed';
/**
Expand Down Expand Up @@ -56,6 +64,14 @@ interface UserStoppedPanning {
readonly type: 'userStoppedPanning';
}

interface UserClickedPanControl {
readonly type: 'userClickedPanControl';
/**
* String that represents the direction in which Resolver can be panned
*/
readonly payload: PanDirection;
}

interface UserMovedPointer {
readonly type: 'userMovedPointer';
/**
Expand All @@ -72,4 +88,7 @@ export type CameraAction =
| UserStartedPanning
| UserStoppedPanning
| UserZoomed
| UserMovedPointer;
| UserMovedPointer
| UserClickedZoomOut
| UserClickedZoomIn
| UserClickedPanControl;
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,66 @@ describe('panning interaction', () => {
});
});
});
describe('panning controls', () => {
describe('when user clicks on pan north button', () => {
beforeEach(() => {
const action: CameraAction = { type: 'userClickedPanControl', payload: 'north' };
store.dispatch(action);
});
it('moves the camera south so that objects appear closer to the bottom of the screen', () => {
const actual = translation(store.getState());
expect(actual).toMatchInlineSnapshot(`
Array [
0,
-32.49906769231164,
]
`);
});
});
describe('when user clicks on pan south button', () => {
beforeEach(() => {
const action: CameraAction = { type: 'userClickedPanControl', payload: 'south' };
store.dispatch(action);
});
it('moves the camera north so that objects appear closer to the top of the screen', () => {
const actual = translation(store.getState());
expect(actual).toMatchInlineSnapshot(`
Array [
0,
32.49906769231164,
]
`);
});
});
describe('when user clicks on pan east button', () => {
beforeEach(() => {
const action: CameraAction = { type: 'userClickedPanControl', payload: 'east' };
store.dispatch(action);
});
it('moves the camera west so that objects appear closer to the left of the screen', () => {
const actual = translation(store.getState());
expect(actual).toMatchInlineSnapshot(`
Array [
-32.49906769231164,
0,
]
`);
});
});
describe('when user clicks on pan west button', () => {
beforeEach(() => {
const action: CameraAction = { type: 'userClickedPanControl', payload: 'west' };
store.dispatch(action);
});
it('moves the camera east so that objects appear closer to the right of the screen', () => {
const actual = translation(store.getState());
expect(actual).toMatchInlineSnapshot(`
Array [
32.49906769231164,
0,
]
`);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { applyMatrix3, subtract } from '../../lib/vector2';
import { userIsPanning, translation, projectionMatrix, inverseProjectionMatrix } from './selectors';
import { clamp } from '../../lib/math';

import { CameraState, ResolverAction } from '../../types';
import { CameraState, ResolverAction, Vector2 } from '../../types';
import { scaleToZoom } from './scale_to_zoom';

function initialState(): CameraState {
Expand All @@ -34,6 +34,16 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = (
...state,
scalingFactor: clamp(action.payload, 0, 1),
};
} else if (action.type === 'userClickedZoomIn') {
return {
...state,
scalingFactor: clamp(state.scalingFactor + 0.1, 0, 1),
};
} else if (action.type === 'userClickedZoomOut') {
return {
...state,
scalingFactor: clamp(state.scalingFactor - 0.1, 0, 1),
};
} else if (action.type === 'userZoomed') {
const stateWithNewScaling: CameraState = {
...state,
Expand Down Expand Up @@ -100,6 +110,32 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = (
} else {
return state;
}
} else if (action.type === 'userClickedPanControl') {
const panDirection = action.payload;
/**
* Delta amount will be in the range of 20 -> 40 depending on the scalingFactor
*/
const deltaAmount = (1 + state.scalingFactor) * 20;
let delta: Vector2;
if (panDirection === 'north') {
delta = [0, -deltaAmount];
} else if (panDirection === 'south') {
delta = [0, deltaAmount];
} else if (panDirection === 'east') {
delta = [-deltaAmount, 0];
} else if (panDirection === 'west') {
delta = [deltaAmount, 0];
} else {
delta = [0, 0];
}

return {
...state,
translationNotCountingCurrentPanning: [
state.translationNotCountingCurrentPanning[0] + delta[0],
state.translationNotCountingCurrentPanning[1] + delta[1],
],
};
} else if (action.type === 'userSetRasterSize') {
/**
* Handle resizes of the Resolver component. We need to know the size in order to convert between screen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ export const scale = (state: CameraState): Vector2 => {
return [value, value];
};

/**
* Scales the coordinate system, used for zooming. Should always be between 0 and 1
*/
export const scalingFactor = (state: CameraState): CameraState['scalingFactor'] => {
return state.scalingFactor;
};

/**
* Whether or not the user is current panning the map.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CameraAction } from './action';
import { cameraReducer } from './reducer';
import { createStore, Store } from 'redux';
import { CameraState, AABB } from '../../types';
import { viewableBoundingBox, inverseProjectionMatrix } from './selectors';
import { viewableBoundingBox, inverseProjectionMatrix, scalingFactor } from './selectors';
import { expectVectorsToBeClose } from './test_helpers';
import { scaleToZoom } from './scale_to_zoom';
import { applyMatrix3 } from '../../lib/vector2';
Expand Down Expand Up @@ -151,4 +151,29 @@ describe('zooming', () => {
});
});
});
describe('zoom controls', () => {
let previousScalingFactor: CameraState['scalingFactor'];
describe('when user clicks on zoom in button', () => {
beforeEach(() => {
previousScalingFactor = scalingFactor(store.getState());
const action: CameraAction = { type: 'userClickedZoomIn' };
store.dispatch(action);
});
it('the scaling factor should increase by 0.1 units', () => {
const actual = scalingFactor(store.getState());
expect(actual).toEqual(previousScalingFactor + 0.1);
});
});
describe('when user clicks on zoom out button', () => {
beforeEach(() => {
previousScalingFactor = scalingFactor(store.getState());
const action: CameraAction = { type: 'userClickedZoomOut' };
store.dispatch(action);
});
it('the scaling factor should decrease by 0.1 units', () => {
const actual = scalingFactor(store.getState());
expect(actual).toEqual(previousScalingFactor - 0.1);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export const inverseProjectionMatrix = composeSelectors(
*/
export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale);

/**
* Scales the coordinate system, used for zooming. Should always be between 0 and 1
*/
export const scalingFactor = composeSelectors(cameraStateSelector, cameraSelectors.scalingFactor);

/**
* Whether or not the user is current panning the map.
*/
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/endpoint/public/embeddables/resolver/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,8 @@ export type ProcessWithWidthMetadata = {
firstChildWidth: null;
}
);

/**
* String that represents the direction in which Resolver can be panned
*/
export type PanDirection = 'north' | 'south' | 'east' | 'west';
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useCallback } from 'react';
import styled from 'styled-components';
import { EuiRange, EuiPanel, EuiIcon } from '@elastic/eui';
import { useSelector, useDispatch } from 'react-redux';
import { ResolverAction, PanDirection } from '../types';
import * as selectors from '../store/selectors';

/**
* Controls for zooming, panning, and centering in Resolver
*/
export const GraphControls = styled(
React.memo(
({
className,
}: {
/**
* A className string provided by `styled`
*/
className?: string;
}) => {
const dispatch: (action: ResolverAction) => unknown = useDispatch();
const scalingFactor = useSelector(selectors.scalingFactor);

const handleZoomAmountChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement>) => {
const valueAsNumber = parseFloat(
(event as React.ChangeEvent<HTMLInputElement>).target.value
);
if (isNaN(valueAsNumber) === false) {
dispatch({
type: 'userSetZoomLevel',
payload: valueAsNumber,
});
}
},
[dispatch]
);

const handleCenterClick = useCallback(() => {
dispatch({
type: 'userSetPositionOfCamera',
payload: [0, 0],
});
}, [dispatch]);

const handleZoomOutClick = useCallback(() => {
dispatch({
type: 'userClickedZoomOut',
});
}, [dispatch]);

const handleZoomInClick = useCallback(() => {
dispatch({
type: 'userClickedZoomIn',
});
}, [dispatch]);

const handlePanClick = (panDirection: PanDirection) => {
return () => {
dispatch({
type: 'userClickedPanControl',
payload: panDirection,
});
};
};

return (
<div className={className}>
<EuiPanel className="panning-controls" paddingSize="none" hasShadow>
<div className="panning-controls-top">
<button className="north-button" title="North" onClick={handlePanClick('north')}>
<EuiIcon type="arrowUp" />
</button>
</div>
<div className="panning-controls-middle">
<button className="west-button" title="West" onClick={handlePanClick('west')}>
<EuiIcon type="arrowLeft" />
</button>
<button className="center-button" title="Center" onClick={handleCenterClick}>
<EuiIcon type="bullseye" />
</button>
<button className="east-button" title="East" onClick={handlePanClick('east')}>
<EuiIcon type="arrowRight" />
</button>
</div>
<div className="panning-controls-bottom">
<button className="south-button" title="South" onClick={handlePanClick('south')}>
<EuiIcon type="arrowDown" />
</button>
</div>
</EuiPanel>
<EuiPanel className="zoom-controls" paddingSize="none" hasShadow>
<button title="Zoom In" onClick={handleZoomInClick}>
<EuiIcon type="plusInCircle" />
</button>
<EuiRange
className="zoom-slider"
min={0}
max={1}
step={0.01}
value={scalingFactor}
onChange={handleZoomAmountChange}
/>
<button title="Zoom Out" onClick={handleZoomOutClick}>
<EuiIcon type="minusInCircle" />
</button>
</EuiPanel>
</div>
);
}
)
)`
position: absolute;
top: 5px;
left: 5px;
z-index: 1;
background-color: #d4d4d4;
color: #333333;
.zoom-controls {
display: flex;
flex-direction: column;
align-items: center;
padding: 5px 0px;
.zoom-slider {
width: 20px;
height: 150px;
margin: 5px 0px 2px 0px;
input[type='range'] {
width: 150px;
height: 20px;
transform-origin: 75px 75px;
transform: rotate(-90deg);
}
}
}
.panning-controls {
text-align: center;
}
`;
Loading

0 comments on commit 7eb934e

Please sign in to comment.