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

Last seen column in drone status line #45

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
be shown if any pair of takeoff positions or any pair of landing positions are
too close to each other.

- Make the "Flash lights" button latchable by holding Shift while activating it.

### Changed

- When setting the start time based on a time offset from the current time,
Expand Down
32 changes: 27 additions & 5 deletions src/components/uavs/UAVOperationsButtonGroup.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import isEmpty from 'lodash-es/isEmpty';
import PropTypes from 'prop-types';
import React from 'react';
import React, { useCallback, useState } from 'react';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { useInterval } from 'react-use';
import { bindActionCreators } from '@reduxjs/toolkit';

import Button from '@material-ui/core/Button';
Expand All @@ -27,6 +28,7 @@ import Colors from '~/components/colors';
import ToolbarDivider from '~/components/ToolbarDivider';
import Bolt from '~/icons/Bolt';

import { UAV_SIGNAL_DURATION } from '~/features/settings/constants';
import {
requestRemovalOfUAVsByIds,
requestRemovalOfUAVsMarkedAsGone,
Expand Down Expand Up @@ -89,16 +91,33 @@ const UAVOperationsButtonGroup = ({
dispatch
);

const [keepFlashing, setKeepFlashing] = useState(false);
const flashLightsButtonOnClick = useCallback(
(event) => {
if (keepFlashing) {
setKeepFlashing(false);
} else if (event.shiftKey) {
setKeepFlashing(true);
flashLight();
} else {
flashLight();
}
},
[flashLight, keepFlashing, setKeepFlashing]
);

useInterval(flashLight, keepFlashing ? UAV_SIGNAL_DURATION * 1000 : null);

const fontSize = size === 'small' ? 'small' : 'medium';
const iconSize = size;

const flashLightsButton =
size === 'small' ? (
<Button
startIcon={<WbSunny />}
startIcon={<WbSunny color={keepFlashing ? 'primary' : undefined} />}
disabled={isSelectionEmpty}
size={iconSize}
onClick={flashLight}
onClick={flashLightsButtonOnClick}
>
{t('UAVOpButtonGrp.flashLights')}
</Button>
Expand All @@ -107,9 +126,12 @@ const UAVOperationsButtonGroup = ({
<IconButton
disabled={isSelectionEmpty}
size={iconSize}
onClick={flashLight}
onClick={flashLightsButtonOnClick}
>
<WbSunny fontSize={fontSize} />
<WbSunny
fontSize={fontSize}
color={keepFlashing ? 'primary' : undefined}
/>
</IconButton>
</Tooltip>
);
Expand Down
7 changes: 6 additions & 1 deletion src/features/settings/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
/**
* Timeout length for broadcast mode.
* Timeout length for broadcast mode, in seconds.
*/
export const BROADCAST_MODE_TIMEOUT_LENGTH = 5;

/**
* Duration of the `UAV-SIGNAL` (flash light) command, in seconds.
*/
export const UAV_SIGNAL_DURATION = 5;
10 changes: 10 additions & 0 deletions src/features/uavs/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { convertRGB565ToCSSNotation } from '~/flockwave/parsing';
import { globalIdToUavId } from '~/model/identifiers';
import { UAVAge } from '~/model/uav';
import { selectionForSubset } from '~/selectors/selection';
import { selectOrdered } from '~/utils/collections';
import { euclideanDistance2D, getMeanAngle } from '~/utils/math';
import { EMPTY_ARRAY } from '~/utils/redux';
import { createDeepResultSelector } from '~/utils/selectors';
Expand Down Expand Up @@ -68,6 +69,15 @@ export const getUAVIdToStateMapping = (state) => state.uavs.byId;
*/
export const getUAVById = (state, uavId) => state.uavs.byId[uavId];

/**
* Selector that calculates and caches the list of all the UAVs in the
* state object.
*/
export const getUAVsInOrder = createSelector(
(state) => state.uavs,
selectOrdered
);

/**
* Returns the current position of the UAV with the given ID, given the current
* state.
Expand Down
8 changes: 8 additions & 0 deletions src/model/layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Map from '@material-ui/icons/Map';
import MyLocation from '@material-ui/icons/MyLocation';
import Timeline from '@material-ui/icons/Timeline';
import TrackChanges from '@material-ui/icons/TrackChanges';
import Wifi from '@material-ui/icons/Wifi';

import type BaseLayer from 'ol/layer/Base';
import type OLLayer from 'ol/layer/Layer';
Expand Down Expand Up @@ -49,6 +50,8 @@ export enum LayerType {
UAV_TRACE = 'uavTrace',
UNAVAILABLE = 'unavailable',
UNTYPED = 'untyped',

RSSI = 'rssi',
}

export type Layer = {
Expand Down Expand Up @@ -76,6 +79,7 @@ export const LayerTypes = [
LayerType.OWN_LOCATION,
LayerType.GEOJSON,
LayerType.HEATMAP,
LayerType.RSSI,
] as const;

export const ProLayerTypes = [LayerType.IMAGE] as const;
Expand Down Expand Up @@ -251,6 +255,10 @@ const propertiesForLayerTypes: Record<
label: 'Untyped layer',
icon: HelpOutline,
},
[LayerType.RSSI]: {
label: 'RSSI',
icon: Wifi,
},
} as const;

/**
Expand Down
4 changes: 4 additions & 0 deletions src/model/uav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export default class UAV {
// to the output later on, thus the object spread can be avoided.
_positionMemoizer: (position: GPSPosition) => GPSPosition;

// TODO: This should be unnecessary if we can ensure that no mutation happens
// to the output later on, thus the object spread can be avoided.
_positionMemoizer: (position: GPSPosition) => GPSPosition;

/**
* Constructor.
*
Expand Down
3 changes: 2 additions & 1 deletion src/utils/messaging.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import mapValues from 'lodash-es/mapValues';
import values from 'lodash-es/values';

import { showConfirmationDialog } from '~/features/prompt/actions';
import { UAV_SIGNAL_DURATION } from '~/features/settings/constants';
import { shouldConfirmUAVOperation } from '~/features/settings/selectors';
import { showNotification } from '~/features/snackbar/actions';
import { MessageSemantics } from '~/features/snackbar/types';
Expand Down Expand Up @@ -148,7 +149,7 @@ export const flashLightOnUAVsAndHideFailures = performMassOperation({
name: 'Light signal command',
mapper: (options) => ({
signals: ['light'],
duration: 5000,
duration: UAV_SIGNAL_DURATION * 1000,
...options,
}),
reportSuccess: false,
Expand Down
2 changes: 2 additions & 0 deletions src/views/map/layers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { TileServerLayerSettings, TileServerLayer } from './tileserver';
import { UAVsLayerSettings, UAVsLayer } from './uavs';
import { UAVTraceLayerSettings, UAVTraceLayer } from './uavtrace';
import { UntypedLayerSettings, UntypedLayer } from './untyped';
import { RSSILayer } from './rssi';

import { LayerType } from '~/model/layers';

Expand Down Expand Up @@ -76,6 +77,7 @@ export const Layers = {
[LayerType.UAVS]: UAVsLayer,
[LayerType.UAV_TRACE]: UAVTraceLayer,
[LayerType.UNTYPED]: UntypedLayer,
[LayerType.RSSI]: RSSILayer,
};

export const stateObjectToLayer = (layer, props) => {
Expand Down
83 changes: 83 additions & 0 deletions src/views/map/layers/rssi.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { RegularShape, Style } from 'ol/style';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';

import { Feature, geom, layer, source } from '@collmot/ol-react';

import { getUAVsInOrder } from '~/features/uavs/selectors';
import { mapViewCoordinateFromLonLat } from '~/utils/geography';
import { dockIdToGlobalId } from '~/model/identifiers';
import { shadowVeryThinOutline, fill } from '~/utils/styles';

// === Helper functions ===

const createRSSIStyle = (rssi) => [
new Style({
image: new RegularShape({
points: 4,
fill: fill(`hsl(${rssi}, 100%, 50%)`),
stroke: shadowVeryThinOutline,
radius: 15,
angle: Math.PI / 4,
}),
}),
];

// === A single feature representing a docking station ===

const RSSIFeature = React.memo(({ uav, ...rest }) => {
const { id, position, rssi } = uav;

if (!position) {
return null;
}

const style = createRSSIStyle(rssi[0]);

return (
<Feature id={dockIdToGlobalId(id)} style={style} {...rest}>
<geom.Point
coordinates={mapViewCoordinateFromLonLat([position.lon, position.lat])}
/>
</Feature>
);
});

RSSIFeature.propTypes = {
selected: PropTypes.bool,
uav: PropTypes.shape({
id: PropTypes.string,
position: PropTypes.shape({
lat: PropTypes.number.required,
lon: PropTypes.number.required,
}),
rssi: PropTypes.arrayOf(PropTypes.number),
}),
};

// === The actual layer to be rendered ===

const RSSILayerPresentation = ({ uavs, zIndex }) => (
<layer.Vector updateWhileAnimating updateWhileInteracting zIndex={zIndex}>
<source.Vector>
{uavs.map((uav) => (
<RSSIFeature key={uav.id} uav={uav} />
))}
</source.Vector>
</layer.Vector>
);

RSSILayerPresentation.propTypes = {
uavs: PropTypes.arrayOf(PropTypes.object).isRequired,
zIndex: PropTypes.number,
};

export const RSSILayer = connect(
// mapStateToProps
(state) => ({
uavs: getUAVsInOrder(state),
}),
// mapDispatchToProps
null
)(RSSILayerPresentation);
22 changes: 14 additions & 8 deletions src/views/uavs/DroneStatusLine.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import {
getSemanticsForRSSI,
} from '~/model/enums';
import { getPreferredCoordinateFormatter } from '~/selectors/formatting';
import { formatCoordinateArray, formatRSSI } from '~/utils/formatting';
import { formatCoordinateArray, formatRSSI, shortTimeAgoFormatter } from '~/utils/formatting';

import TimeAgo from 'react-timeago';

/**
* Converts the absolute value of a heading deviation, in degrees, to the
Expand Down Expand Up @@ -60,6 +62,7 @@ const useStyles = makeStyles(
fontVariantNumeric: 'lining-nums tabular-nums',
marginTop: [-2, '!important'],
marginBottom: [-4, '!important'],
userSelect: 'none',
whiteSpace: 'pre',
},
gone: {
Expand Down Expand Up @@ -122,6 +125,7 @@ const DroneStatusLine = ({
secondaryLabel,
text,
textSemantics,
lastUpdated,
}) => {
const classes = useStyles();
const { amsl, ahl, agl } = position || {};
Expand Down Expand Up @@ -168,13 +172,6 @@ const DroneStatusLine = ({
>
{abbreviateGPSFixType(gpsFixType)}
</StatusPill>
{localPosition ? (
padEnd(localCoordinateFormatter(localPosition), 25)
) : position ? (
padEnd(coordinateFormatter([position.lon, position.lat]), 25)
) : (
<span className={classes.muted}>{padEnd('no position', 25)}</span>
)}
{!isNil(amsl) ? (
padStart(position.amsl.toFixed(1), 6) + 'm'
) : (
Expand All @@ -193,6 +190,14 @@ const DroneStatusLine = ({
<StatusText status={headingDeviationToStatus(headingDeviation)}>
{padStart(!isNil(heading) ? Math.round(heading) + '°' : '', 5)}
</StatusText>
<TimeAgo formatter={(...args) => padStart(padEnd(shortTimeAgoFormatter(...args), 5), 6)} date={lastUpdated} />
{localPosition ? (
padEnd(localCoordinateFormatter(localPosition), 25)
) : position ? (
padEnd(coordinateFormatter([position.lon, position.lat]), 25)
) : (
<span className={classes.muted}>{padEnd('no position', 25)}</span>
)}
<span className={classes.debugString}>
{debugString ? ' ' + debugString : ''}
</span>
Expand Down Expand Up @@ -274,6 +279,7 @@ export default connect(
mode: uav ? uav.mode : undefined,
position: uav ? uav.position : undefined,
rssi: uav ? uav.rssi?.[0] : undefined,
lastUpdated: uav ? uav.lastUpdated : undefined,
...statusSummarySelector(state, ownProps.id),
};
};
Expand Down
21 changes: 14 additions & 7 deletions src/views/uavs/SortAndFilterHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,6 @@ const COMMON_HEADER_TEXT_PARTS = Object.freeze([
width: 40,
},
},
{
label: 'Position',
style: {
textAlign: 'left',
width: 198,
},
},
{
label: 'AMSL',
sortKey: UAVSortKey.ALTITUDE_MSL,
Expand Down Expand Up @@ -239,6 +232,20 @@ const COMMON_HEADER_TEXT_PARTS = Object.freeze([
width: 40,
},
},
{
label: 'Seen',
style:{
textAlign: 'left',
width: 40,
}
},
{
label: 'Position',
style: {
textAlign: 'left',
width: 198,
},
},
{
label: 'Details',
style: {
Expand Down
Loading