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

[Endpoint] Resolver zoom, pan, and center controls #55221

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`translation should be updated north':

can you explain why this number is negative? If 'north' means towards the top of the screen, and if we want to see more stuff north of the current viewport, wouldn't we want to move the camera towards the top of the screen?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it('moves the camera south so that objects appear closer to the bottom of the screen', () => {

]
`);
});
});
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe

    /**
     * Delta amount will be in the range of 20 -> 40
     */

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