From ab7a235775d523ace94406c61dcd31f13d979d31 Mon Sep 17 00:00:00 2001 From: zarcell Date: Tue, 7 Oct 2025 15:54:12 +0200 Subject: [PATCH 01/70] feat(rtk): add saved coord restoration, initial commit, review required --- .../rtk/RTKCoordinateRestorationDialog.jsx | 99 +++++++++++++++++++ .../rtk/RTKCorrectionSourceSelector.jsx | 26 ++++- src/features/rtk/RTKSetupDialog.jsx | 24 +++-- src/features/rtk/RTKStatusUpdater.jsx | 37 ++++++- src/features/rtk/actions.js | 92 ++++++++++++++++- src/features/rtk/selectors.ts | 64 +++++++++++- src/features/rtk/slice.ts | 74 +++++++++++++- src/features/rtk/types.ts | 7 ++ src/flockwave/operations.js | 18 ++++ src/i18n/en.json | 9 +- src/i18n/hu.json | 7 ++ 11 files changed, 437 insertions(+), 20 deletions(-) create mode 100644 src/features/rtk/RTKCoordinateRestorationDialog.jsx diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx new file mode 100644 index 000000000..e91571321 --- /dev/null +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -0,0 +1,99 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; + +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; + +import { closeCoordinateRestorationDialog } from '~/features/rtk/slice'; +import { getCoordinateRestorationDialog, getFormattedSavedCoordinatePosition } from '~/features/rtk/selectors'; +import { setSelectedPresetAsSource, useSavedCoordinateForPreset } from '~/features/rtk/actions'; + +const RTKCoordinateRestorationDialog = ({ + t, + dialog, + formattedPosition, + onClose, + onUseSaved, + onStartNew +}) => { + if (!dialog.open || !dialog.savedCoordinate) { + return null; + } + + const { presetId, savedCoordinate } = dialog; + const { accuracy, savedAt } = savedCoordinate; + const savedDate = new Date(savedAt).toLocaleDateString(); + + const handleUseSaved = () => { + onClose(); // Close dialog first + onUseSaved(presetId, savedCoordinate); // Then start async operation + }; + + const handleStartNew = () => { + onStartNew(presetId); + onClose(); + }; + + return ( + + + {t('RTKCoordinateRestorationDialog.title')} + + + + {t('RTKCoordinateRestorationDialog.message', { + position: formattedPosition, + accuracy: accuracy.toFixed(3), + date: savedDate, + })} + + + + + + + + + ); +}; + +RTKCoordinateRestorationDialog.propTypes = { + t: PropTypes.func, + dialog: PropTypes.object, + formattedPosition: PropTypes.string, + onClose: PropTypes.func, + onUseSaved: PropTypes.func, + onStartNew: PropTypes.func, +}; + +export default connect( + (state) => ({ + dialog: getCoordinateRestorationDialog(state), + formattedPosition: getCoordinateRestorationDialog(state).savedCoordinate + ? getFormattedSavedCoordinatePosition(state, getCoordinateRestorationDialog(state).presetId) + : undefined, + }), + { + onClose: closeCoordinateRestorationDialog, + onUseSaved: useSavedCoordinateForPreset, + onStartNew: setSelectedPresetAsSource, + } +)(withTranslation()(RTKCoordinateRestorationDialog)); diff --git a/src/features/rtk/RTKCorrectionSourceSelector.jsx b/src/features/rtk/RTKCorrectionSourceSelector.jsx index 49b4ef2bc..84b5d4246 100644 --- a/src/features/rtk/RTKCorrectionSourceSelector.jsx +++ b/src/features/rtk/RTKCorrectionSourceSelector.jsx @@ -10,7 +10,8 @@ import InputLabel from '@material-ui/core/InputLabel'; import MenuItem from '@material-ui/core/MenuItem'; import Select from '@material-ui/core/Select'; -import { resetRTKStatistics } from '~/features/rtk/slice'; +import { resetRTKStatistics, showCoordinateRestorationDialog } from '~/features/rtk/slice'; +import { hasSavedCoordinateForPreset, getSavedCoordinateForPreset } from '~/features/rtk/selectors'; import messageHub from '~/message-hub'; const NULL_ID = '__null__'; @@ -20,7 +21,7 @@ const nullPreset = { title: 'RTK disabled', }; -const RTKCorrectionSourceSelector = ({ onSourceChanged, t }) => { +const RTKCorrectionSourceSelector = ({ onSourceChanged, t, hasSavedCoordinateForPreset, getSavedCoordinateForPreset, showCoordinateRestorationDialog }) => { const [selectedByUser, setSelectedByUser] = useState(); const [selectionState, getSelectionFromServer] = useAsyncFn(async () => messageHub.query.getSelectedRTKPresetId() @@ -45,10 +46,20 @@ const RTKCorrectionSourceSelector = ({ onSourceChanged, t }) => { const hasPresets = presets && presets.length > 0; const handleChange = async (event) => { + const newPresetId = event.target.value; + + // Check if there's a saved coordinate for this preset + if (newPresetId !== NULL_ID && hasSavedCoordinateForPreset(newPresetId)) { + const savedCoordinate = getSavedCoordinateForPreset(newPresetId); + showCoordinateRestorationDialog({ presetId: newPresetId, savedCoordinate }); + // Don't proceed with the selection yet - wait for user decision in dialog + // return; + } + // We assume that the request will succeed so we eagerly select the new // value. If changing the RTK source fails, it will be changed back in the // response handler triggered by the effect that we set up below. - setSelectedByUser(event.target.value); + setSelectedByUser(newPresetId); if (onSourceChanged) { onSourceChanged(); @@ -145,13 +156,20 @@ const RTKCorrectionSourceSelector = ({ onSourceChanged, t }) => { RTKCorrectionSourceSelector.propTypes = { onSourceChanged: PropTypes.func, t: PropTypes.func, + hasSavedCoordinateForPreset: PropTypes.func, + getSavedCoordinateForPreset: PropTypes.func, + showCoordinateRestorationDialog: PropTypes.func, }; export default connect( // mapStateToProps - null, + (state) => ({ + hasSavedCoordinateForPreset: (presetId) => hasSavedCoordinateForPreset(state, presetId), + getSavedCoordinateForPreset: (presetId) => getSavedCoordinateForPreset(state, presetId), + }), // mapDispatchToProps { onSourceChanged: resetRTKStatistics, + showCoordinateRestorationDialog, } )(withTranslation()(RTKCorrectionSourceSelector)); diff --git a/src/features/rtk/RTKSetupDialog.jsx b/src/features/rtk/RTKSetupDialog.jsx index 4895273b8..00b7cdaa0 100644 --- a/src/features/rtk/RTKSetupDialog.jsx +++ b/src/features/rtk/RTKSetupDialog.jsx @@ -6,6 +6,7 @@ import Box from '@material-ui/core/Box'; import Dialog from '@material-ui/core/Dialog'; import RTKCorrectionSourceSelector from './RTKCorrectionSourceSelector'; +import RTKCoordinateRestorationDialog from './RTKCoordinateRestorationDialog'; import RTKMessageStatistics from './RTKMessageStatistics'; import RTKSetupDialogBottomPanel from './RTKSetupDialogBottomPanel'; import RTKStatusUpdater from './RTKStatusUpdater'; @@ -16,18 +17,21 @@ import { closeRTKSetupDialog } from './slice'; * monitor the RTK correction source for the UAVs. */ const RTKSetupDialog = ({ onClose, open }) => ( - - - - - - - + <> + + + + + + + + + - - - + + + ); RTKSetupDialog.propTypes = { diff --git a/src/features/rtk/RTKStatusUpdater.jsx b/src/features/rtk/RTKStatusUpdater.jsx index dfb763ae1..e2bbc2424 100644 --- a/src/features/rtk/RTKStatusUpdater.jsx +++ b/src/features/rtk/RTKStatusUpdater.jsx @@ -3,10 +3,11 @@ import isNil from 'lodash-es/isNil'; import mapValues from 'lodash-es/mapValues'; import PropTypes from 'prop-types'; import { useEffect } from 'react'; -import { connect } from 'react-redux'; +import { connect, useDispatch, useSelector } from 'react-redux'; import handleError from '~/error-handling'; import { updateRTKStatistics } from '~/features/rtk/slice'; +import { saveCurrentCoordinateForPreset } from '~/features/rtk/actions'; import useMessageHub from '~/hooks/useMessageHub'; /** @@ -15,6 +16,8 @@ import useMessageHub from '~/hooks/useMessageHub'; */ const RTKStatusUpdater = ({ onStatusChanged, period }) => { const messageHub = useMessageHub(); + const dispatch = useDispatch(); + const savedCoordinates = useSelector((state) => state.rtk.savedCoordinates); useEffect(() => { const valueHolder = { @@ -28,6 +31,36 @@ const RTKStatusUpdater = ({ onStatusChanged, period }) => { // eslint-disable-next-line no-await-in-loop const status = await messageHub.query.getRTKStatus(); onStatusChanged(status); + + // Autosave base station coordinate on first valid fix per preset + const hasECEF = Array.isArray(status?.antenna?.positionECEF); + const accuracy = status?.survey?.accuracy; + const flags = status?.survey?.flags; + const surveyedCoordinateValid = typeof flags === 'number' ? (flags & 0b100) !== 0 : false; + const hasValidFix = hasECEF && (surveyedCoordinateValid || typeof accuracy === 'number'); + + if (hasValidFix) { + // eslint-disable-next-line no-await-in-loop + const selectedPresetId = await messageHub.query.getSelectedRTKPresetId(); + if (selectedPresetId) { + const incomingECEF = Array.isArray(status?.antenna?.positionECEF) + ? status.antenna.positionECEF.slice(0, 3).map((x) => Math.round(x)) + : undefined; + const saved = savedCoordinates && savedCoordinates[selectedPresetId]; + const savedECEF = saved && Array.isArray(saved.positionECEF) + ? saved.positionECEF.slice(0, 3) + : undefined; + + const isSameECEF = + incomingECEF && savedECEF + ? incomingECEF.length === savedECEF.length && incomingECEF.every((v, i) => v === savedECEF[i]) + : false; + + if (!isSameECEF) { + dispatch(saveCurrentCoordinateForPreset(selectedPresetId)); + } + } + } } catch (error) { handleError(error, 'RTK status query'); } @@ -43,7 +76,7 @@ const RTKStatusUpdater = ({ onStatusChanged, period }) => { valueHolder.finished = true; valueHolder.promise = null; }; - }, [messageHub, onStatusChanged, period]); + }, [messageHub, onStatusChanged, period, dispatch, savedCoordinates]); return null; }; diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index 33c711f60..c2ee6b3b5 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -7,7 +7,7 @@ import { getFormattedAntennaPosition, isShowingAntennaPositionInECEF, } from './selectors'; -import { closeSurveySettingsPanel, setAntennaPositionFormat } from './slice'; +import { closeSurveySettingsPanel, setAntennaPositionFormat, saveCoordinateForPreset } from './slice'; export const copyAntennaPositionToClipboard = () => (dispatch, getState) => { copy(getFormattedAntennaPosition(getState())); @@ -30,6 +30,30 @@ export const startNewSurveyOnServer = (settings) => async (dispatch) => { dispatch(closeSurveySettingsPanel()); }; +/** + * Sets the server-side RTK correction source to the given preset. + */ +export const setSelectedPresetAsSource = (presetId) => async (dispatch) => { + try { + await messageHub.execute.setRTKCorrectionsSource(presetId); + } catch (error) { + dispatch( + showNotification({ + message: 'Failed to set RTK correction source.', + semantics: MessageSemantics.ERROR, + }) + ); + return; + } + + dispatch( + showNotification({ + message: `RTK correction source set to preset ${presetId}`, + semantics: MessageSemantics.SUCCESS, + }) + ); +}; + export const toggleAntennaPositionFormat = () => (dispatch, getState) => { dispatch( setAntennaPositionFormat( @@ -37,3 +61,69 @@ export const toggleAntennaPositionFormat = () => (dispatch, getState) => { ) ); }; + +export const useSavedCoordinateForPreset = (presetId, savedCoordinate) => async (dispatch, getState) => { + try { + // Set the saved coordinate as the current antenna position + await messageHub.execute.setRTKAntennaPosition(savedCoordinate.positionECEF); + + // Check if the component is still mounted by checking if the dialog is still open + const currentState = getState(); + if (!currentState.rtk.dialog.coordinateRestorationDialog.open) { + return; + } + + dispatch( + showNotification({ + message: `Using saved coordinate for preset ${presetId}`, + semantics: MessageSemantics.SUCCESS, + }) + ); + } catch (error) { + console.warn('Failed to set saved coordinate:', error); + + // Check if the component is still mounted + const currentState = getState(); + if (!currentState.rtk.dialog.coordinateRestorationDialog.open) { + return; + } + + dispatch( + showNotification({ + message: 'Failed to use saved coordinate.', + semantics: MessageSemantics.ERROR, + }) + ); + } +}; + +export const saveCurrentCoordinateForPreset = (presetId) => (dispatch, getState) => { + const state = getState(); + const { antenna, survey } = state.rtk.stats; + + if (!antenna.position || !antenna.positionECEF || !survey.accuracy) { + dispatch( + showNotification({ + message: 'No valid coordinate data available to save.', + semantics: MessageSemantics.ERROR, + }) + ); + return; + } + + const savedCoordinate = { + position: antenna.position, + positionECEF: antenna.positionECEF, + accuracy: survey.accuracy, + savedAt: Date.now(), + }; + + dispatch(saveCoordinateForPreset({ presetId, coordinate: savedCoordinate })); + + dispatch( + showNotification({ + message: `Coordinate saved for preset ${presetId}`, + semantics: MessageSemantics.SUCCESS, + }) + ); +}; diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index 5a9469f57..7dec47fb5 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -4,7 +4,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { getPreferredCoordinateFormatter } from '~/selectors/formatting'; import type { RootState } from '~/store/reducers'; -import { RTKAntennaPositionFormat } from './types'; +import { RTKAntennaPositionFormat, type RTKSavedCoordinate } from './types'; /** * Returns whether the antenna position should be shown in ECEF coordinates. @@ -159,3 +159,65 @@ export const getSurveyStatus = createSelector( */ export const shouldShowSurveySettings = (state: RootState): boolean => state.rtk.dialog.surveySettingsEditorVisible; + +/** + * Returns whether there is a saved coordinate for the given RTK preset ID. + */ +export const hasSavedCoordinateForPreset = (state: RootState, presetId: string): boolean => + Boolean(state.rtk.savedCoordinates[presetId]); + +/** + * Returns the saved coordinate for the given RTK preset ID, or undefined if none exists. + */ +export const getSavedCoordinateForPreset = (state: RootState, presetId: string): RTKSavedCoordinate | undefined => + state.rtk.savedCoordinates[presetId]; + +/** + * Returns all saved coordinates as an array of { presetId, coordinate } objects. + */ +export const getAllSavedCoordinates = createSelector( + (state: RootState) => state.rtk.savedCoordinates, + (savedCoordinates) => + Object.entries(savedCoordinates).map(([presetId, coordinate]) => ({ + presetId, + coordinate, + })) +); + +/** + * Returns the formatted saved coordinate position for a given preset ID. + */ +export const getFormattedSavedCoordinatePosition = createSelector( + (state: RootState, presetId: string) => getSavedCoordinateForPreset(state, presetId), + getPreferredCoordinateFormatter, + isShowingAntennaPositionInECEF, + (savedCoordinate, formatter, isECEF) => { + if (!savedCoordinate) { + return undefined; + } + + if (isECEF) { + const { positionECEF } = savedCoordinate; + return positionECEF && Array.isArray(positionECEF) + ? `[${(positionECEF[0] / 1e3).toFixed(3)}, ${( + positionECEF[1] / 1e3 + ).toFixed(3)}, ${(positionECEF[2] / 1e3).toFixed(3)}]` + : undefined; + } else { + const { position } = savedCoordinate; + return position ? formatter(position) : undefined; + } + } +); + +/** + * Returns whether the coordinate restoration dialog should be visible. + */ +export const shouldShowCoordinateRestorationDialog = (state: RootState): boolean => + state.rtk.dialog.coordinateRestorationDialog.open; + +/** + * Returns the coordinate restoration dialog state. + */ +export const getCoordinateRestorationDialog = (state: RootState) => + state.rtk.dialog.coordinateRestorationDialog; diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index f2798b2ca..fc3b2df4f 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -8,15 +8,24 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { noPayload } from '~/utils/redux'; -import { RTKAntennaPositionFormat, type RTKStatistics } from './types'; +import { RTKAntennaPositionFormat, type RTKStatistics, type RTKSavedCoordinate } from './types'; type RTKSliceState = { stats: RTKStatistics; + /** Saved coordinates per RTK preset ID */ + savedCoordinates: Record; + dialog: { open: boolean; antennaPositionFormat: RTKAntennaPositionFormat; surveySettingsEditorVisible: boolean; + /** Dialog for asking user if they want to use saved coordinates */ + coordinateRestorationDialog: { + open: boolean; + presetId: string | null; + savedCoordinate: RTKSavedCoordinate | null; + }; }; }; @@ -39,10 +48,31 @@ const initialState: RTKSliceState = { }, }, + savedCoordinates: { + // TODO: Fake data for testing - remove in production + // '-dev-cu.usbmodem101-0': { + // position: [19.0402, 47.4979] as any, // Budapest coordinates + // positionECEF: [4080855000, 1408354000, 4679340000] as [number, number, number], // Approximate ECEF for Budapest + // accuracy: 0.02, + // savedAt: Date.now() - 86400000, // 1 day ago + // }, + // '-dev-cu.usbmodem101-1': { + // position: [21.6254, 47.5289] as any, // Debrecen coordinates + // positionECEF: [4010557000, 1590103000, 4681871000] as [number, number, number], // Approximate ECEF for Debrecen + // accuracy: 0.015, + // savedAt: Date.now() - 172800000, // 2 days ago + // }, + }, + dialog: { open: false, antennaPositionFormat: RTKAntennaPositionFormat.LON_LAT, surveySettingsEditorVisible: false, + coordinateRestorationDialog: { + open: false, + presetId: null, + savedCoordinate: null, + }, }, }; @@ -107,15 +137,57 @@ const { actions, reducer } = createSlice({ state.stats.survey.flags = survey.flags; } }, + + // Saved coordinates management + saveCoordinateForPreset( + state, + action: PayloadAction<{ presetId: string; coordinate: RTKSavedCoordinate }> + ) { + const { presetId, coordinate } = action.payload; + state.savedCoordinates[presetId] = coordinate; + }, + + removeSavedCoordinateForPreset( + state, + action: PayloadAction + ) { + const presetId = action.payload; + delete state.savedCoordinates[presetId]; + }, + + // Coordinate restoration dialog management + showCoordinateRestorationDialog( + state, + action: PayloadAction<{ presetId: string; savedCoordinate: RTKSavedCoordinate }> + ) { + const { presetId, savedCoordinate } = action.payload; + state.dialog.coordinateRestorationDialog = { + open: true, + presetId, + savedCoordinate, + }; + }, + + closeCoordinateRestorationDialog: noPayload((state) => { + state.dialog.coordinateRestorationDialog = { + open: false, + presetId: null, + savedCoordinate: null, + }; + }), }, }); export const { closeRTKSetupDialog, closeSurveySettingsPanel, + closeCoordinateRestorationDialog, + removeSavedCoordinateForPreset, resetRTKStatistics, + saveCoordinateForPreset, setAntennaPositionFormat, showRTKSetupDialog, + showCoordinateRestorationDialog, toggleSurveySettingsPanel, updateRTKStatistics, } = actions; diff --git a/src/features/rtk/types.ts b/src/features/rtk/types.ts index 4a4d8a61c..b49b496eb 100644 --- a/src/features/rtk/types.ts +++ b/src/features/rtk/types.ts @@ -6,6 +6,13 @@ export enum RTKAntennaPositionFormat { ECEF = 'ecef', } +export type RTKSavedCoordinate = { + position: LonLat; + positionECEF: Coordinate3D; + accuracy: number; + savedAt: number; +}; + export type RTKStatistics = { /** * Timestamp when the statistics was updated the last time, diff --git a/src/flockwave/operations.js b/src/flockwave/operations.js index ac910ed72..834c30efe 100644 --- a/src/flockwave/operations.js +++ b/src/flockwave/operations.js @@ -148,6 +148,23 @@ export async function startRTKSurvey(hub, { accuracy, duration }) { } } +/** + * Sets the RTK antenna position on the server by submitting explicit + * survey settings that contain a fixed position instead of starting a survey. + */ +export async function setRTKAntennaPosition(hub, position) { + const response = await hub.sendMessage({ + type: 'X-RTK-SURVEY', + settings: { + position, + }, + }); + + if (response.body.type !== 'ACK-ACK') { + throw new Error('Failed to set RTK antenna position on the server'); + } +} + /** * Asks the server to upload a drone show specification to a given UAV. */ @@ -282,6 +299,7 @@ export class OperationExecutor { setShowConfiguration, setShowLightConfiguration, startRTKSurvey, + setRTKAntennaPosition, uploadDroneShow, uploadFirmware, uploadMission, diff --git a/src/i18n/en.json b/src/i18n/en.json index 092243c2e..18e7dbab4 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -21,6 +21,13 @@ "passed": "Onboard preflight checks passed.", "signOffOn": "Sign off on onboard preflight checks" }, + "RTKCoordinateRestorationDialog": { + "cancel": "Cancel", + "message": "A saved coordinate exists for this RTK preset: Position: {{position}}, Accuracy: {{accuracy}}m, Saved: {{date}}, Do you want to use this saved coordinate or start a new survey?", + "startNew": "Start new survey", + "title": "Use saved coordinate?", + "useSaved": "Use saved coordinate" + }, "RTKCorrectionSourceSelector": { "RTKCorrections": "RTK corrections", "error": "Error while loading RTK sources from server", @@ -806,4 +813,4 @@ "timezone": "Timezone", "utcOffset": "UTC offset" } -} +} \ No newline at end of file diff --git a/src/i18n/hu.json b/src/i18n/hu.json index 451314b53..02dffb64d 100644 --- a/src/i18n/hu.json +++ b/src/i18n/hu.json @@ -27,6 +27,13 @@ "noRTKData": "Nincsenek RTK adatforrások a szerveren", "pleaseWait": "Kérem várjon, RTK források betöltése…" }, + "RTKCoordinateRestorationDialog": { + "title": "Használod a mentett koordinátát?", + "message": "Ehhez az RTK beállításhoz mentett koordináta áll rendelkezésre: Pozíció: {{position}}, Pontosság: {{accuracy}} m, Mentve: {{date}}, A mentett koordinátát szeretnéd használni, vagy indítsunk új felmérést?", + "cancel": "Mégse", + "startNew": "Új felmérés", + "useSaved": "Mentett használata" + }, "RTKMessage": { "noRTKMessagesYet": "Még nem érkezett RTK üzenet" }, From 2981b3c4249fc418d48c1be627104811abb55eda Mon Sep 17 00:00:00 2001 From: zarcell Date: Wed, 15 Oct 2025 09:11:53 +0200 Subject: [PATCH 02/70] chore: prettier and xo fixes --- .../rtk/RTKCoordinateRestorationDialog.jsx | 45 ++++--- .../rtk/RTKCorrectionSourceSelector.jsx | 41 ++++-- src/features/rtk/RTKSatelliteObservations.jsx | 2 +- src/features/rtk/RTKStatusUpdater.jsx | 45 ++++--- src/features/rtk/actions.js | 118 ++++++++++-------- src/features/rtk/selectors.ts | 24 ++-- src/features/rtk/slice.ts | 25 ++-- 7 files changed, 180 insertions(+), 120 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index e91571321..8710a6e51 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -11,16 +11,22 @@ import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; import { closeCoordinateRestorationDialog } from '~/features/rtk/slice'; -import { getCoordinateRestorationDialog, getFormattedSavedCoordinatePosition } from '~/features/rtk/selectors'; -import { setSelectedPresetAsSource, useSavedCoordinateForPreset } from '~/features/rtk/actions'; +import { + getCoordinateRestorationDialog, + getFormattedSavedCoordinatePosition, +} from '~/features/rtk/selectors'; +import { + setSelectedPresetAsSource, + useSavedCoordinateForPreset, +} from '~/features/rtk/actions'; -const RTKCoordinateRestorationDialog = ({ - t, - dialog, - formattedPosition, - onClose, - onUseSaved, - onStartNew +const RTKCoordinateRestorationDialog = ({ + t, + dialog, + formattedPosition, + onClose, + onUseSaved, + onStartNew, }) => { if (!dialog.open || !dialog.savedCoordinate) { return null; @@ -42,13 +48,13 @@ const RTKCoordinateRestorationDialog = ({ return ( - + {t('RTKCoordinateRestorationDialog.title')} @@ -61,13 +67,13 @@ const RTKCoordinateRestorationDialog = ({ - - - @@ -87,8 +93,11 @@ RTKCoordinateRestorationDialog.propTypes = { export default connect( (state) => ({ dialog: getCoordinateRestorationDialog(state), - formattedPosition: getCoordinateRestorationDialog(state).savedCoordinate - ? getFormattedSavedCoordinatePosition(state, getCoordinateRestorationDialog(state).presetId) + formattedPosition: getCoordinateRestorationDialog(state).savedCoordinate + ? getFormattedSavedCoordinatePosition( + state, + getCoordinateRestorationDialog(state).presetId + ) : undefined, }), { diff --git a/src/features/rtk/RTKCorrectionSourceSelector.jsx b/src/features/rtk/RTKCorrectionSourceSelector.jsx index 84b5d4246..1b8c439bd 100644 --- a/src/features/rtk/RTKCorrectionSourceSelector.jsx +++ b/src/features/rtk/RTKCorrectionSourceSelector.jsx @@ -10,8 +10,14 @@ import InputLabel from '@material-ui/core/InputLabel'; import MenuItem from '@material-ui/core/MenuItem'; import Select from '@material-ui/core/Select'; -import { resetRTKStatistics, showCoordinateRestorationDialog } from '~/features/rtk/slice'; -import { hasSavedCoordinateForPreset, getSavedCoordinateForPreset } from '~/features/rtk/selectors'; +import { + resetRTKStatistics, + showCoordinateRestorationDialog, +} from '~/features/rtk/slice'; +import { + hasSavedCoordinateForPreset, + getSavedCoordinateForPreset, +} from '~/features/rtk/selectors'; import messageHub from '~/message-hub'; const NULL_ID = '__null__'; @@ -21,7 +27,13 @@ const nullPreset = { title: 'RTK disabled', }; -const RTKCorrectionSourceSelector = ({ onSourceChanged, t, hasSavedCoordinateForPreset, getSavedCoordinateForPreset, showCoordinateRestorationDialog }) => { +const RTKCorrectionSourceSelector = ({ + onSourceChanged, + t, + hasSavedCoordinateForPreset, + getSavedCoordinateForPreset, + showCoordinateRestorationDialog, +}) => { const [selectedByUser, setSelectedByUser] = useState(); const [selectionState, getSelectionFromServer] = useAsyncFn(async () => messageHub.query.getSelectedRTKPresetId() @@ -35,23 +47,24 @@ const RTKCorrectionSourceSelector = ({ onSourceChanged, t, hasSavedCoordinateFor const loading = presetsState.loading || selectionState.loading; const hasError = presetsState.error || selectionState.error; - const presets = presetsState.value ? presetsState.value : []; + const presets = presetsState.value || []; const hasSelectionFromServer = selectionState.value !== undefined; const selectedOnServer = - selectionState.value !== undefined - ? selectionState.value === null - ? NULL_ID - : selectionState.value - : undefined; + selectionState.value === undefined + ? undefined + : (selectionState.value ?? NULL_ID); const hasPresets = presets && presets.length > 0; const handleChange = async (event) => { const newPresetId = event.target.value; - + // Check if there's a saved coordinate for this preset if (newPresetId !== NULL_ID && hasSavedCoordinateForPreset(newPresetId)) { const savedCoordinate = getSavedCoordinateForPreset(newPresetId); - showCoordinateRestorationDialog({ presetId: newPresetId, savedCoordinate }); + showCoordinateRestorationDialog({ + presetId: newPresetId, + savedCoordinate, + }); // Don't proceed with the selection yet - wait for user decision in dialog // return; } @@ -164,8 +177,10 @@ RTKCorrectionSourceSelector.propTypes = { export default connect( // mapStateToProps (state) => ({ - hasSavedCoordinateForPreset: (presetId) => hasSavedCoordinateForPreset(state, presetId), - getSavedCoordinateForPreset: (presetId) => getSavedCoordinateForPreset(state, presetId), + hasSavedCoordinateForPreset: (presetId) => + hasSavedCoordinateForPreset(state, presetId), + getSavedCoordinateForPreset: (presetId) => + getSavedCoordinateForPreset(state, presetId), }), // mapDispatchToProps { diff --git a/src/features/rtk/RTKSatelliteObservations.jsx b/src/features/rtk/RTKSatelliteObservations.jsx index 93f8c56f7..77c6fdcfb 100644 --- a/src/features/rtk/RTKSatelliteObservations.jsx +++ b/src/features/rtk/RTKSatelliteObservations.jsx @@ -175,7 +175,7 @@ const RTKSatelliteObservations = ({ height, items }) => { items ? createDataFromItemsAndDrawingContext(items, chart.ctx) : NO_DATA ); } - }, [chartRef.current, items]); + }, [items]); return ( diff --git a/src/features/rtk/RTKStatusUpdater.jsx b/src/features/rtk/RTKStatusUpdater.jsx index e2bbc2424..3bd33cecb 100644 --- a/src/features/rtk/RTKStatusUpdater.jsx +++ b/src/features/rtk/RTKStatusUpdater.jsx @@ -36,29 +36,40 @@ const RTKStatusUpdater = ({ onStatusChanged, period }) => { const hasECEF = Array.isArray(status?.antenna?.positionECEF); const accuracy = status?.survey?.accuracy; const flags = status?.survey?.flags; - const surveyedCoordinateValid = typeof flags === 'number' ? (flags & 0b100) !== 0 : false; - const hasValidFix = hasECEF && (surveyedCoordinateValid || typeof accuracy === 'number'); + const surveyedCoordinateValid = + typeof flags === 'number' ? (flags & 0b100) !== 0 : false; + const hasValidFix = + hasECEF && + (surveyedCoordinateValid || typeof accuracy === 'number'); if (hasValidFix) { - // eslint-disable-next-line no-await-in-loop - const selectedPresetId = await messageHub.query.getSelectedRTKPresetId(); - if (selectedPresetId) { - const incomingECEF = Array.isArray(status?.antenna?.positionECEF) - ? status.antenna.positionECEF.slice(0, 3).map((x) => Math.round(x)) - : undefined; - const saved = savedCoordinates && savedCoordinates[selectedPresetId]; - const savedECEF = saved && Array.isArray(saved.positionECEF) + const selectedPresetId = + // eslint-disable-next-line no-await-in-loop + await messageHub.query.getSelectedRTKPresetId(); + if (!selectedPresetId) { + continue; + } + + const incomingECEF = Array.isArray(status?.antenna?.positionECEF) + ? status.antenna.positionECEF + .slice(0, 3) + .map((x) => Math.round(x)) + : undefined; + const saved = + savedCoordinates && savedCoordinates[selectedPresetId]; + const savedECEF = + saved && Array.isArray(saved.positionECEF) ? saved.positionECEF.slice(0, 3) : undefined; - const isSameECEF = - incomingECEF && savedECEF - ? incomingECEF.length === savedECEF.length && incomingECEF.every((v, i) => v === savedECEF[i]) - : false; + const isSameECEF = + incomingECEF && savedECEF + ? incomingECEF.length === savedECEF.length && + incomingECEF.every((v, i) => v === savedECEF[i]) + : false; - if (!isSameECEF) { - dispatch(saveCurrentCoordinateForPreset(selectedPresetId)); - } + if (!isSameECEF) { + dispatch(saveCurrentCoordinateForPreset(selectedPresetId)); } } } catch (error) { diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index c2ee6b3b5..8b29bf05f 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -7,7 +7,11 @@ import { getFormattedAntennaPosition, isShowingAntennaPositionInECEF, } from './selectors'; -import { closeSurveySettingsPanel, setAntennaPositionFormat, saveCoordinateForPreset } from './slice'; +import { + closeSurveySettingsPanel, + setAntennaPositionFormat, + saveCoordinateForPreset, +} from './slice'; export const copyAntennaPositionToClipboard = () => (dispatch, getState) => { copy(getFormattedAntennaPosition(getState())); @@ -36,7 +40,7 @@ export const startNewSurveyOnServer = (settings) => async (dispatch) => { export const setSelectedPresetAsSource = (presetId) => async (dispatch) => { try { await messageHub.execute.setRTKCorrectionsSource(presetId); - } catch (error) { + } catch { dispatch( showNotification({ message: 'Failed to set RTK correction source.', @@ -62,68 +66,74 @@ export const toggleAntennaPositionFormat = () => (dispatch, getState) => { ); }; -export const useSavedCoordinateForPreset = (presetId, savedCoordinate) => async (dispatch, getState) => { - try { - // Set the saved coordinate as the current antenna position - await messageHub.execute.setRTKAntennaPosition(savedCoordinate.positionECEF); - - // Check if the component is still mounted by checking if the dialog is still open - const currentState = getState(); - if (!currentState.rtk.dialog.coordinateRestorationDialog.open) { - return; +export const useSavedCoordinateForPreset = + (presetId, savedCoordinate) => async (dispatch, getState) => { + try { + // Set the saved coordinate as the current antenna position + await messageHub.execute.setRTKAntennaPosition( + savedCoordinate.positionECEF + ); + + // Check if the component is still mounted by checking if the dialog is still open + const currentState = getState(); + if (!currentState.rtk.dialog.coordinateRestorationDialog.open) { + return; + } + + dispatch( + showNotification({ + message: `Using saved coordinate for preset ${presetId}`, + semantics: MessageSemantics.SUCCESS, + }) + ); + } catch (error) { + console.warn('Failed to set saved coordinate:', error); + + // Check if the component is still mounted + const currentState = getState(); + if (!currentState.rtk.dialog.coordinateRestorationDialog.open) { + return; + } + + dispatch( + showNotification({ + message: 'Failed to use saved coordinate.', + semantics: MessageSemantics.ERROR, + }) + ); } + }; - dispatch( - showNotification({ - message: `Using saved coordinate for preset ${presetId}`, - semantics: MessageSemantics.SUCCESS, - }) - ); - } catch (error) { - console.warn('Failed to set saved coordinate:', error); - - // Check if the component is still mounted - const currentState = getState(); - if (!currentState.rtk.dialog.coordinateRestorationDialog.open) { +export const saveCurrentCoordinateForPreset = + (presetId) => (dispatch, getState) => { + const state = getState(); + const { antenna, survey } = state.rtk.stats; + + if (!antenna.position || !antenna.positionECEF || !survey.accuracy) { + dispatch( + showNotification({ + message: 'No valid coordinate data available to save.', + semantics: MessageSemantics.ERROR, + }) + ); return; } + const savedCoordinate = { + position: antenna.position, + positionECEF: antenna.positionECEF, + accuracy: survey.accuracy, + savedAt: Date.now(), + }; + dispatch( - showNotification({ - message: 'Failed to use saved coordinate.', - semantics: MessageSemantics.ERROR, - }) + saveCoordinateForPreset({ presetId, coordinate: savedCoordinate }) ); - } -}; - -export const saveCurrentCoordinateForPreset = (presetId) => (dispatch, getState) => { - const state = getState(); - const { antenna, survey } = state.rtk.stats; - if (!antenna.position || !antenna.positionECEF || !survey.accuracy) { dispatch( showNotification({ - message: 'No valid coordinate data available to save.', - semantics: MessageSemantics.ERROR, + message: `Coordinate saved for preset ${presetId}`, + semantics: MessageSemantics.SUCCESS, }) ); - return; - } - - const savedCoordinate = { - position: antenna.position, - positionECEF: antenna.positionECEF, - accuracy: survey.accuracy, - savedAt: Date.now(), }; - - dispatch(saveCoordinateForPreset({ presetId, coordinate: savedCoordinate })); - - dispatch( - showNotification({ - message: `Coordinate saved for preset ${presetId}`, - semantics: MessageSemantics.SUCCESS, - }) - ); -}; diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index 7dec47fb5..41da9ca60 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -163,14 +163,18 @@ export const shouldShowSurveySettings = (state: RootState): boolean => /** * Returns whether there is a saved coordinate for the given RTK preset ID. */ -export const hasSavedCoordinateForPreset = (state: RootState, presetId: string): boolean => - Boolean(state.rtk.savedCoordinates[presetId]); +export const hasSavedCoordinateForPreset = ( + state: RootState, + presetId: string +): boolean => Boolean(state.rtk.savedCoordinates[presetId]); /** * Returns the saved coordinate for the given RTK preset ID, or undefined if none exists. */ -export const getSavedCoordinateForPreset = (state: RootState, presetId: string): RTKSavedCoordinate | undefined => - state.rtk.savedCoordinates[presetId]; +export const getSavedCoordinateForPreset = ( + state: RootState, + presetId: string +): RTKSavedCoordinate | undefined => state.rtk.savedCoordinates[presetId]; /** * Returns all saved coordinates as an array of { presetId, coordinate } objects. @@ -188,7 +192,8 @@ export const getAllSavedCoordinates = createSelector( * Returns the formatted saved coordinate position for a given preset ID. */ export const getFormattedSavedCoordinatePosition = createSelector( - (state: RootState, presetId: string) => getSavedCoordinateForPreset(state, presetId), + (state: RootState, presetId: string) => + getSavedCoordinateForPreset(state, presetId), getPreferredCoordinateFormatter, isShowingAntennaPositionInECEF, (savedCoordinate, formatter, isECEF) => { @@ -213,11 +218,14 @@ export const getFormattedSavedCoordinatePosition = createSelector( /** * Returns whether the coordinate restoration dialog should be visible. */ -export const shouldShowCoordinateRestorationDialog = (state: RootState): boolean => - state.rtk.dialog.coordinateRestorationDialog.open; +export const shouldShowCoordinateRestorationDialog = ( + state: RootState +): boolean => state.rtk.dialog.coordinateRestorationDialog.open; /** * Returns the coordinate restoration dialog state. */ -export const getCoordinateRestorationDialog = (state: RootState) => +export const getCoordinateRestorationDialog = ( + state: RootState +): RootState['rtk']['dialog']['coordinateRestorationDialog'] => state.rtk.dialog.coordinateRestorationDialog; diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index fc3b2df4f..871e03540 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -8,7 +8,11 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { noPayload } from '~/utils/redux'; -import { RTKAntennaPositionFormat, type RTKStatistics, type RTKSavedCoordinate } from './types'; +import { + RTKAntennaPositionFormat, + type RTKStatistics, + type RTKSavedCoordinate, +} from './types'; type RTKSliceState = { stats: RTKStatistics; @@ -23,8 +27,8 @@ type RTKSliceState = { /** Dialog for asking user if they want to use saved coordinates */ coordinateRestorationDialog: { open: boolean; - presetId: string | null; - savedCoordinate: RTKSavedCoordinate | null; + presetId: string | undefined; + savedCoordinate: RTKSavedCoordinate | undefined; }; }; }; @@ -141,16 +145,16 @@ const { actions, reducer } = createSlice({ // Saved coordinates management saveCoordinateForPreset( state, - action: PayloadAction<{ presetId: string; coordinate: RTKSavedCoordinate }> + action: PayloadAction<{ + presetId: string; + coordinate: RTKSavedCoordinate; + }> ) { const { presetId, coordinate } = action.payload; state.savedCoordinates[presetId] = coordinate; }, - removeSavedCoordinateForPreset( - state, - action: PayloadAction - ) { + removeSavedCoordinateForPreset(state, action: PayloadAction) { const presetId = action.payload; delete state.savedCoordinates[presetId]; }, @@ -158,7 +162,10 @@ const { actions, reducer } = createSlice({ // Coordinate restoration dialog management showCoordinateRestorationDialog( state, - action: PayloadAction<{ presetId: string; savedCoordinate: RTKSavedCoordinate }> + action: PayloadAction<{ + presetId: string; + savedCoordinate: RTKSavedCoordinate; + }> ) { const { presetId, savedCoordinate } = action.payload; state.dialog.coordinateRestorationDialog = { From 89c7528ad1fc8bc9d8b3b055366ad1d43bd5d5be Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 20 Oct 2025 10:24:52 +0200 Subject: [PATCH 03/70] fix: remove unused function --- src/features/rtk/selectors.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index 41da9ca60..8d5d44fd9 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -215,13 +215,6 @@ export const getFormattedSavedCoordinatePosition = createSelector( } ); -/** - * Returns whether the coordinate restoration dialog should be visible. - */ -export const shouldShowCoordinateRestorationDialog = ( - state: RootState -): boolean => state.rtk.dialog.coordinateRestorationDialog.open; - /** * Returns the coordinate restoration dialog state. */ From cfb4612275f97eada987cc299ca2233c1649fdb4 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 20 Oct 2025 10:25:10 +0200 Subject: [PATCH 04/70] fix: format dialog text --- .../rtk/RTKCoordinateRestorationDialog.jsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 8710a6e51..bddb21c26 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -1,3 +1,4 @@ +import formatDate from 'date-fns/format'; import PropTypes from 'prop-types'; import React from 'react'; import { withTranslation } from 'react-i18next'; @@ -34,7 +35,7 @@ const RTKCoordinateRestorationDialog = ({ const { presetId, savedCoordinate } = dialog; const { accuracy, savedAt } = savedCoordinate; - const savedDate = new Date(savedAt).toLocaleDateString(); + const savedDate = formatDate(new Date(savedAt), 'yyyy-MM-dd'); const handleUseSaved = () => { onClose(); // Close dialog first @@ -91,15 +92,15 @@ RTKCoordinateRestorationDialog.propTypes = { }; export default connect( - (state) => ({ - dialog: getCoordinateRestorationDialog(state), - formattedPosition: getCoordinateRestorationDialog(state).savedCoordinate - ? getFormattedSavedCoordinatePosition( - state, - getCoordinateRestorationDialog(state).presetId - ) - : undefined, - }), + (state) => { + const dialog = getCoordinateRestorationDialog(state); + return { + dialog, + formattedPosition: dialog.savedCoordinate && dialog.presetId + ? getFormattedSavedCoordinatePosition(state, dialog.presetId) + : undefined, + }; + }, { onClose: closeCoordinateRestorationDialog, onUseSaved: useSavedCoordinateForPreset, From acdb11d0951f817ef8e895038fad8119a5404807 Mon Sep 17 00:00:00 2001 From: zarcell Date: Wed, 5 Nov 2025 10:26:04 +0100 Subject: [PATCH 05/70] fix: align slice dialog state with types --- src/features/rtk/slice.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index 871e03540..f8698428a 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -74,8 +74,8 @@ const initialState: RTKSliceState = { surveySettingsEditorVisible: false, coordinateRestorationDialog: { open: false, - presetId: null, - savedCoordinate: null, + presetId: undefined, + savedCoordinate: undefined, }, }, }; @@ -178,8 +178,8 @@ const { actions, reducer } = createSlice({ closeCoordinateRestorationDialog: noPayload((state) => { state.dialog.coordinateRestorationDialog = { open: false, - presetId: null, - savedCoordinate: null, + presetId: undefined, + savedCoordinate: undefined, }; }), }, From d65737ee91d05890590165a3ec32cfce9b8cd9b7 Mon Sep 17 00:00:00 2001 From: zarcell Date: Wed, 12 Nov 2025 11:13:03 +0100 Subject: [PATCH 06/70] fix(rtk): cleaner dialog, update accuracy, delete rtk from balcklist and add filter --- .../rtk/RTKCoordinateRestorationDialog.jsx | 43 +++++++++---------- src/features/rtk/actions.js | 31 ++----------- src/flockwave/operations.js | 3 +- src/i18n/en.json | 9 ++-- src/i18n/hu.json | 9 ++-- src/store/index.js | 4 +- 6 files changed, 39 insertions(+), 60 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index bddb21c26..ada04a0af 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -4,22 +4,21 @@ import React from 'react'; import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; +import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; +import Typography from '@material-ui/core/Typography'; import { closeCoordinateRestorationDialog } from '~/features/rtk/slice'; import { getCoordinateRestorationDialog, getFormattedSavedCoordinatePosition, } from '~/features/rtk/selectors'; -import { - setSelectedPresetAsSource, - useSavedCoordinateForPreset, -} from '~/features/rtk/actions'; +import { useSavedCoordinateForPreset } from '~/features/rtk/actions'; const RTKCoordinateRestorationDialog = ({ t, @@ -27,7 +26,6 @@ const RTKCoordinateRestorationDialog = ({ formattedPosition, onClose, onUseSaved, - onStartNew, }) => { if (!dialog.open || !dialog.savedCoordinate) { return null; @@ -35,18 +33,13 @@ const RTKCoordinateRestorationDialog = ({ const { presetId, savedCoordinate } = dialog; const { accuracy, savedAt } = savedCoordinate; - const savedDate = formatDate(new Date(savedAt), 'yyyy-MM-dd'); + const savedDateTime = formatDate(new Date(savedAt), 'yyyy-MM-dd HH:mm:ss'); const handleUseSaved = () => { onClose(); // Close dialog first onUseSaved(presetId, savedCoordinate); // Then start async operation }; - const handleStartNew = () => { - onStartNew(presetId); - onClose(); - }; - return ( - - {t('RTKCoordinateRestorationDialog.message', { - position: formattedPosition, - accuracy: accuracy.toFixed(3), - date: savedDate, - })} - + + + + {t('RTKCoordinateRestorationDialog.positionLabel')}:{' '} + {formattedPosition} + + + {t('RTKCoordinateRestorationDialog.accuracyLabel')}:{' '} + {accuracy.toFixed(3)} m + + + {t('RTKCoordinateRestorationDialog.dateLabel')}:{' '} + {savedDateTime} + + + - @@ -88,7 +87,6 @@ RTKCoordinateRestorationDialog.propTypes = { formattedPosition: PropTypes.string, onClose: PropTypes.func, onUseSaved: PropTypes.func, - onStartNew: PropTypes.func, }; export default connect( @@ -104,6 +102,5 @@ export default connect( { onClose: closeCoordinateRestorationDialog, onUseSaved: useSavedCoordinateForPreset, - onStartNew: setSelectedPresetAsSource, } )(withTranslation()(RTKCoordinateRestorationDialog)); diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index 8b29bf05f..552ee787c 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -34,30 +34,6 @@ export const startNewSurveyOnServer = (settings) => async (dispatch) => { dispatch(closeSurveySettingsPanel()); }; -/** - * Sets the server-side RTK correction source to the given preset. - */ -export const setSelectedPresetAsSource = (presetId) => async (dispatch) => { - try { - await messageHub.execute.setRTKCorrectionsSource(presetId); - } catch { - dispatch( - showNotification({ - message: 'Failed to set RTK correction source.', - semantics: MessageSemantics.ERROR, - }) - ); - return; - } - - dispatch( - showNotification({ - message: `RTK correction source set to preset ${presetId}`, - semantics: MessageSemantics.SUCCESS, - }) - ); -}; - export const toggleAntennaPositionFormat = () => (dispatch, getState) => { dispatch( setAntennaPositionFormat( @@ -70,9 +46,10 @@ export const useSavedCoordinateForPreset = (presetId, savedCoordinate) => async (dispatch, getState) => { try { // Set the saved coordinate as the current antenna position - await messageHub.execute.setRTKAntennaPosition( - savedCoordinate.positionECEF - ); + await messageHub.execute.setRTKAntennaPosition({ + position: savedCoordinate.positionECEF, + accuracy: savedCoordinate.accuracy, + }); // Check if the component is still mounted by checking if the dialog is still open const currentState = getState(); diff --git a/src/flockwave/operations.js b/src/flockwave/operations.js index 834c30efe..6d6ee0572 100644 --- a/src/flockwave/operations.js +++ b/src/flockwave/operations.js @@ -152,11 +152,12 @@ export async function startRTKSurvey(hub, { accuracy, duration }) { * Sets the RTK antenna position on the server by submitting explicit * survey settings that contain a fixed position instead of starting a survey. */ -export async function setRTKAntennaPosition(hub, position) { +export async function setRTKAntennaPosition(hub, { position, accuracy }) { const response = await hub.sendMessage({ type: 'X-RTK-SURVEY', settings: { position, + accuracy, }, }); diff --git a/src/i18n/en.json b/src/i18n/en.json index 18e7dbab4..4984a0fe6 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -22,11 +22,12 @@ "signOffOn": "Sign off on onboard preflight checks" }, "RTKCoordinateRestorationDialog": { + "accuracyLabel": "Accuracy", "cancel": "Cancel", - "message": "A saved coordinate exists for this RTK preset: Position: {{position}}, Accuracy: {{accuracy}}m, Saved: {{date}}, Do you want to use this saved coordinate or start a new survey?", - "startNew": "Start new survey", - "title": "Use saved coordinate?", - "useSaved": "Use saved coordinate" + "dateLabel": "Saved", + "positionLabel": "Position", + "title": "Use last known coordinate?", + "useSaved": "Use coordinate" }, "RTKCorrectionSourceSelector": { "RTKCorrections": "RTK corrections", diff --git a/src/i18n/hu.json b/src/i18n/hu.json index 02dffb64d..eefaa45b4 100644 --- a/src/i18n/hu.json +++ b/src/i18n/hu.json @@ -28,11 +28,12 @@ "pleaseWait": "Kérem várjon, RTK források betöltése…" }, "RTKCoordinateRestorationDialog": { - "title": "Használod a mentett koordinátát?", - "message": "Ehhez az RTK beállításhoz mentett koordináta áll rendelkezésre: Pozíció: {{position}}, Pontosság: {{accuracy}} m, Mentve: {{date}}, A mentett koordinátát szeretnéd használni, vagy indítsunk új felmérést?", + "title": "Használod a legutóbbi ismert koordinátát?", + "positionLabel": "Pozíció", + "accuracyLabel": "Pontosság", + "dateLabel": "Mentve", "cancel": "Mégse", - "startNew": "Új felmérés", - "useSaved": "Mentett használata" + "useSaved": "Koordináta használata" }, "RTKMessage": { "noRTKMessagesYet": "Még nem érkezett RTK üzenet" diff --git a/src/store/index.js b/src/store/index.js index 200fccfce..18b715320 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -62,7 +62,6 @@ const persistConfig = { 'log', 'logDownload', 'messages', - 'rtk', 'servers', 'session', 'snackbar', @@ -113,6 +112,9 @@ const persistConfig = { // Store only the persistent settings of the upload procedure createFilter('upload', ['settings']), + // Store only saved coordinates from RTK + createFilter('rtk', ['savedCoordinates']), + // We do not wish to save 3D view tooltips, camera pose or the scene ID createBlacklistFilter('threeD', ['camera', 'tooltip', 'sceneId']), ], From 4cb8f3c89ead66e0cc2d8fa0285501ed950c31dd Mon Sep 17 00:00:00 2001 From: zarcell Date: Wed, 3 Dec 2025 22:32:40 +0100 Subject: [PATCH 07/70] feat(rtk): improve saved coordinate management and restoration flow --- .../rtk/RTKCoordinateRestorationDialog.jsx | 74 ++++++++++-------- .../rtk/RTKCorrectionSourceSelector.jsx | 51 +++++-------- .../rtk/RTKSetupDialogBottomPanel.jsx | 68 +++++++++++++++-- src/features/rtk/actions.js | 15 ++++ src/features/rtk/selectors.ts | 31 +++++--- src/features/rtk/slice.ts | 75 ++++++++++++++----- src/flockwave/queries.js | 7 +- src/i18n/en.json | 1 + 8 files changed, 225 insertions(+), 97 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index ada04a0af..ecc219bb3 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -9,35 +9,51 @@ import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; -import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; -import Typography from '@material-ui/core/Typography'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; import { closeCoordinateRestorationDialog } from '~/features/rtk/slice'; import { getCoordinateRestorationDialog, - getFormattedSavedCoordinatePosition, + getFormattedCoordinatePosition, + getSavedCoordinatesForPreset, } from '~/features/rtk/selectors'; import { useSavedCoordinateForPreset } from '~/features/rtk/actions'; +const SavedCoordinateItem = connect((state, { coordinate }) => ({ + formattedPosition: getFormattedCoordinatePosition(state, coordinate), +}))(({ coordinate, formattedPosition, onClick }) => { + const { accuracy, savedAt } = coordinate; + const savedDateTime = formatDate(new Date(savedAt), 'yyyy-MM-dd HH:mm:ss'); + + return ( + onClick(coordinate)}> + + + ); +}); + const RTKCoordinateRestorationDialog = ({ t, dialog, - formattedPosition, + savedCoordinates, onClose, onUseSaved, }) => { - if (!dialog.open || !dialog.savedCoordinate) { + if (!dialog.open) { return null; } - const { presetId, savedCoordinate } = dialog; - const { accuracy, savedAt } = savedCoordinate; - const savedDateTime = formatDate(new Date(savedAt), 'yyyy-MM-dd HH:mm:ss'); + const { presetId } = dialog; - const handleUseSaved = () => { + const handleUseSaved = (coordinate) => { onClose(); // Close dialog first - onUseSaved(presetId, savedCoordinate); // Then start async operation + onUseSaved(presetId, coordinate); // Then start async operation }; return ( @@ -53,29 +69,27 @@ const RTKCoordinateRestorationDialog = ({ - - - {t('RTKCoordinateRestorationDialog.positionLabel')}:{' '} - {formattedPosition} - - - {t('RTKCoordinateRestorationDialog.accuracyLabel')}:{' '} - {accuracy.toFixed(3)} m + {savedCoordinates.length === 0 ? ( + + {t('RTKCoordinateRestorationDialog.noSavedCoordinates')} - - {t('RTKCoordinateRestorationDialog.dateLabel')}:{' '} - {savedDateTime} - - + ) : ( + + {savedCoordinates.map((coordinate, index) => ( + + ))} + + )} - ); @@ -84,7 +98,7 @@ const RTKCoordinateRestorationDialog = ({ RTKCoordinateRestorationDialog.propTypes = { t: PropTypes.func, dialog: PropTypes.object, - formattedPosition: PropTypes.string, + savedCoordinates: PropTypes.array, onClose: PropTypes.func, onUseSaved: PropTypes.func, }; @@ -94,9 +108,9 @@ export default connect( const dialog = getCoordinateRestorationDialog(state); return { dialog, - formattedPosition: dialog.savedCoordinate && dialog.presetId - ? getFormattedSavedCoordinatePosition(state, dialog.presetId) - : undefined, + savedCoordinates: dialog.presetId + ? getSavedCoordinatesForPreset(state, dialog.presetId) + : [], }; }, { diff --git a/src/features/rtk/RTKCorrectionSourceSelector.jsx b/src/features/rtk/RTKCorrectionSourceSelector.jsx index 1b8c439bd..6ba925886 100644 --- a/src/features/rtk/RTKCorrectionSourceSelector.jsx +++ b/src/features/rtk/RTKCorrectionSourceSelector.jsx @@ -12,12 +12,8 @@ import Select from '@material-ui/core/Select'; import { resetRTKStatistics, - showCoordinateRestorationDialog, + setCurrentRTKPresetId, } from '~/features/rtk/slice'; -import { - hasSavedCoordinateForPreset, - getSavedCoordinateForPreset, -} from '~/features/rtk/selectors'; import messageHub from '~/message-hub'; const NULL_ID = '__null__'; @@ -30,9 +26,7 @@ const nullPreset = { const RTKCorrectionSourceSelector = ({ onSourceChanged, t, - hasSavedCoordinateForPreset, - getSavedCoordinateForPreset, - showCoordinateRestorationDialog, + setCurrentRTKPresetId, }) => { const [selectedByUser, setSelectedByUser] = useState(); const [selectionState, getSelectionFromServer] = useAsyncFn(async () => @@ -45,7 +39,11 @@ const RTKCorrectionSourceSelector = ({ ); const loading = presetsState.loading || selectionState.loading; - const hasError = presetsState.error || selectionState.error; + const hasError = presetsState.error; + + if (selectionState.error) { + console.warn('Failed to load RTK selection state:', selectionState.error); + } const presets = presetsState.value || []; const hasSelectionFromServer = selectionState.value !== undefined; @@ -58,17 +56,6 @@ const RTKCorrectionSourceSelector = ({ const handleChange = async (event) => { const newPresetId = event.target.value; - // Check if there's a saved coordinate for this preset - if (newPresetId !== NULL_ID && hasSavedCoordinateForPreset(newPresetId)) { - const savedCoordinate = getSavedCoordinateForPreset(newPresetId); - showCoordinateRestorationDialog({ - presetId: newPresetId, - savedCoordinate, - }); - // Don't proceed with the selection yet - wait for user decision in dialog - // return; - } - // We assume that the request will succeed so we eagerly select the new // value. If changing the RTK source fails, it will be changed back in the // response handler triggered by the effect that we set up below. @@ -79,6 +66,16 @@ const RTKCorrectionSourceSelector = ({ } }; + // Update current preset ID in Redux + useEffect(() => { + const currentId = + selectedByUser || (hasSelectionFromServer ? selectedOnServer : undefined); + // Convert NULL_ID to undefined or keep as is? The selector uses 'undefined' for no selection usually. + // But presets have IDs. + const effectiveId = currentId === NULL_ID ? undefined : currentId; + setCurrentRTKPresetId(effectiveId); + }, [selectedByUser, selectedOnServer, hasSelectionFromServer, setCurrentRTKPresetId]); + // If we have the preset list, but we don't have the current selection yet, // load the current selection useEffect(() => { @@ -169,22 +166,14 @@ const RTKCorrectionSourceSelector = ({ RTKCorrectionSourceSelector.propTypes = { onSourceChanged: PropTypes.func, t: PropTypes.func, - hasSavedCoordinateForPreset: PropTypes.func, - getSavedCoordinateForPreset: PropTypes.func, - showCoordinateRestorationDialog: PropTypes.func, + setCurrentRTKPresetId: PropTypes.func, }; export default connect( - // mapStateToProps - (state) => ({ - hasSavedCoordinateForPreset: (presetId) => - hasSavedCoordinateForPreset(state, presetId), - getSavedCoordinateForPreset: (presetId) => - getSavedCoordinateForPreset(state, presetId), - }), + null, // mapDispatchToProps { onSourceChanged: resetRTKStatistics, - showCoordinateRestorationDialog, + setCurrentRTKPresetId, } )(withTranslation()(RTKCorrectionSourceSelector)); diff --git a/src/features/rtk/RTKSetupDialogBottomPanel.jsx b/src/features/rtk/RTKSetupDialogBottomPanel.jsx index 34179d66f..6fc79aa5f 100644 --- a/src/features/rtk/RTKSetupDialogBottomPanel.jsx +++ b/src/features/rtk/RTKSetupDialogBottomPanel.jsx @@ -6,7 +6,9 @@ import { connect } from 'react-redux'; import Box from '@material-ui/core/Box'; import IconButton from '@material-ui/core/IconButton'; import { makeStyles } from '@material-ui/core/styles'; +import Delete from '@material-ui/icons/Delete'; import Place from '@material-ui/icons/Place'; +import Restore from '@material-ui/icons/Restore'; import { createSecondaryAreaStyle, @@ -16,8 +18,17 @@ import Tooltip from '@skybrush/mui-components/lib/Tooltip'; import FadeAndSlide from '~/components/transitions/FadeAndSlide'; -import { getSurveyStatus, shouldShowSurveySettings } from './selectors'; -import { toggleSurveySettingsPanel } from './slice'; +import { + getSurveyStatus, + shouldShowSurveySettings, + hasSavedCoordinateForPreset, + getCurrentRTKPresetId, +} from './selectors'; +import { + clearAllSavedCoordinates, + toggleSurveySettingsPanel, + showCoordinateRestorationDialog, +} from './slice'; import AntennaPositionIndicator from './AntennaPositionIndicator'; import RTKSatelliteObservations from './RTKSatelliteObservations'; @@ -53,7 +64,11 @@ const useStyles = makeStyles( const RTKSetupDialogBottomPanel = ({ chartHeight, + currentPresetId, + hasSavedCoordinates, inset, + onClearAllSavedCoordinates, + onShowSavedCoordinates, onToggleSurveySettings, surveySettingsVisible, surveyStatus, @@ -66,6 +81,12 @@ const RTKSetupDialogBottomPanel = ({ } }, [onToggleSurveySettings, surveySettingsVisible, surveyStatus?.supported]); + const handleShowSavedCoordinates = () => { + if (currentPresetId) { + onShowSavedCoordinates(currentPresetId); + } + }; + return ( + {onShowSavedCoordinates && ( + + + + + + + + )} + {onClearAllSavedCoordinates && ( + + + + + + )} @@ -114,7 +157,11 @@ const RTKSetupDialogBottomPanel = ({ RTKSetupDialogBottomPanel.propTypes = { chartHeight: PropTypes.number, + currentPresetId: PropTypes.string, + hasSavedCoordinates: PropTypes.bool, inset: PropTypes.bool, + onClearAllSavedCoordinates: PropTypes.func, + onShowSavedCoordinates: PropTypes.func, onToggleSurveySettings: PropTypes.func, surveySettingsVisible: PropTypes.bool, surveyStatus: PropTypes.object, @@ -126,12 +173,21 @@ RTKSetupDialogBottomPanel.defaultProps = { export default connect( // mapStateToProps - (state) => ({ - surveyStatus: getSurveyStatus(state), - surveySettingsVisible: shouldShowSurveySettings(state), - }), + (state) => { + const currentPresetId = getCurrentRTKPresetId(state); + return { + currentPresetId, + hasSavedCoordinates: currentPresetId + ? hasSavedCoordinateForPreset(state, currentPresetId) + : false, + surveyStatus: getSurveyStatus(state), + surveySettingsVisible: shouldShowSurveySettings(state), + }; + }, // mapDispatchToProps { + onClearAllSavedCoordinates: clearAllSavedCoordinates, + onShowSavedCoordinates: showCoordinateRestorationDialog, onToggleSurveySettings: toggleSurveySettingsPanel, } )(RTKSetupDialogBottomPanel); diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index 552ee787c..9a36b0768 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -103,6 +103,21 @@ export const saveCurrentCoordinateForPreset = savedAt: Date.now(), }; + // Check if this exact coordinate is already the latest saved one + const savedCoordinates = state.rtk.savedCoordinates[presetId] || []; + if (savedCoordinates.length > 0) { + const latest = savedCoordinates[0]; + if ( + latest && + latest.positionECEF[0] === savedCoordinate.positionECEF[0] && + latest.positionECEF[1] === savedCoordinate.positionECEF[1] && + latest.positionECEF[2] === savedCoordinate.positionECEF[2] + ) { + // Coordinate is already saved as the latest, do nothing + return; + } + } + dispatch( saveCoordinateForPreset({ presetId, coordinate: savedCoordinate }) ); diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index 8d5d44fd9..1b90cd98d 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -166,34 +166,36 @@ export const shouldShowSurveySettings = (state: RootState): boolean => export const hasSavedCoordinateForPreset = ( state: RootState, presetId: string -): boolean => Boolean(state.rtk.savedCoordinates[presetId]); +): boolean => { + const coords = state.rtk.savedCoordinates[presetId]; + return Boolean(coords && coords.length > 0); +}; /** - * Returns the saved coordinate for the given RTK preset ID, or undefined if none exists. + * Returns the saved coordinates for the given RTK preset ID, or empty array if none exists. */ -export const getSavedCoordinateForPreset = ( +export const getSavedCoordinatesForPreset = ( state: RootState, presetId: string -): RTKSavedCoordinate | undefined => state.rtk.savedCoordinates[presetId]; +): RTKSavedCoordinate[] => state.rtk.savedCoordinates[presetId] || []; /** - * Returns all saved coordinates as an array of { presetId, coordinate } objects. + * Returns all saved coordinates as an array of { presetId, coordinates } objects. */ export const getAllSavedCoordinates = createSelector( (state: RootState) => state.rtk.savedCoordinates, (savedCoordinates) => - Object.entries(savedCoordinates).map(([presetId, coordinate]) => ({ + Object.entries(savedCoordinates).map(([presetId, coordinates]) => ({ presetId, - coordinate, + coordinates, })) ); /** - * Returns the formatted saved coordinate position for a given preset ID. + * Returns the formatted saved coordinate position for a given coordinate. */ -export const getFormattedSavedCoordinatePosition = createSelector( - (state: RootState, presetId: string) => - getSavedCoordinateForPreset(state, presetId), +export const getFormattedCoordinatePosition = createSelector( + (_state: RootState, coordinate: RTKSavedCoordinate) => coordinate, getPreferredCoordinateFormatter, isShowingAntennaPositionInECEF, (savedCoordinate, formatter, isECEF) => { @@ -215,6 +217,13 @@ export const getFormattedSavedCoordinatePosition = createSelector( } ); +/** + * Returns the current RTK preset ID. + */ +export const getCurrentRTKPresetId = (state: RootState): string | undefined => + state.rtk.currentPresetId; + + /** * Returns the coordinate restoration dialog state. */ diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index f8698428a..738a2530a 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -18,7 +18,9 @@ type RTKSliceState = { stats: RTKStatistics; /** Saved coordinates per RTK preset ID */ - savedCoordinates: Record; + savedCoordinates: Record; + + currentPresetId: string | undefined; dialog: { open: boolean; @@ -28,7 +30,6 @@ type RTKSliceState = { coordinateRestorationDialog: { open: boolean; presetId: string | undefined; - savedCoordinate: RTKSavedCoordinate | undefined; }; }; }; @@ -54,20 +55,22 @@ const initialState: RTKSliceState = { savedCoordinates: { // TODO: Fake data for testing - remove in production - // '-dev-cu.usbmodem101-0': { + // '-dev-cu.usbmodem101-0': [{ // position: [19.0402, 47.4979] as any, // Budapest coordinates // positionECEF: [4080855000, 1408354000, 4679340000] as [number, number, number], // Approximate ECEF for Budapest // accuracy: 0.02, // savedAt: Date.now() - 86400000, // 1 day ago - // }, - // '-dev-cu.usbmodem101-1': { + // }], + // '-dev-cu.usbmodem101-1': [{ // position: [21.6254, 47.5289] as any, // Debrecen coordinates // positionECEF: [4010557000, 1590103000, 4681871000] as [number, number, number], // Approximate ECEF for Debrecen // accuracy: 0.015, // savedAt: Date.now() - 172800000, // 2 days ago - // }, + // }], }, + currentPresetId: undefined, + dialog: { open: false, antennaPositionFormat: RTKAntennaPositionFormat.LON_LAT, @@ -75,7 +78,6 @@ const initialState: RTKSliceState = { coordinateRestorationDialog: { open: false, presetId: undefined, - savedCoordinate: undefined, }, }, }; @@ -151,7 +153,46 @@ const { actions, reducer } = createSlice({ }> ) { const { presetId, coordinate } = action.payload; - state.savedCoordinates[presetId] = coordinate; + + if (!state.savedCoordinates[presetId]) { + state.savedCoordinates[presetId] = []; + } + + const existing = state.savedCoordinates[presetId]; + + // Check if the latest coordinate is the same as the one we are trying to save + if (existing.length > 0) { + const latest = existing[0]; + if ( + latest && + latest.positionECEF[0] === coordinate.positionECEF[0] && + latest.positionECEF[1] === coordinate.positionECEF[1] && + latest.positionECEF[2] === coordinate.positionECEF[2] + ) { + return; + } + } + + const duplicateIndex = existing.findIndex( + (c) => + c.positionECEF[0] === coordinate.positionECEF[0] && + c.positionECEF[1] === coordinate.positionECEF[1] && + c.positionECEF[2] === coordinate.positionECEF[2] + ); + + if (duplicateIndex !== -1) { + existing.splice(duplicateIndex, 1); + } + + existing.unshift(coordinate); + + if (existing.length > 5) { + existing.pop(); + } + }, + + setCurrentRTKPresetId(state, action: PayloadAction) { + state.currentPresetId = action.payload; }, removeSavedCoordinateForPreset(state, action: PayloadAction) { @@ -159,19 +200,16 @@ const { actions, reducer } = createSlice({ delete state.savedCoordinates[presetId]; }, + clearAllSavedCoordinates(state) { + state.savedCoordinates = {}; + }, + // Coordinate restoration dialog management - showCoordinateRestorationDialog( - state, - action: PayloadAction<{ - presetId: string; - savedCoordinate: RTKSavedCoordinate; - }> - ) { - const { presetId, savedCoordinate } = action.payload; + showCoordinateRestorationDialog(state, action: PayloadAction) { + const presetId = action.payload; state.dialog.coordinateRestorationDialog = { open: true, presetId, - savedCoordinate, }; }, @@ -179,7 +217,6 @@ const { actions, reducer } = createSlice({ state.dialog.coordinateRestorationDialog = { open: false, presetId: undefined, - savedCoordinate: undefined, }; }), }, @@ -189,10 +226,12 @@ export const { closeRTKSetupDialog, closeSurveySettingsPanel, closeCoordinateRestorationDialog, + clearAllSavedCoordinates, removeSavedCoordinateForPreset, resetRTKStatistics, saveCoordinateForPreset, setAntennaPositionFormat, + setCurrentRTKPresetId, showRTKSetupDialog, showCoordinateRestorationDialog, toggleSurveySettingsPanel, diff --git a/src/flockwave/queries.js b/src/flockwave/queries.js index 635603c51..b3677d787 100644 --- a/src/flockwave/queries.js +++ b/src/flockwave/queries.js @@ -343,7 +343,12 @@ export async function getRTKStatus(hub) { * Returns the currently selected RTK data source ID. */ export async function getSelectedRTKPresetId(hub) { - const response = await hub.sendMessage({ type: 'X-RTK-SOURCE' }); + const response = await hub.sendMessage( + { type: 'X-RTK-SOURCE' }, + // Use a larger timeout as the server might be busy reconfiguring the RTK + // source when we ask for it. + { timeout: 15 } + ); if (response.body && response.body.type === 'X-RTK-SOURCE') { return get(response, 'body.id'); diff --git a/src/i18n/en.json b/src/i18n/en.json index 4984a0fe6..89177ae31 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -25,6 +25,7 @@ "accuracyLabel": "Accuracy", "cancel": "Cancel", "dateLabel": "Saved", + "noSavedCoordinates": "", "positionLabel": "Position", "title": "Use last known coordinate?", "useSaved": "Use coordinate" From e70b142f5fd097476047eb962a2ee54f3446ec3e Mon Sep 17 00:00:00 2001 From: zarcell Date: Wed, 3 Dec 2025 22:37:07 +0100 Subject: [PATCH 08/70] fix(rtk): remove temporary delete button --- src/features/rtk/RTKSetupDialogBottomPanel.jsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/features/rtk/RTKSetupDialogBottomPanel.jsx b/src/features/rtk/RTKSetupDialogBottomPanel.jsx index 6fc79aa5f..7cb07f513 100644 --- a/src/features/rtk/RTKSetupDialogBottomPanel.jsx +++ b/src/features/rtk/RTKSetupDialogBottomPanel.jsx @@ -6,7 +6,6 @@ import { connect } from 'react-redux'; import Box from '@material-ui/core/Box'; import IconButton from '@material-ui/core/IconButton'; import { makeStyles } from '@material-ui/core/styles'; -import Delete from '@material-ui/icons/Delete'; import Place from '@material-ui/icons/Place'; import Restore from '@material-ui/icons/Restore'; @@ -25,7 +24,6 @@ import { getCurrentRTKPresetId, } from './selectors'; import { - clearAllSavedCoordinates, toggleSurveySettingsPanel, showCoordinateRestorationDialog, } from './slice'; @@ -67,7 +65,6 @@ const RTKSetupDialogBottomPanel = ({ currentPresetId, hasSavedCoordinates, inset, - onClearAllSavedCoordinates, onShowSavedCoordinates, onToggleSurveySettings, surveySettingsVisible, @@ -128,16 +125,6 @@ const RTKSetupDialogBottomPanel = ({ )} - {onClearAllSavedCoordinates && ( - - - - - - )} @@ -160,7 +147,6 @@ RTKSetupDialogBottomPanel.propTypes = { currentPresetId: PropTypes.string, hasSavedCoordinates: PropTypes.bool, inset: PropTypes.bool, - onClearAllSavedCoordinates: PropTypes.func, onShowSavedCoordinates: PropTypes.func, onToggleSurveySettings: PropTypes.func, surveySettingsVisible: PropTypes.bool, @@ -186,7 +172,6 @@ export default connect( }, // mapDispatchToProps { - onClearAllSavedCoordinates: clearAllSavedCoordinates, onShowSavedCoordinates: showCoordinateRestorationDialog, onToggleSurveySettings: toggleSurveySettingsPanel, } From a84a03456476f885b2a62c090833221f06a5940b Mon Sep 17 00:00:00 2001 From: zarcell Date: Thu, 4 Dec 2025 10:42:43 +0100 Subject: [PATCH 09/70] fix(rtk): xo complaints --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 2 +- src/features/rtk/RTKCorrectionSourceSelector.jsx | 7 ++++++- src/features/rtk/selectors.ts | 3 +-- src/features/rtk/slice.ts | 4 +--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index ecc219bb3..f71af8ea6 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -75,7 +75,7 @@ const RTKCoordinateRestorationDialog = ({ ) : ( - {savedCoordinates.map((coordinate, index) => ( + {savedCoordinates.map((coordinate) => ( state.rtk.savedCoordinates[presetId] || []; +): RTKSavedCoordinate[] => state.rtk.savedCoordinates[presetId] ?? []; /** * Returns all saved coordinates as an array of { presetId, coordinates } objects. @@ -223,7 +223,6 @@ export const getFormattedCoordinatePosition = createSelector( export const getCurrentRTKPresetId = (state: RootState): string | undefined => state.rtk.currentPresetId; - /** * Returns the coordinate restoration dialog state. */ diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index 738a2530a..fd77c7dd9 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -154,9 +154,7 @@ const { actions, reducer } = createSlice({ ) { const { presetId, coordinate } = action.payload; - if (!state.savedCoordinates[presetId]) { - state.savedCoordinates[presetId] = []; - } + state.savedCoordinates[presetId] ||= []; const existing = state.savedCoordinates[presetId]; From 3080a5e21ac640d994ccad6a5ee36bc06e57f0ee Mon Sep 17 00:00:00 2001 From: zarcell Date: Thu, 4 Dec 2025 12:24:42 +0100 Subject: [PATCH 10/70] chore(rtk): migrate RTK dialog to MUI v5 and add types --- .../rtk/RTKCoordinateRestorationDialog.jsx | 31 ++++++++++--------- src/flockwave/operations.ts | 5 ++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index f71af8ea6..71215cee9 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -4,15 +4,16 @@ import React from 'react'; import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; -import Box from '@material-ui/core/Box'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; import { closeCoordinateRestorationDialog } from '~/features/rtk/slice'; import { @@ -29,11 +30,13 @@ const SavedCoordinateItem = connect((state, { coordinate }) => ({ const savedDateTime = formatDate(new Date(savedAt), 'yyyy-MM-dd HH:mm:ss'); return ( - onClick(coordinate)}> - + + onClick(coordinate)}> + + ); }); diff --git a/src/flockwave/operations.ts b/src/flockwave/operations.ts index 411ee2234..31b6c540a 100644 --- a/src/flockwave/operations.ts +++ b/src/flockwave/operations.ts @@ -229,7 +229,10 @@ export async function startRTKSurvey( * Sets the RTK antenna position on the server by submitting explicit * survey settings that contain a fixed position instead of starting a survey. */ -export async function setRTKAntennaPosition(hub, { position, accuracy }) { +export async function setRTKAntennaPosition( + hub: MessageHub, + { position, accuracy }: { position: [number, number, number]; accuracy: number } +) { const response = await hub.sendMessage({ type: 'X-RTK-SURVEY', settings: { From 7acf399b005886debc6e2210dcbb60344be34443 Mon Sep 17 00:00:00 2001 From: zarcell Date: Thu, 4 Dec 2025 12:37:32 +0100 Subject: [PATCH 11/70] fix(rtk): improve saved coordinate dialog appearance and title --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 2 +- src/i18n/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 71215cee9..a5c48af94 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -71,7 +71,7 @@ const RTKCoordinateRestorationDialog = ({ {t('RTKCoordinateRestorationDialog.title')} - + {savedCoordinates.length === 0 ? ( {t('RTKCoordinateRestorationDialog.noSavedCoordinates')} diff --git a/src/i18n/en.json b/src/i18n/en.json index 68928a3f9..6955964d4 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -27,7 +27,7 @@ "dateLabel": "Saved", "noSavedCoordinates": "", "positionLabel": "Position", - "title": "Use last known coordinate?", + "title": "Choose a saved coordinate", "useSaved": "Use coordinate" }, "RTKCorrectionSourceSelector": { From 840e3c580122e157bbb06239209b181c03e38a6b Mon Sep 17 00:00:00 2001 From: zarcell Date: Thu, 4 Dec 2025 12:57:18 +0100 Subject: [PATCH 12/70] feat(rtk): move coordinate restoration button to antenna position indicator --- src/features/rtk/AntennaPositionIndicator.jsx | 24 +++++++++++++++++-- .../rtk/RTKSetupDialogBottomPanel.jsx | 20 +++++----------- src/i18n/en.json | 3 ++- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/features/rtk/AntennaPositionIndicator.jsx b/src/features/rtk/AntennaPositionIndicator.jsx index c95fe94e5..08deca0dd 100644 --- a/src/features/rtk/AntennaPositionIndicator.jsx +++ b/src/features/rtk/AntennaPositionIndicator.jsx @@ -1,3 +1,5 @@ +import Restore from '@mui/icons-material/Restore'; +import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import PropTypes from 'prop-types'; @@ -16,15 +18,17 @@ import { import { getAntennaInfoSummary } from './selectors'; const AntennaPositionIndicator = ({ + hasSavedCoordinates, onCopyAntennaPositionToClipboard, onClick, + onShowSavedCoordinates, position, t, }) => { const hasAntennaPosition = Boolean(position); return ( - <> + )} - + {onShowSavedCoordinates && ( + + + + + + )} + ); }; AntennaPositionIndicator.propTypes = { // description: PropTypes.string, + hasSavedCoordinates: PropTypes.bool, position: PropTypes.string, onClick: PropTypes.func, onCopyAntennaPositionToClipboard: PropTypes.func, + onShowSavedCoordinates: PropTypes.func, t: PropTypes.func, }; diff --git a/src/features/rtk/RTKSetupDialogBottomPanel.jsx b/src/features/rtk/RTKSetupDialogBottomPanel.jsx index 64e34d26b..83e26a1ad 100644 --- a/src/features/rtk/RTKSetupDialogBottomPanel.jsx +++ b/src/features/rtk/RTKSetupDialogBottomPanel.jsx @@ -1,5 +1,4 @@ import Place from '@mui/icons-material/Place'; -import Restore from '@mui/icons-material/Restore'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; import makeStyles from '@mui/styles/makeStyles'; @@ -113,19 +112,12 @@ const RTKSetupDialogBottomPanel = ({ )} - {onShowSavedCoordinates && ( - - - - - - - - )} - + diff --git a/src/i18n/en.json b/src/i18n/en.json index 6955964d4..57a3947e7 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -76,7 +76,8 @@ "antennaPositionIndicator": { "antennaPositionNotKnown": "Antenna position not known", "clickToToggle": "Click to toggle between lon/lat and ECEF format", - "copyToClipboard": "Copy to clipboard" + "copyToClipboard": "Copy to clipboard", + "useSavedCoordinate": "Use saved coordinate" }, "augmentMappingButton": { "assignSparesToEmptySlots": "Assign spares to empty slots" From d112af33cabf7f6db61d05cd7014ca5445464ac1 Mon Sep 17 00:00:00 2001 From: zarcell Date: Thu, 4 Dec 2025 16:47:10 +0100 Subject: [PATCH 13/70] fix(rtk): hide restore button tooltip on focus --- src/features/rtk/AntennaPositionIndicator.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/rtk/AntennaPositionIndicator.jsx b/src/features/rtk/AntennaPositionIndicator.jsx index 03c17ca9e..99a5f11ab 100644 --- a/src/features/rtk/AntennaPositionIndicator.jsx +++ b/src/features/rtk/AntennaPositionIndicator.jsx @@ -53,6 +53,7 @@ const AntennaPositionIndicator = ({ {onShowSavedCoordinates && ( Date: Thu, 4 Dec 2025 17:21:34 +0100 Subject: [PATCH 14/70] feat(rtk): show preset title in save coordinate toast --- src/features/rtk/actions.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index 9a36b0768..c6ae26229 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -82,7 +82,7 @@ export const useSavedCoordinateForPreset = }; export const saveCurrentCoordinateForPreset = - (presetId) => (dispatch, getState) => { + (presetId) => async (dispatch, getState) => { const state = getState(); const { antenna, survey } = state.rtk.stats; @@ -122,9 +122,20 @@ export const saveCurrentCoordinateForPreset = saveCoordinateForPreset({ presetId, coordinate: savedCoordinate }) ); + let presetName = presetId; + try { + const presets = await messageHub.query.getRTKPresets(); + const preset = presets.find((p) => p.id === presetId); + if (preset) { + presetName = preset.title; + } + } catch (error) { + console.warn('Failed to fetch RTK presets:', error); + } + dispatch( showNotification({ - message: `Coordinate saved for preset ${presetId}`, + message: `Coordinate saved for preset ${presetName}`, semantics: MessageSemantics.SUCCESS, }) ); From 80d9b3b4fd843cd01c88610cd75d3995c647cdc4 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 8 Dec 2025 10:16:06 +0100 Subject: [PATCH 15/70] fix(rtk): fix coordinate check logic and cleanup fake data --- src/features/rtk/RTKStatusUpdater.jsx | 6 ++++-- src/features/rtk/slice.ts | 16 +--------------- src/i18n/en.json | 2 +- src/i18n/hu.json | 1 + 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/features/rtk/RTKStatusUpdater.jsx b/src/features/rtk/RTKStatusUpdater.jsx index 8a05ae3bb..09b61dd05 100644 --- a/src/features/rtk/RTKStatusUpdater.jsx +++ b/src/features/rtk/RTKStatusUpdater.jsx @@ -58,8 +58,10 @@ const RTKStatusUpdater = ({ onStatusChanged, period = 1000 }) => { const saved = savedCoordinates && savedCoordinates[selectedPresetId]; const savedECEF = - saved && Array.isArray(saved.positionECEF) - ? saved.positionECEF.slice(0, 3) + saved && + saved.length > 0 && + Array.isArray(saved[0].positionECEF) + ? saved[0].positionECEF.slice(0, 3) : undefined; const isSameECEF = diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index fd77c7dd9..05fade5d0 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -53,21 +53,7 @@ const initialState: RTKSliceState = { }, }, - savedCoordinates: { - // TODO: Fake data for testing - remove in production - // '-dev-cu.usbmodem101-0': [{ - // position: [19.0402, 47.4979] as any, // Budapest coordinates - // positionECEF: [4080855000, 1408354000, 4679340000] as [number, number, number], // Approximate ECEF for Budapest - // accuracy: 0.02, - // savedAt: Date.now() - 86400000, // 1 day ago - // }], - // '-dev-cu.usbmodem101-1': [{ - // position: [21.6254, 47.5289] as any, // Debrecen coordinates - // positionECEF: [4010557000, 1590103000, 4681871000] as [number, number, number], // Approximate ECEF for Debrecen - // accuracy: 0.015, - // savedAt: Date.now() - 172800000, // 2 days ago - // }], - }, + savedCoordinates: {}, currentPresetId: undefined, diff --git a/src/i18n/en.json b/src/i18n/en.json index d06e5c9b7..1efeb152b 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -25,7 +25,7 @@ "accuracyLabel": "Accuracy", "cancel": "Cancel", "dateLabel": "Saved", - "noSavedCoordinates": "", + "noSavedCoordinates": "No saved coordinates found.", "positionLabel": "Position", "title": "Choose a saved coordinate", "useSaved": "Use coordinate" diff --git a/src/i18n/hu.json b/src/i18n/hu.json index 85a766276..9d9d1fd1b 100644 --- a/src/i18n/hu.json +++ b/src/i18n/hu.json @@ -32,6 +32,7 @@ "positionLabel": "Pozíció", "accuracyLabel": "Pontosság", "dateLabel": "Mentve", + "noSavedCoordinates": "Nincsenek mentett koordináták.", "cancel": "Mégse", "useSaved": "Koordináta használata" }, From fe8d3d02337e8548c699834d983da880cb3467f7 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 8 Dec 2025 10:38:21 +0100 Subject: [PATCH 16/70] fix(rtk): xo problems --- src/features/rtk/AntennaPositionIndicator.jsx | 2 +- src/features/rtk/RTKStatusUpdater.jsx | 61 ++++++------------- src/features/rtk/slice.ts | 4 +- src/features/rtk/utils.ts | 51 +++++++++++++++- 4 files changed, 71 insertions(+), 47 deletions(-) diff --git a/src/features/rtk/AntennaPositionIndicator.jsx b/src/features/rtk/AntennaPositionIndicator.jsx index 99a5f11ab..caebec586 100644 --- a/src/features/rtk/AntennaPositionIndicator.jsx +++ b/src/features/rtk/AntennaPositionIndicator.jsx @@ -58,8 +58,8 @@ const AntennaPositionIndicator = ({ diff --git a/src/features/rtk/RTKStatusUpdater.jsx b/src/features/rtk/RTKStatusUpdater.jsx index 09b61dd05..a1b77168e 100644 --- a/src/features/rtk/RTKStatusUpdater.jsx +++ b/src/features/rtk/RTKStatusUpdater.jsx @@ -8,6 +8,7 @@ import { connect, useDispatch, useSelector } from 'react-redux'; import handleError from '~/error-handling'; import { updateRTKStatistics } from '~/features/rtk/slice'; import { saveCurrentCoordinateForPreset } from '~/features/rtk/actions'; +import { hasValidFix, shouldSaveCoordinate } from '~/features/rtk/utils'; import useMessageHub from '~/hooks/useMessageHub'; /** @@ -25,6 +26,22 @@ const RTKStatusUpdater = ({ onStatusChanged, period = 1000 }) => { promise: null, }; + const checkAndAutosave = async (status) => { + // Autosave base station coordinate on first valid fix per preset + if (!hasValidFix(status)) { + return; + } + + const selectedPresetId = await messageHub.query.getSelectedRTKPresetId(); + + if ( + selectedPresetId && + shouldSaveCoordinate(status, savedCoordinates, selectedPresetId) + ) { + dispatch(saveCurrentCoordinateForPreset(selectedPresetId)); + } + }; + const updateStatus = async () => { while (!valueHolder.finished) { try { @@ -32,48 +49,8 @@ const RTKStatusUpdater = ({ onStatusChanged, period = 1000 }) => { const status = await messageHub.query.getRTKStatus(); onStatusChanged(status); - // Autosave base station coordinate on first valid fix per preset - const hasECEF = Array.isArray(status?.antenna?.positionECEF); - const accuracy = status?.survey?.accuracy; - const flags = status?.survey?.flags; - const surveyedCoordinateValid = - typeof flags === 'number' ? (flags & 0b100) !== 0 : false; - const hasValidFix = - hasECEF && - (surveyedCoordinateValid || typeof accuracy === 'number'); - - if (hasValidFix) { - const selectedPresetId = - // eslint-disable-next-line no-await-in-loop - await messageHub.query.getSelectedRTKPresetId(); - if (!selectedPresetId) { - continue; - } - - const incomingECEF = Array.isArray(status?.antenna?.positionECEF) - ? status.antenna.positionECEF - .slice(0, 3) - .map((x) => Math.round(x)) - : undefined; - const saved = - savedCoordinates && savedCoordinates[selectedPresetId]; - const savedECEF = - saved && - saved.length > 0 && - Array.isArray(saved[0].positionECEF) - ? saved[0].positionECEF.slice(0, 3) - : undefined; - - const isSameECEF = - incomingECEF && savedECEF - ? incomingECEF.length === savedECEF.length && - incomingECEF.every((v, i) => v === savedECEF[i]) - : false; - - if (!isSameECEF) { - dispatch(saveCurrentCoordinateForPreset(selectedPresetId)); - } - } + // eslint-disable-next-line no-await-in-loop + await checkAndAutosave(status); } catch (error) { handleError(error, 'RTK status query'); } diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index 05fade5d0..9228db72a 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -117,9 +117,7 @@ const { actions, reducer } = createSlice({ // gone state.stats.satellites = satellites; - if (state.stats.survey === undefined) { - state.stats.survey = {}; - } + state.stats.survey ??= {}; if (!isNil(survey.accuracy)) { state.stats.survey.accuracy = survey.accuracy; diff --git a/src/features/rtk/utils.ts b/src/features/rtk/utils.ts index 0315f75b8..b8c2d9e7a 100644 --- a/src/features/rtk/utils.ts +++ b/src/features/rtk/utils.ts @@ -1,3 +1,5 @@ +import type { RTKSavedCoordinate, RTKStatistics } from './types'; + const descriptions: Record = { 'rtcm2/1': 'Differential GPS Corrections', 'rtcm2/2': 'Delta Differential GPS Corrections', @@ -88,7 +90,7 @@ const descriptions: Record = { 'rtcm3/1121': 'BeiDou MSM1 (DGNSS pseudorange)', 'rtcm3/1122': 'BeiDou MSM2 (RTK pseudorange)', 'rtcm3/1123': 'BeiDou MSM3 (code, carrier)', - 'rtcm3/1124': 'BeiDou MSM4 (code, carrier, CNR)', + 'rtcm3/1124': 'BeiDou MSM3 (code, carrier, CNR)', 'rtcm3/1125': 'BeiDou MSM5 (code, carrier, doppler, CNR)', 'rtcm3/1126': 'BeiDou MSM6 (hi-res code, carrier, doppler)', 'rtcm3/1127': 'BeiDou MSM7 (hi-res code, carrier, doppler, CNR)', @@ -118,3 +120,50 @@ export function formatSurveyAccuracy( ? (value * 100).toFixed(short ? 0 : 1) + 'cm' : (value * 100).toFixed(1) + 'cm'; } + +/** + * Checks if the RTK status indicates a valid fix. + * + * @param status - The RTK statistics object. + * @returns True if a valid fix is present, false otherwise. + */ +export function hasValidFix(status: Partial): boolean { + const hasECEF = Array.isArray(status?.antenna?.positionECEF); + const accuracy = status?.survey?.accuracy; + const flags = status?.survey?.flags; + const surveyedCoordinateValid = + typeof flags === 'number' ? (flags & 0b100) !== 0 : false; + + return hasECEF && (surveyedCoordinateValid || typeof accuracy === 'number'); +} + +/** + * Determines whether the current coordinate should be saved for a given preset. + * + * @param status - The RTK statistics object containing the current antenna position. + * @param savedCoordinates - The record of saved coordinates keyed by preset ID. + * @param presetId - The ID of the preset to check against. + * @returns True if the coordinate is new and should be saved, false otherwise. + */ +export function shouldSaveCoordinate( + status: Partial, + savedCoordinates: Record | undefined, + presetId: string +): boolean { + const incomingECEF = Array.isArray(status?.antenna?.positionECEF) + ? status.antenna?.positionECEF?.slice(0, 3).map((x) => Math.round(x)) + : undefined; + const saved = savedCoordinates?.[presetId]; + const savedECEF = + saved && saved.length > 0 && Array.isArray(saved[0]?.positionECEF) + ? saved[0]?.positionECEF.slice(0, 3) + : undefined; + + const isSameECEF = + incomingECEF && savedECEF + ? incomingECEF.length === savedECEF.length && + incomingECEF.every((v, i) => v === savedECEF[i]) + : false; + + return !isSameECEF; +} From 0bb9b2639e8da5ab2024bc7d263c90aaa866c949 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 09:56:35 +0100 Subject: [PATCH 17/70] fix: accidental change --- src/features/rtk/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/utils.ts b/src/features/rtk/utils.ts index b8c2d9e7a..615ee10d2 100644 --- a/src/features/rtk/utils.ts +++ b/src/features/rtk/utils.ts @@ -90,7 +90,7 @@ const descriptions: Record = { 'rtcm3/1121': 'BeiDou MSM1 (DGNSS pseudorange)', 'rtcm3/1122': 'BeiDou MSM2 (RTK pseudorange)', 'rtcm3/1123': 'BeiDou MSM3 (code, carrier)', - 'rtcm3/1124': 'BeiDou MSM3 (code, carrier, CNR)', + 'rtcm3/1124': 'BeiDou MSM4 (code, carrier, CNR)', 'rtcm3/1125': 'BeiDou MSM5 (code, carrier, doppler, CNR)', 'rtcm3/1126': 'BeiDou MSM6 (hi-res code, carrier, doppler)', 'rtcm3/1127': 'BeiDou MSM7 (hi-res code, carrier, doppler, CNR)', From a0b953f2e23a2adf886bc9f3aca6e6b6273a16fe Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 11:04:21 +0100 Subject: [PATCH 18/70] fix: cleaner placement instead of hacky margin --- src/features/rtk/AntennaPositionIndicator.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/rtk/AntennaPositionIndicator.jsx b/src/features/rtk/AntennaPositionIndicator.jsx index caebec586..1397352ae 100644 --- a/src/features/rtk/AntennaPositionIndicator.jsx +++ b/src/features/rtk/AntennaPositionIndicator.jsx @@ -27,7 +27,7 @@ const AntennaPositionIndicator = ({ const hasAntennaPosition = Boolean(position); return ( - + From 8f16dc0ec32b4cdb487a8cb90ef864272e5cb355 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 11:23:48 +0100 Subject: [PATCH 19/70] fix: make SavedCoordinateItem a presentational component --- .../rtk/RTKCoordinateRestorationDialog.jsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index a5c48af94..848b476c5 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -23,12 +23,12 @@ import { } from '~/features/rtk/selectors'; import { useSavedCoordinateForPreset } from '~/features/rtk/actions'; -const SavedCoordinateItem = connect((state, { coordinate }) => ({ - formattedPosition: getFormattedCoordinatePosition(state, coordinate), -}))(({ coordinate, formattedPosition, onClick }) => { +const SavedCoordinateItem = ({ coordinate, coordinateFormatter, onClick }) => { const { accuracy, savedAt } = coordinate; const savedDateTime = formatDate(new Date(savedAt), 'yyyy-MM-dd HH:mm:ss'); + const formattedPosition = coordinateFormatter(coordinate); + return ( onClick(coordinate)}> @@ -39,12 +39,13 @@ const SavedCoordinateItem = connect((state, { coordinate }) => ({ ); -}); +}; const RTKCoordinateRestorationDialog = ({ t, dialog, savedCoordinates, + coordinateFormatter, onClose, onUseSaved, }) => { @@ -82,6 +83,7 @@ const RTKCoordinateRestorationDialog = ({ ))} @@ -102,6 +104,7 @@ RTKCoordinateRestorationDialog.propTypes = { t: PropTypes.func, dialog: PropTypes.object, savedCoordinates: PropTypes.array, + coordinateFormatter: PropTypes.func, onClose: PropTypes.func, onUseSaved: PropTypes.func, }; @@ -109,11 +112,16 @@ RTKCoordinateRestorationDialog.propTypes = { export default connect( (state) => { const dialog = getCoordinateRestorationDialog(state); + + const coordinateFormatter = (coordinate) => + getFormattedCoordinatePosition(state, coordinate); + return { dialog, savedCoordinates: dialog.presetId ? getSavedCoordinatesForPreset(state, dialog.presetId) : [], + coordinateFormatter, }; }, { From fef235a2befad5a9e81d522dd8c83709920141ee Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 11:36:18 +0100 Subject: [PATCH 20/70] fix: use formatDistance from utils --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 848b476c5..3ec2574da 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -22,6 +22,7 @@ import { getSavedCoordinatesForPreset, } from '~/features/rtk/selectors'; import { useSavedCoordinateForPreset } from '~/features/rtk/actions'; +import { formatDistance } from '~/utils/formatting'; const SavedCoordinateItem = ({ coordinate, coordinateFormatter, onClick }) => { const { accuracy, savedAt } = coordinate; @@ -34,7 +35,7 @@ const SavedCoordinateItem = ({ coordinate, coordinateFormatter, onClick }) => { onClick(coordinate)}> From 60641a695b557398957a48b6613f27693b36ebad Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 11:57:37 +0100 Subject: [PATCH 21/70] fix: sort alphabetically --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 3ec2574da..d67aae29f 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -43,12 +43,12 @@ const SavedCoordinateItem = ({ coordinate, coordinateFormatter, onClick }) => { }; const RTKCoordinateRestorationDialog = ({ - t, - dialog, - savedCoordinates, coordinateFormatter, + dialog, onClose, onUseSaved, + savedCoordinates, + t, }) => { if (!dialog.open) { return null; @@ -102,12 +102,12 @@ const RTKCoordinateRestorationDialog = ({ }; RTKCoordinateRestorationDialog.propTypes = { - t: PropTypes.func, - dialog: PropTypes.object, - savedCoordinates: PropTypes.array, coordinateFormatter: PropTypes.func, + dialog: PropTypes.object, onClose: PropTypes.func, onUseSaved: PropTypes.func, + savedCoordinates: PropTypes.array, + t: PropTypes.func, }; export default connect( From 868ad9fc21a5898ab5f2d7e6abfff24a49e9d320 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 12:15:26 +0100 Subject: [PATCH 22/70] fix: removed the redundant early return check --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index d67aae29f..cc796b7c1 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -50,10 +50,6 @@ const RTKCoordinateRestorationDialog = ({ savedCoordinates, t, }) => { - if (!dialog.open) { - return null; - } - const { presetId } = dialog; const handleUseSaved = (coordinate) => { From 9b3b351ede3543f9de56eaf1e7d375b19dd1f553 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 12:50:26 +0100 Subject: [PATCH 23/70] fix: use BackgroundHint --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index cc796b7c1..439fd8f94 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -15,6 +15,8 @@ import ListItem from '@mui/material/ListItem'; import ListItemButton from '@mui/material/ListItemButton'; import ListItemText from '@mui/material/ListItemText'; +import { BackgroundHint } from '@skybrush/mui-components'; + import { closeCoordinateRestorationDialog } from '~/features/rtk/slice'; import { getCoordinateRestorationDialog, @@ -71,9 +73,7 @@ const RTKCoordinateRestorationDialog = ({ {savedCoordinates.length === 0 ? ( - - {t('RTKCoordinateRestorationDialog.noSavedCoordinates')} - + ) : ( {savedCoordinates.map((coordinate) => ( From b9724dd0e96f33aa13bd3408d8d7d3d0f4ceb5b2 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 13:45:40 +0100 Subject: [PATCH 24/70] add: comment to explain why we dont need focus there --- src/features/rtk/AntennaPositionIndicator.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/features/rtk/AntennaPositionIndicator.jsx b/src/features/rtk/AntennaPositionIndicator.jsx index 1397352ae..8b515b323 100644 --- a/src/features/rtk/AntennaPositionIndicator.jsx +++ b/src/features/rtk/AntennaPositionIndicator.jsx @@ -53,6 +53,8 @@ const AntennaPositionIndicator = ({ {onShowSavedCoordinates && ( Date: Mon, 19 Jan 2026 13:49:44 +0100 Subject: [PATCH 25/70] add: void operator to explicitly ignore promise in handleUseSaved --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 439fd8f94..4c688d45d 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -56,7 +56,7 @@ const RTKCoordinateRestorationDialog = ({ const handleUseSaved = (coordinate) => { onClose(); // Close dialog first - onUseSaved(presetId, coordinate); // Then start async operation + void onUseSaved(presetId, coordinate); // Then start async operation }; return ( From 8807df298d15cdf7abef6bfafa77d6953a48eac2 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 13:54:38 +0100 Subject: [PATCH 26/70] fix: use general.action.cancel translation key --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 2 +- src/i18n/en.json | 1 - src/i18n/hu.json | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 4c688d45d..796c7c18f 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -90,7 +90,7 @@ const RTKCoordinateRestorationDialog = ({ diff --git a/src/i18n/en.json b/src/i18n/en.json index 1efeb152b..1c23ff023 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -23,7 +23,6 @@ }, "RTKCoordinateRestorationDialog": { "accuracyLabel": "Accuracy", - "cancel": "Cancel", "dateLabel": "Saved", "noSavedCoordinates": "No saved coordinates found.", "positionLabel": "Position", diff --git a/src/i18n/hu.json b/src/i18n/hu.json index 9d9d1fd1b..85d995665 100644 --- a/src/i18n/hu.json +++ b/src/i18n/hu.json @@ -33,7 +33,6 @@ "accuracyLabel": "Pontosság", "dateLabel": "Mentve", "noSavedCoordinates": "Nincsenek mentett koordináták.", - "cancel": "Mégse", "useSaved": "Koordináta használata" }, "RTKMessage": { From 98e0b88d8d5d3946aa7d63b27df25e57a7b33355 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 14:09:38 +0100 Subject: [PATCH 27/70] fix: sort --- src/features/rtk/RTKCorrectionSourceSelector.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKCorrectionSourceSelector.jsx b/src/features/rtk/RTKCorrectionSourceSelector.jsx index ba6b26335..19fb35bd1 100644 --- a/src/features/rtk/RTKCorrectionSourceSelector.jsx +++ b/src/features/rtk/RTKCorrectionSourceSelector.jsx @@ -24,8 +24,8 @@ const nullPreset = { const RTKCorrectionSourceSelector = ({ onSourceChanged, - t, setCurrentRTKPresetId, + t, }) => { const [selectedByUser, setSelectedByUser] = useState(); const [selectionState, getSelectionFromServer] = useAsyncFn(async () => From 8ea1489b5bdbc279be555cec8300ee6f6a07fb10 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 14:13:04 +0100 Subject: [PATCH 28/70] fix: prefer nullish coalescing --- src/features/rtk/RTKCorrectionSourceSelector.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKCorrectionSourceSelector.jsx b/src/features/rtk/RTKCorrectionSourceSelector.jsx index 19fb35bd1..9f926a633 100644 --- a/src/features/rtk/RTKCorrectionSourceSelector.jsx +++ b/src/features/rtk/RTKCorrectionSourceSelector.jsx @@ -44,7 +44,7 @@ const RTKCorrectionSourceSelector = ({ console.warn('Failed to load RTK selection state:', selectionState.error); } - const presets = presetsState.value || []; + const presets = presetsState.value ?? []; const hasSelectionFromServer = selectionState.value !== undefined; const selectedOnServer = selectionState.value === undefined From 5efe0ae85b70181be893900081ce92828a55e5c4 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 14:16:20 +0100 Subject: [PATCH 29/70] refactor: keep the original logic --- src/features/rtk/RTKCorrectionSourceSelector.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/features/rtk/RTKCorrectionSourceSelector.jsx b/src/features/rtk/RTKCorrectionSourceSelector.jsx index 9f926a633..ebff5d724 100644 --- a/src/features/rtk/RTKCorrectionSourceSelector.jsx +++ b/src/features/rtk/RTKCorrectionSourceSelector.jsx @@ -47,9 +47,11 @@ const RTKCorrectionSourceSelector = ({ const presets = presetsState.value ?? []; const hasSelectionFromServer = selectionState.value !== undefined; const selectedOnServer = - selectionState.value === undefined - ? undefined - : (selectionState.value ?? NULL_ID); + selectionState.value !== undefined + ? selectionState.value === null + ? NULL_ID + : selectionState.value + : undefined; const hasPresets = presets && presets.length > 0; const handleChange = async (event) => { From 8335d865fef17c80088fc95cfd7e324ed7e3e93d Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 14:22:55 +0100 Subject: [PATCH 30/70] fix: sort --- src/features/rtk/RTKCorrectionSourceSelector.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKCorrectionSourceSelector.jsx b/src/features/rtk/RTKCorrectionSourceSelector.jsx index ebff5d724..5144156ad 100644 --- a/src/features/rtk/RTKCorrectionSourceSelector.jsx +++ b/src/features/rtk/RTKCorrectionSourceSelector.jsx @@ -76,9 +76,9 @@ const RTKCorrectionSourceSelector = ({ const effectiveId = currentId === NULL_ID ? undefined : currentId; setCurrentRTKPresetId(effectiveId); }, [ + hasSelectionFromServer, selectedByUser, selectedOnServer, - hasSelectionFromServer, setCurrentRTKPresetId, ]); From 627058c56370d37f68dd92c3565a93f56db15870 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 14:23:47 +0100 Subject: [PATCH 31/70] fix: sort --- src/features/rtk/RTKCorrectionSourceSelector.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKCorrectionSourceSelector.jsx b/src/features/rtk/RTKCorrectionSourceSelector.jsx index 5144156ad..e004210e5 100644 --- a/src/features/rtk/RTKCorrectionSourceSelector.jsx +++ b/src/features/rtk/RTKCorrectionSourceSelector.jsx @@ -171,8 +171,8 @@ const RTKCorrectionSourceSelector = ({ RTKCorrectionSourceSelector.propTypes = { onSourceChanged: PropTypes.func, - t: PropTypes.func, setCurrentRTKPresetId: PropTypes.func, + t: PropTypes.func, }; export default connect( From aa7867bc59fe6ad6c21f8824e0df7dd6c5226bb3 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 14:50:27 +0100 Subject: [PATCH 32/70] fix: sorted --- src/features/rtk/RTKSetupDialogBottomPanel.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/rtk/RTKSetupDialogBottomPanel.jsx b/src/features/rtk/RTKSetupDialogBottomPanel.jsx index 14f4e2c6f..329f06790 100644 --- a/src/features/rtk/RTKSetupDialogBottomPanel.jsx +++ b/src/features/rtk/RTKSetupDialogBottomPanel.jsx @@ -16,10 +16,10 @@ import { Tooltip } from '@skybrush/mui-components'; import FadeAndSlide from '~/components/transitions/FadeAndSlide'; import { + getCurrentRTKPresetId, getSurveyStatus, - shouldShowSurveySettings, hasSavedCoordinateForPreset, - getCurrentRTKPresetId, + shouldShowSurveySettings, } from './selectors'; import { toggleSurveySettingsPanel, From c77cc4bb3dd9b8266f02a4fe99412ba7e6248214 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 14:50:56 +0100 Subject: [PATCH 33/70] fix: sorted --- src/features/rtk/RTKSetupDialogBottomPanel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKSetupDialogBottomPanel.jsx b/src/features/rtk/RTKSetupDialogBottomPanel.jsx index 329f06790..87f342feb 100644 --- a/src/features/rtk/RTKSetupDialogBottomPanel.jsx +++ b/src/features/rtk/RTKSetupDialogBottomPanel.jsx @@ -22,8 +22,8 @@ import { shouldShowSurveySettings, } from './selectors'; import { - toggleSurveySettingsPanel, showCoordinateRestorationDialog, + toggleSurveySettingsPanel, } from './slice'; import AntennaPositionIndicator from './AntennaPositionIndicator'; From 7a7e92cd9c4574a6f2bf0f9239ff682c0454484a Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 14:53:41 +0100 Subject: [PATCH 34/70] fix: added optional chaining --- src/features/rtk/RTKSetupDialogBottomPanel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKSetupDialogBottomPanel.jsx b/src/features/rtk/RTKSetupDialogBottomPanel.jsx index 87f342feb..bcc92606b 100644 --- a/src/features/rtk/RTKSetupDialogBottomPanel.jsx +++ b/src/features/rtk/RTKSetupDialogBottomPanel.jsx @@ -77,7 +77,7 @@ const RTKSetupDialogBottomPanel = ({ const handleShowSavedCoordinates = () => { if (currentPresetId) { - onShowSavedCoordinates(currentPresetId); + onShowSavedCoordinates?.(currentPresetId); } }; From 9654fb3c2d2f84185e56cb3f91534acb5dce4cf1 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 14:55:19 +0100 Subject: [PATCH 35/70] fix: replaced the ternary with logical expression --- src/features/rtk/RTKSetupDialogBottomPanel.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/rtk/RTKSetupDialogBottomPanel.jsx b/src/features/rtk/RTKSetupDialogBottomPanel.jsx index bcc92606b..c63b19ad4 100644 --- a/src/features/rtk/RTKSetupDialogBottomPanel.jsx +++ b/src/features/rtk/RTKSetupDialogBottomPanel.jsx @@ -151,9 +151,9 @@ export default connect( const currentPresetId = getCurrentRTKPresetId(state); return { currentPresetId, - hasSavedCoordinates: currentPresetId - ? hasSavedCoordinateForPreset(state, currentPresetId) - : false, + hasSavedCoordinates: + Boolean(currentPresetId) && + hasSavedCoordinateForPreset(state, currentPresetId), surveyStatus: getSurveyStatus(state), surveySettingsVisible: shouldShowSurveySettings(state), }; From 75f852a893d8c7494096117136b056989e775656 Mon Sep 17 00:00:00 2001 From: zarcell Date: Mon, 19 Jan 2026 15:01:25 +0100 Subject: [PATCH 36/70] fix: simplified the prop assignment --- src/features/rtk/RTKSetupDialogBottomPanel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKSetupDialogBottomPanel.jsx b/src/features/rtk/RTKSetupDialogBottomPanel.jsx index c63b19ad4..aee121382 100644 --- a/src/features/rtk/RTKSetupDialogBottomPanel.jsx +++ b/src/features/rtk/RTKSetupDialogBottomPanel.jsx @@ -115,7 +115,7 @@ const RTKSetupDialogBottomPanel = ({ From 744be83de01b97462529a100950d631aaab10fed Mon Sep 17 00:00:00 2001 From: zarcell Date: Tue, 20 Jan 2026 13:16:26 +0100 Subject: [PATCH 37/70] fix: sort imports --- src/features/rtk/RTKStatusUpdater.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKStatusUpdater.jsx b/src/features/rtk/RTKStatusUpdater.jsx index a1b77168e..25a001955 100644 --- a/src/features/rtk/RTKStatusUpdater.jsx +++ b/src/features/rtk/RTKStatusUpdater.jsx @@ -6,8 +6,8 @@ import { useEffect } from 'react'; import { connect, useDispatch, useSelector } from 'react-redux'; import handleError from '~/error-handling'; -import { updateRTKStatistics } from '~/features/rtk/slice'; import { saveCurrentCoordinateForPreset } from '~/features/rtk/actions'; +import { updateRTKStatistics } from '~/features/rtk/slice'; import { hasValidFix, shouldSaveCoordinate } from '~/features/rtk/utils'; import useMessageHub from '~/hooks/useMessageHub'; From 4621a02254cfa878d2fc3e2bead7676f479528e6 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 09:57:41 +0100 Subject: [PATCH 38/70] fix: use selector --- src/features/rtk/RTKStatusUpdater.jsx | 3 ++- src/features/rtk/selectors.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/features/rtk/RTKStatusUpdater.jsx b/src/features/rtk/RTKStatusUpdater.jsx index 25a001955..b6a165a8d 100644 --- a/src/features/rtk/RTKStatusUpdater.jsx +++ b/src/features/rtk/RTKStatusUpdater.jsx @@ -7,6 +7,7 @@ import { connect, useDispatch, useSelector } from 'react-redux'; import handleError from '~/error-handling'; import { saveCurrentCoordinateForPreset } from '~/features/rtk/actions'; +import { getSavedCoordinates } from '~/features/rtk/selectors'; import { updateRTKStatistics } from '~/features/rtk/slice'; import { hasValidFix, shouldSaveCoordinate } from '~/features/rtk/utils'; import useMessageHub from '~/hooks/useMessageHub'; @@ -18,7 +19,7 @@ import useMessageHub from '~/hooks/useMessageHub'; const RTKStatusUpdater = ({ onStatusChanged, period = 1000 }) => { const messageHub = useMessageHub(); const dispatch = useDispatch(); - const savedCoordinates = useSelector((state) => state.rtk.savedCoordinates); + const savedCoordinates = useSelector(getSavedCoordinates); useEffect(() => { const valueHolder = { diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index ed7a64b5b..8c58c5094 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -179,11 +179,18 @@ export const getSavedCoordinatesForPreset = ( presetId: string ): RTKSavedCoordinate[] => state.rtk.savedCoordinates[presetId] ?? []; +/** + * Returns the full saved coordinates map keyed by preset ID. + */ +export const getSavedCoordinates = ( + state: RootState +): RootState['rtk']['savedCoordinates'] => state.rtk.savedCoordinates; + /** * Returns all saved coordinates as an array of { presetId, coordinates } objects. */ export const getAllSavedCoordinates = createSelector( - (state: RootState) => state.rtk.savedCoordinates, + getSavedCoordinates, (savedCoordinates) => Object.entries(savedCoordinates).map(([presetId, coordinates]) => ({ presetId, From a5cf8c1e9912ba4956de0bea67912b5c0d690d79 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:01:03 +0100 Subject: [PATCH 39/70] refactor: flag check in hasValidFix --- src/features/rtk/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/utils.ts b/src/features/rtk/utils.ts index 615ee10d2..8ca17fcff 100644 --- a/src/features/rtk/utils.ts +++ b/src/features/rtk/utils.ts @@ -132,7 +132,7 @@ export function hasValidFix(status: Partial): boolean { const accuracy = status?.survey?.accuracy; const flags = status?.survey?.flags; const surveyedCoordinateValid = - typeof flags === 'number' ? (flags & 0b100) !== 0 : false; + typeof flags === 'number' && (flags & 0b100) !== 0; return hasECEF && (surveyedCoordinateValid || typeof accuracy === 'number'); } From 936770d20a6fb52f489b2765221803d6a397d2bb Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:04:27 +0100 Subject: [PATCH 40/70] fix: remove undifined (never happens) --- src/features/rtk/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/utils.ts b/src/features/rtk/utils.ts index 8ca17fcff..d5803f029 100644 --- a/src/features/rtk/utils.ts +++ b/src/features/rtk/utils.ts @@ -147,7 +147,7 @@ export function hasValidFix(status: Partial): boolean { */ export function shouldSaveCoordinate( status: Partial, - savedCoordinates: Record | undefined, + savedCoordinates: Record, presetId: string ): boolean { const incomingECEF = Array.isArray(status?.antenna?.positionECEF) From aa83d151e1291bcdd0cdcadf0a5c7212e1d280e8 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:05:36 +0100 Subject: [PATCH 41/70] fix: sort --- src/features/rtk/RTKStatusUpdater.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKStatusUpdater.jsx b/src/features/rtk/RTKStatusUpdater.jsx index b6a165a8d..354f69096 100644 --- a/src/features/rtk/RTKStatusUpdater.jsx +++ b/src/features/rtk/RTKStatusUpdater.jsx @@ -67,7 +67,7 @@ const RTKStatusUpdater = ({ onStatusChanged, period = 1000 }) => { valueHolder.finished = true; valueHolder.promise = null; }; - }, [messageHub, onStatusChanged, period, dispatch, savedCoordinates]); + }, [dispatch, messageHub, onStatusChanged, period, savedCoordinates]); return null; }; From 90d393e2324133a3fcb9fff8dd5292d472ca3b0a Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:06:27 +0100 Subject: [PATCH 42/70] fix: sort --- src/features/rtk/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index c6ae26229..e3a275444 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -9,8 +9,8 @@ import { } from './selectors'; import { closeSurveySettingsPanel, - setAntennaPositionFormat, saveCoordinateForPreset, + setAntennaPositionFormat, } from './slice'; export const copyAntennaPositionToClipboard = () => (dispatch, getState) => { From 92e5588a46cc48ccb288d823c835c9ba3fac151c Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:10:58 +0100 Subject: [PATCH 43/70] refactor: show RTK preset notifications even after dialog closes --- src/features/rtk/actions.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index e3a275444..46724f441 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -43,7 +43,7 @@ export const toggleAntennaPositionFormat = () => (dispatch, getState) => { }; export const useSavedCoordinateForPreset = - (presetId, savedCoordinate) => async (dispatch, getState) => { + (presetId, savedCoordinate) => async (dispatch) => { try { // Set the saved coordinate as the current antenna position await messageHub.execute.setRTKAntennaPosition({ @@ -51,12 +51,6 @@ export const useSavedCoordinateForPreset = accuracy: savedCoordinate.accuracy, }); - // Check if the component is still mounted by checking if the dialog is still open - const currentState = getState(); - if (!currentState.rtk.dialog.coordinateRestorationDialog.open) { - return; - } - dispatch( showNotification({ message: `Using saved coordinate for preset ${presetId}`, @@ -66,12 +60,6 @@ export const useSavedCoordinateForPreset = } catch (error) { console.warn('Failed to set saved coordinate:', error); - // Check if the component is still mounted - const currentState = getState(); - if (!currentState.rtk.dialog.coordinateRestorationDialog.open) { - return; - } - dispatch( showNotification({ message: 'Failed to use saved coordinate.', From db7f4d712451981d954568d60bd5bc8705147dfa Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:13:40 +0100 Subject: [PATCH 44/70] fix: nullish coalescing --- src/features/rtk/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index 46724f441..627ffd08c 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -92,7 +92,7 @@ export const saveCurrentCoordinateForPreset = }; // Check if this exact coordinate is already the latest saved one - const savedCoordinates = state.rtk.savedCoordinates[presetId] || []; + const savedCoordinates = state.rtk.savedCoordinates[presetId] ?? []; if (savedCoordinates.length > 0) { const latest = savedCoordinates[0]; if ( From 935f4df875bd1c102d07a1fd6fea47a9d14500ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20Zahor=C3=A1n?= <43791948+zarcell@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:15:27 +0100 Subject: [PATCH 45/70] Update src/features/rtk/selectors.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: István Donkó --- src/features/rtk/selectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index 8c58c5094..a12d1f841 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -168,7 +168,7 @@ export const hasSavedCoordinateForPreset = ( presetId: string ): boolean => { const coords = state.rtk.savedCoordinates[presetId]; - return Boolean(coords && coords.length > 0); + return Boolean(coords) && coords.length > 0; }; /** From c5edddf8ce6d38a5e2dcdb4d1bd76cb6dc9e6fc1 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:18:39 +0100 Subject: [PATCH 46/70] fix: remove leftover function --- src/features/rtk/selectors.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index a12d1f841..99c78d45b 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -186,18 +186,6 @@ export const getSavedCoordinates = ( state: RootState ): RootState['rtk']['savedCoordinates'] => state.rtk.savedCoordinates; -/** - * Returns all saved coordinates as an array of { presetId, coordinates } objects. - */ -export const getAllSavedCoordinates = createSelector( - getSavedCoordinates, - (savedCoordinates) => - Object.entries(savedCoordinates).map(([presetId, coordinates]) => ({ - presetId, - coordinates, - })) -); - /** * Returns the formatted saved coordinate position for a given coordinate. */ From 622444d24a404f7518a734c7cf3a0fdc2381fbc8 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:26:36 +0100 Subject: [PATCH 47/70] refactor: rtk saved coordinate selector to return formatter --- .../rtk/RTKCoordinateRestorationDialog.jsx | 5 +-- src/features/rtk/selectors.ts | 37 ++++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 796c7c18f..047cfeb2d 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -20,7 +20,7 @@ import { BackgroundHint } from '@skybrush/mui-components'; import { closeCoordinateRestorationDialog } from '~/features/rtk/slice'; import { getCoordinateRestorationDialog, - getFormattedCoordinatePosition, + getPreferredSavedRTKPositionFormatter, getSavedCoordinatesForPreset, } from '~/features/rtk/selectors'; import { useSavedCoordinateForPreset } from '~/features/rtk/actions'; @@ -110,8 +110,7 @@ export default connect( (state) => { const dialog = getCoordinateRestorationDialog(state); - const coordinateFormatter = (coordinate) => - getFormattedCoordinatePosition(state, coordinate); + const coordinateFormatter = getPreferredSavedRTKPositionFormatter(state); return { dialog, diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index 99c78d45b..1118a0f44 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -187,29 +187,30 @@ export const getSavedCoordinates = ( ): RootState['rtk']['savedCoordinates'] => state.rtk.savedCoordinates; /** - * Returns the formatted saved coordinate position for a given coordinate. + * Returns a formatter function that formats a saved coordinate position + * according to the current RTK display settings. */ -export const getFormattedCoordinatePosition = createSelector( - (_state: RootState, coordinate: RTKSavedCoordinate) => coordinate, +export const getPreferredSavedRTKPositionFormatter = createSelector( getPreferredCoordinateFormatter, isShowingAntennaPositionInECEF, - (savedCoordinate, formatter, isECEF) => { - if (!savedCoordinate) { - return undefined; - } + (formatter, isECEF) => + (savedCoordinate?: RTKSavedCoordinate): string | undefined => { + if (!savedCoordinate) { + return undefined; + } - if (isECEF) { - const { positionECEF } = savedCoordinate; - return positionECEF && Array.isArray(positionECEF) - ? `[${(positionECEF[0] / 1e3).toFixed(3)}, ${( - positionECEF[1] / 1e3 - ).toFixed(3)}, ${(positionECEF[2] / 1e3).toFixed(3)}]` - : undefined; - } else { - const { position } = savedCoordinate; - return position ? formatter(position) : undefined; + if (isECEF) { + const { positionECEF } = savedCoordinate; + return positionECEF && Array.isArray(positionECEF) + ? `[${(positionECEF[0] / 1e3).toFixed(3)}, ${( + positionECEF[1] / 1e3 + ).toFixed(3)}, ${(positionECEF[2] / 1e3).toFixed(3)}]` + : undefined; + } else { + const { position } = savedCoordinate; + return position ? formatter(position) : undefined; + } } - } ); /** From 9e51aaa9603ebdd33e2c938a0dcf3f758d958247 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:32:47 +0100 Subject: [PATCH 48/70] add: icon to visualize clickability --- .../rtk/RTKCoordinateRestorationDialog.jsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 047cfeb2d..6bb48fc63 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -24,6 +24,7 @@ import { getSavedCoordinatesForPreset, } from '~/features/rtk/selectors'; import { useSavedCoordinateForPreset } from '~/features/rtk/actions'; +import Download from '~/icons/Download'; import { formatDistance } from '~/utils/formatting'; const SavedCoordinateItem = ({ coordinate, coordinateFormatter, onClick }) => { @@ -35,10 +36,20 @@ const SavedCoordinateItem = ({ coordinate, coordinateFormatter, onClick }) => { return ( onClick(coordinate)}> - + + + + ); From 1f58834fa7eb070b62cf4960088ea674ec28084f Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:36:55 +0100 Subject: [PATCH 49/70] refactor: ECEF coordinate formatting into shared helper --- src/features/rtk/selectors.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index 1118a0f44..ae0da1f11 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -6,6 +6,13 @@ import { getPreferredCoordinateFormatter } from '~/selectors/formatting'; import type { RootState } from '~/store/reducers'; import { RTKAntennaPositionFormat, type RTKSavedCoordinate } from './types'; +const formatPositionECEF = ( + positionECEF?: RTKSavedCoordinate['positionECEF'] +): string | undefined => + positionECEF && Array.isArray(positionECEF) + ? `[${positionECEF.map((c) => (c / 1e3).toFixed(3)).join(', ')}]` + : undefined; + /** * Returns whether the antenna position should be shown in ECEF coordinates. */ @@ -23,11 +30,7 @@ export const getFormattedAntennaPosition = createSelector( (antennaInfo, formatter, isECEF) => { if (isECEF) { const { positionECEF } = antennaInfo || {}; - return positionECEF && Array.isArray(positionECEF) - ? `[${(positionECEF[0] / 1e3).toFixed(3)}, ${( - positionECEF[1] / 1e3 - ).toFixed(3)}, ${(positionECEF[2] / 1e3).toFixed(3)}]` - : undefined; + return formatPositionECEF(positionECEF); } else { const { position } = antennaInfo || {}; return position ? formatter(position) : undefined; @@ -201,11 +204,7 @@ export const getPreferredSavedRTKPositionFormatter = createSelector( if (isECEF) { const { positionECEF } = savedCoordinate; - return positionECEF && Array.isArray(positionECEF) - ? `[${(positionECEF[0] / 1e3).toFixed(3)}, ${( - positionECEF[1] / 1e3 - ).toFixed(3)}, ${(positionECEF[2] / 1e3).toFixed(3)}]` - : undefined; + return formatPositionECEF(positionECEF); } else { const { position } = savedCoordinate; return position ? formatter(position) : undefined; From 570f2e7a0572206be61e71313bae4889ffc552ab Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:39:49 +0100 Subject: [PATCH 50/70] chore: rename function --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 4 ++-- src/features/rtk/selectors.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 6bb48fc63..ff116863f 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -19,7 +19,7 @@ import { BackgroundHint } from '@skybrush/mui-components'; import { closeCoordinateRestorationDialog } from '~/features/rtk/slice'; import { - getCoordinateRestorationDialog, + getCoordinateRestorationDialogState, getPreferredSavedRTKPositionFormatter, getSavedCoordinatesForPreset, } from '~/features/rtk/selectors'; @@ -119,7 +119,7 @@ RTKCoordinateRestorationDialog.propTypes = { export default connect( (state) => { - const dialog = getCoordinateRestorationDialog(state); + const dialog = getCoordinateRestorationDialogState(state); const coordinateFormatter = getPreferredSavedRTKPositionFormatter(state); diff --git a/src/features/rtk/selectors.ts b/src/features/rtk/selectors.ts index ae0da1f11..e49502fff 100644 --- a/src/features/rtk/selectors.ts +++ b/src/features/rtk/selectors.ts @@ -221,7 +221,7 @@ export const getCurrentRTKPresetId = (state: RootState): string | undefined => /** * Returns the coordinate restoration dialog state. */ -export const getCoordinateRestorationDialog = ( +export const getCoordinateRestorationDialogState = ( state: RootState ): RootState['rtk']['dialog']['coordinateRestorationDialog'] => state.rtk.dialog.coordinateRestorationDialog; From 206f11c659e35e73b65c1eba34bc9fe729a465cb Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:41:27 +0100 Subject: [PATCH 51/70] chore: sort --- src/features/rtk/slice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index 9228db72a..03790b916 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -10,8 +10,8 @@ import { noPayload } from '~/utils/redux'; import { RTKAntennaPositionFormat, - type RTKStatistics, type RTKSavedCoordinate, + type RTKStatistics, } from './types'; type RTKSliceState = { From 99b0d5a1d8ab620559e0b7f97d177ca1d64b2f15 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:42:31 +0100 Subject: [PATCH 52/70] fix: optional property --- src/features/rtk/slice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index 03790b916..bb23d5257 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -20,7 +20,7 @@ type RTKSliceState = { /** Saved coordinates per RTK preset ID */ savedCoordinates: Record; - currentPresetId: string | undefined; + currentPresetId?: string; dialog: { open: boolean; From 0a7f8205465a9942cd61d9fbbf04d2c5a92a27ae Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:43:14 +0100 Subject: [PATCH 53/70] fix: optional property --- src/features/rtk/slice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index bb23d5257..f4b597454 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -29,7 +29,7 @@ type RTKSliceState = { /** Dialog for asking user if they want to use saved coordinates */ coordinateRestorationDialog: { open: boolean; - presetId: string | undefined; + presetId?: string; }; }; }; From a35fbf7203a914ce54c67864a901e2eeddf6b803 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:48:19 +0100 Subject: [PATCH 54/70] fix: use nullish coalescing assignment --- src/features/rtk/slice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index f4b597454..2fae2c979 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -138,7 +138,7 @@ const { actions, reducer } = createSlice({ ) { const { presetId, coordinate } = action.payload; - state.savedCoordinates[presetId] ||= []; + state.savedCoordinates[presetId] ??= []; const existing = state.savedCoordinates[presetId]; From 1929000327ec8c8c32dde8ed15e8f65e1ff50635 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:49:56 +0100 Subject: [PATCH 55/70] refactor: use isEqual from lodash --- src/features/rtk/slice.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index 2fae2c979..96f168972 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -3,6 +3,7 @@ * selected RTK stream on the server. */ +import isEqual from 'lodash-es/isEqual'; import isNil from 'lodash-es/isNil'; import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; @@ -147,19 +148,14 @@ const { actions, reducer } = createSlice({ const latest = existing[0]; if ( latest && - latest.positionECEF[0] === coordinate.positionECEF[0] && - latest.positionECEF[1] === coordinate.positionECEF[1] && - latest.positionECEF[2] === coordinate.positionECEF[2] + isEqual(latest.positionECEF, coordinate.positionECEF) ) { return; } } const duplicateIndex = existing.findIndex( - (c) => - c.positionECEF[0] === coordinate.positionECEF[0] && - c.positionECEF[1] === coordinate.positionECEF[1] && - c.positionECEF[2] === coordinate.positionECEF[2] + (c) => isEqual(c.positionECEF, coordinate.positionECEF) ); if (duplicateIndex !== -1) { From e4cf2e02d348e6e05657212bd7dcf894ba39ace1 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:51:42 +0100 Subject: [PATCH 56/70] refactor: use isEqual here too --- src/features/rtk/actions.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index 627ffd08c..d100d3a83 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -1,4 +1,5 @@ import copy from 'copy-to-clipboard'; +import isEqual from 'lodash-es/isEqual'; import { showNotification } from '~/features/snackbar/actions'; import { MessageSemantics } from '~/features/snackbar/types'; import messageHub from '~/message-hub'; @@ -95,12 +96,7 @@ export const saveCurrentCoordinateForPreset = const savedCoordinates = state.rtk.savedCoordinates[presetId] ?? []; if (savedCoordinates.length > 0) { const latest = savedCoordinates[0]; - if ( - latest && - latest.positionECEF[0] === savedCoordinate.positionECEF[0] && - latest.positionECEF[1] === savedCoordinate.positionECEF[1] && - latest.positionECEF[2] === savedCoordinate.positionECEF[2] - ) { + if (latest && isEqual(latest.positionECEF, savedCoordinate.positionECEF)) { // Coordinate is already saved as the latest, do nothing return; } From 556dc187646709ad81087dc8d894e16abcac693d Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:53:39 +0100 Subject: [PATCH 57/70] fix: just let it be --- src/features/rtk/slice.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index 96f168972..156fa38db 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -143,17 +143,6 @@ const { actions, reducer } = createSlice({ const existing = state.savedCoordinates[presetId]; - // Check if the latest coordinate is the same as the one we are trying to save - if (existing.length > 0) { - const latest = existing[0]; - if ( - latest && - isEqual(latest.positionECEF, coordinate.positionECEF) - ) { - return; - } - } - const duplicateIndex = existing.findIndex( (c) => isEqual(c.positionECEF, coordinate.positionECEF) ); From d624797b828ab407b0c6618f1b5e6e82353cf511 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 10:58:44 +0100 Subject: [PATCH 58/70] fix: remove leftover function --- src/features/rtk/slice.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index 156fa38db..471fcbbd8 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -162,11 +162,6 @@ const { actions, reducer } = createSlice({ state.currentPresetId = action.payload; }, - removeSavedCoordinateForPreset(state, action: PayloadAction) { - const presetId = action.payload; - delete state.savedCoordinates[presetId]; - }, - clearAllSavedCoordinates(state) { state.savedCoordinates = {}; }, @@ -194,7 +189,6 @@ export const { closeSurveySettingsPanel, closeCoordinateRestorationDialog, clearAllSavedCoordinates, - removeSavedCoordinateForPreset, resetRTKStatistics, saveCoordinateForPreset, setAntennaPositionFormat, From 5969c034222035533e82e188ddc91a41b07fe36c Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 11:01:01 +0100 Subject: [PATCH 59/70] chore: sort --- src/features/rtk/slice.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index 471fcbbd8..cd9b90be9 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -185,16 +185,16 @@ const { actions, reducer } = createSlice({ }); export const { + clearAllSavedCoordinates, + closeCoordinateRestorationDialog, closeRTKSetupDialog, closeSurveySettingsPanel, - closeCoordinateRestorationDialog, - clearAllSavedCoordinates, resetRTKStatistics, saveCoordinateForPreset, setAntennaPositionFormat, setCurrentRTKPresetId, - showRTKSetupDialog, showCoordinateRestorationDialog, + showRTKSetupDialog, toggleSurveySettingsPanel, updateRTKStatistics, } = actions; From 02f85e66f3242f7995a5c7518ddd27077e88b8d0 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 11:13:48 +0100 Subject: [PATCH 60/70] refactor: change to conjunction --- src/features/rtk/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/rtk/utils.ts b/src/features/rtk/utils.ts index d5803f029..459ac3389 100644 --- a/src/features/rtk/utils.ts +++ b/src/features/rtk/utils.ts @@ -134,7 +134,8 @@ export function hasValidFix(status: Partial): boolean { const surveyedCoordinateValid = typeof flags === 'number' && (flags & 0b100) !== 0; - return hasECEF && (surveyedCoordinateValid || typeof accuracy === 'number'); + // Consider fix valid only with ECEF position, valid-coordinate flag, and numeric accuracy. + return hasECEF && surveyedCoordinateValid && typeof accuracy === 'number'; } /** From 29f847ae1d5a282789665137d9f6e3378e3064d2 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 11:15:59 +0100 Subject: [PATCH 61/70] refactor: replaced the ternary with a logical expression --- src/features/rtk/utils.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/features/rtk/utils.ts b/src/features/rtk/utils.ts index 459ac3389..a411d7e06 100644 --- a/src/features/rtk/utils.ts +++ b/src/features/rtk/utils.ts @@ -1,3 +1,5 @@ +import isEqual from 'lodash-es/isEqual'; + import type { RTKSavedCoordinate, RTKStatistics } from './types'; const descriptions: Record = { @@ -161,10 +163,7 @@ export function shouldSaveCoordinate( : undefined; const isSameECEF = - incomingECEF && savedECEF - ? incomingECEF.length === savedECEF.length && - incomingECEF.every((v, i) => v === savedECEF[i]) - : false; + !!incomingECEF && !!savedECEF && isEqual(incomingECEF, savedECEF); return !isSameECEF; } From dbc8de01be165fa65e3c26fbaf6fccff10517426 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 11:17:44 +0100 Subject: [PATCH 62/70] refactor: use Coordinate3D --- src/flockwave/operations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/flockwave/operations.ts b/src/flockwave/operations.ts index 700c01bf9..8bbc75ff1 100644 --- a/src/flockwave/operations.ts +++ b/src/flockwave/operations.ts @@ -13,6 +13,7 @@ import type { } from '@skybrush/flockwave-spec'; import { errorToString } from '~/error-handling'; +import type { Coordinate3D } from '~/utils/math'; import { createBulkParameterUploadRequest, @@ -232,7 +233,7 @@ export async function startRTKSurvey( */ export async function setRTKAntennaPosition( hub: MessageHub, - { position, accuracy }: { position: [number, number, number]; accuracy: number } + { position, accuracy }: { position: Coordinate3D; accuracy: number } ) { const response = await hub.sendMessage({ type: 'X-RTK-SURVEY', From 4deb7c92bc5aa7d587cd84389e8e8e5a94494380 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 11:20:46 +0100 Subject: [PATCH 63/70] chore: sort --- src/flockwave/operations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flockwave/operations.ts b/src/flockwave/operations.ts index 8bbc75ff1..1f3911555 100644 --- a/src/flockwave/operations.ts +++ b/src/flockwave/operations.ts @@ -399,8 +399,8 @@ const _operations = { sendDebugMessage, setParameter, setParameters, - setRTKCorrectionsSource, setRTKAntennaPosition, + setRTKCorrectionsSource, setShowConfiguration, setShowLightConfiguration, startRTKSurvey, From 4c4c2e832f7a7da202fef9765d186330c38d42c1 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 11:23:12 +0100 Subject: [PATCH 64/70] chore: longer is better than larger :) --- src/flockwave/queries.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flockwave/queries.js b/src/flockwave/queries.js index 0688eef44..7353391b1 100644 --- a/src/flockwave/queries.js +++ b/src/flockwave/queries.js @@ -345,7 +345,7 @@ export async function getRTKStatus(hub) { export async function getSelectedRTKPresetId(hub) { const response = await hub.sendMessage( { type: 'X-RTK-SOURCE' }, - // Use a larger timeout as the server might be busy reconfiguring the RTK + // Use a longer timeout as the server might be busy reconfiguring the RTK // source when we ask for it. { timeout: 15 } ); From 641b089e324b904d7962ddd89df974fc54889211 Mon Sep 17 00:00:00 2001 From: zarcell Date: Fri, 23 Jan 2026 11:26:25 +0100 Subject: [PATCH 65/70] fix: update title --- src/i18n/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/hu.json b/src/i18n/hu.json index 85d995665..2a1eeaae1 100644 --- a/src/i18n/hu.json +++ b/src/i18n/hu.json @@ -28,7 +28,7 @@ "pleaseWait": "Kérem várjon, RTK források betöltése…" }, "RTKCoordinateRestorationDialog": { - "title": "Használod a legutóbbi ismert koordinátát?", + "title": "Válassz egy mentett koordinátát", "positionLabel": "Pozíció", "accuracyLabel": "Pontosság", "dateLabel": "Mentve", From be2dd269c58b58d9c158348106c85fbb385d3531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Donk=C3=B3?= Date: Sun, 25 Jan 2026 17:38:31 +0100 Subject: [PATCH 66/70] chore: remove a11y attributes as discussed earlier --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index ff116863f..21fc56ca1 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -71,16 +71,8 @@ const RTKCoordinateRestorationDialog = ({ }; return ( - - - {t('RTKCoordinateRestorationDialog.title')} - + + {t('RTKCoordinateRestorationDialog.title')} {savedCoordinates.length === 0 ? ( From db9da5d69cff8db1db34afbd5f4c50c3936903ba Mon Sep 17 00:00:00 2001 From: zarcell Date: Sun, 25 Jan 2026 17:50:23 +0100 Subject: [PATCH 67/70] refactor: nullish coalescing operator --- src/features/rtk/RTKCorrectionSourceSelector.jsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/features/rtk/RTKCorrectionSourceSelector.jsx b/src/features/rtk/RTKCorrectionSourceSelector.jsx index e004210e5..29a78fad9 100644 --- a/src/features/rtk/RTKCorrectionSourceSelector.jsx +++ b/src/features/rtk/RTKCorrectionSourceSelector.jsx @@ -69,18 +69,12 @@ const RTKCorrectionSourceSelector = ({ // Update current preset ID in Redux useEffect(() => { - const currentId = - selectedByUser || (hasSelectionFromServer ? selectedOnServer : undefined); + const currentId = selectedByUser ?? selectedOnServer; // Convert NULL_ID to undefined or keep as is? The selector uses 'undefined' for no selection usually. // But presets have IDs. const effectiveId = currentId === NULL_ID ? undefined : currentId; setCurrentRTKPresetId(effectiveId); - }, [ - hasSelectionFromServer, - selectedByUser, - selectedOnServer, - setCurrentRTKPresetId, - ]); + }, [selectedByUser, selectedOnServer, setCurrentRTKPresetId]); // If we have the preset list, but we don't have the current selection yet, // load the current selection From 0792630e9f6921a4aae0b8459a656ff46d66a632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Donk=C3=B3?= Date: Sun, 25 Jan 2026 17:58:05 +0100 Subject: [PATCH 68/70] chore: add changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78af2a91f..e2404acfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for keeping the original light configuration when adapting the show in the show adaptation dialog. +- RTK base station coordinates can now be restored from positions stored during + earlier surveys. + ## [2.12.1] - 2025-12-15 ### Fixed From f848288f5760c30666c56c13ea9b93a914ca1004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Donk=C3=B3?= Date: Sun, 25 Jan 2026 18:29:43 +0100 Subject: [PATCH 69/70] chore: apply automatic formatting fixes --- src/features/rtk/RTKCoordinateRestorationDialog.jsx | 4 +++- src/features/rtk/actions.js | 5 ++++- src/features/rtk/slice.ts | 4 ++-- src/i18n/en.json | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/features/rtk/RTKCoordinateRestorationDialog.jsx b/src/features/rtk/RTKCoordinateRestorationDialog.jsx index 21fc56ca1..0a5f40235 100644 --- a/src/features/rtk/RTKCoordinateRestorationDialog.jsx +++ b/src/features/rtk/RTKCoordinateRestorationDialog.jsx @@ -76,7 +76,9 @@ const RTKCoordinateRestorationDialog = ({ {savedCoordinates.length === 0 ? ( - + ) : ( {savedCoordinates.map((coordinate) => ( diff --git a/src/features/rtk/actions.js b/src/features/rtk/actions.js index d100d3a83..d1a51e074 100644 --- a/src/features/rtk/actions.js +++ b/src/features/rtk/actions.js @@ -96,7 +96,10 @@ export const saveCurrentCoordinateForPreset = const savedCoordinates = state.rtk.savedCoordinates[presetId] ?? []; if (savedCoordinates.length > 0) { const latest = savedCoordinates[0]; - if (latest && isEqual(latest.positionECEF, savedCoordinate.positionECEF)) { + if ( + latest && + isEqual(latest.positionECEF, savedCoordinate.positionECEF) + ) { // Coordinate is already saved as the latest, do nothing return; } diff --git a/src/features/rtk/slice.ts b/src/features/rtk/slice.ts index cd9b90be9..6bffdbcb2 100644 --- a/src/features/rtk/slice.ts +++ b/src/features/rtk/slice.ts @@ -143,8 +143,8 @@ const { actions, reducer } = createSlice({ const existing = state.savedCoordinates[presetId]; - const duplicateIndex = existing.findIndex( - (c) => isEqual(c.positionECEF, coordinate.positionECEF) + const duplicateIndex = existing.findIndex((c) => + isEqual(c.positionECEF, coordinate.positionECEF) ); if (duplicateIndex !== -1) { diff --git a/src/i18n/en.json b/src/i18n/en.json index 1c23ff023..75b8433a1 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -853,4 +853,4 @@ "timezone": "Timezone", "utcOffset": "UTC offset" } -} \ No newline at end of file +} From be774b8efc84633bcdce2514a7f55a0cd4dfa4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Donk=C3=B3?= Date: Mon, 2 Feb 2026 11:58:54 +0100 Subject: [PATCH 70/70] fixup! chore: prettier and xo fixes --- src/features/rtk/RTKSatelliteObservations.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/rtk/RTKSatelliteObservations.jsx b/src/features/rtk/RTKSatelliteObservations.jsx index 00f4a96f9..b3e416215 100644 --- a/src/features/rtk/RTKSatelliteObservations.jsx +++ b/src/features/rtk/RTKSatelliteObservations.jsx @@ -175,7 +175,7 @@ const RTKSatelliteObservations = ({ height = 160, items }) => { items ? createDataFromItemsAndDrawingContext(items, chart.ctx) : NO_DATA ); } - }, [items]); + }, [chartRef.current, items]); return (