Skip to content

Commit 3dad249

Browse files
authored
Save FLA colours to localstorage for persistence (#1157)
* Save FLA colours to localstorage for persistence * Bump ver * Address copilot review comments
1 parent 78dbf55 commit 3dad249

8 files changed

Lines changed: 139 additions & 14 deletions

File tree

gcs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"name": "Avis-Drone-Labs"
77
},
88
"private": true,
9-
"version": "0.2.4-alpha",
9+
"version": "0.2.5-alpha",
1010
"license": "GPL-3.0-only",
1111
"homepage": "https://fgcs.projectfalcon.uk",
1212
"githubLink": "https://github.com/Avis-Drone-Labs/FGCS",

gcs/src/components/fla/chartDataCard.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
setCanSavePreset,
1919
setCustomColors,
2020
setMessageFilters,
21+
setPersistentColorMapEntry,
2122
} from "../../redux/slices/logAnalyserSlice.js"
2223

2324
function ChartDataCard({ item, unit, messageMeans }) {
@@ -35,6 +36,7 @@ function ChartDataCard({ item, unit, messageMeans }) {
3536

3637
const newColors = { ...customColors, [label]: color }
3738
dispatch(setCustomColors(newColors))
39+
dispatch(setPersistentColorMapEntry({ key: label, color }))
3840
},
3941
[customColors, dispatch],
4042
)

gcs/src/components/fla/messagesFiltersAccordion.jsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import {
2020
selectCustomColors,
2121
selectLogType,
2222
selectMessageFilters,
23+
selectPersistentColorMap,
2324
setCanSavePreset,
2425
setColorIndex,
2526
setCustomColors,
2627
setMessageFilters,
28+
setPersistentColorMapEntry,
2729
} from "../../redux/slices/logAnalyserSlice.js"
2830
import { colorPalette } from "./constants.js"
2931

@@ -64,6 +66,7 @@ export default function MessagesFiltersAccordion() {
6466

6567
const messageFilters = useSelector(selectMessageFilters)
6668
const customColors = useSelector(selectCustomColors)
69+
const persistentColorMap = useSelector(selectPersistentColorMap)
6770
const colorIndex = useSelector(selectColorIndex)
6871
const logType = useSelector(selectLogType)
6972
const aircraftType = useSelector(selectAircraftType)
@@ -113,14 +116,26 @@ export default function MessagesFiltersAccordion() {
113116

114117
const checked = event.currentTarget.checked
115118
let newColors = { ...customColors }
119+
const fieldKey = `${messageName}/${fieldName}`
116120

117121
if (!checked) {
118-
delete newColors[`${messageName}/${fieldName}`]
122+
delete newColors[fieldKey]
119123
} else {
120-
if (!newColors[`${messageName}/${fieldName}`]) {
121-
newColors[`${messageName}/${fieldName}`] =
122-
colorPalette[colorIndex % colorPalette.length]
123-
dispatch(setColorIndex((colorIndex + 1) % colorPalette.length))
124+
if (!newColors[fieldKey]) {
125+
// Check persistent map first, then fall back to palette
126+
if (persistentColorMap[fieldKey]) {
127+
newColors[fieldKey] = persistentColorMap[fieldKey]
128+
} else {
129+
const assignedColor = colorPalette[colorIndex % colorPalette.length]
130+
newColors[fieldKey] = assignedColor
131+
dispatch(
132+
setPersistentColorMapEntry({
133+
key: fieldKey,
134+
color: assignedColor,
135+
}),
136+
)
137+
dispatch(setColorIndex((colorIndex + 1) % colorPalette.length))
138+
}
124139
}
125140
}
126141

@@ -134,7 +149,7 @@ export default function MessagesFiltersAccordion() {
134149
)
135150
dispatch(setCanSavePreset(hasSelectedFilters))
136151
},
137-
[messageFilters, customColors, colorIndex, dispatch],
152+
[messageFilters, customColors, persistentColorMap, colorIndex, dispatch],
138153
)
139154

140155
// Memoize description getters

gcs/src/components/fla/presetAccordionItem.jsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ import {
2323
import {
2424
selectAircraftType,
2525
// Selectors
26+
selectColorIndex,
2627
selectLogType,
2728
selectMessageFilters,
29+
selectPersistentColorMap,
2830
setCanSavePreset,
2931
setColorIndex,
3032
setCustomColors,
3133
setMessageFilters,
34+
setPersistentColorMapEntry,
3235
} from "../../redux/slices/logAnalyserSlice.js"
3336

3437
// Utility function to convert a string to title case
@@ -42,8 +45,10 @@ export default function PresetAccordionItem({ category, deleteCustomPreset }) {
4245
// Redux
4346
const dispatch = useDispatch()
4447
const aircraftType = useSelector(selectAircraftType)
48+
const colorIndex = useSelector(selectColorIndex)
4549
const logType = useSelector(selectLogType)
4650
const messageFilters = useSelector(selectMessageFilters)
51+
const persistentColorMap = useSelector(selectPersistentColorMap)
4752

4853
// Preset selection
4954
function selectPreset(preset) {
@@ -60,7 +65,11 @@ export default function PresetAccordionItem({ category, deleteCustomPreset }) {
6065
newFilters[categoryName][fieldName] = false
6166
})
6267
})
63-
let newColors = {}
68+
// Use preset's custom colors if available, otherwise initialize empty
69+
let newColors = preset.customColors
70+
? structuredClone(preset.customColors)
71+
: {}
72+
let nextColorIndex = colorIndex
6473

6574
// Turn on filters for the given preset
6675
Object.keys(preset.filters).forEach((requestedName) => {
@@ -78,10 +87,26 @@ export default function PresetAccordionItem({ category, deleteCustomPreset }) {
7887
}
7988
newFilters[actualMessageName][field] = true
8089

81-
// Assign a color using the actual message name
82-
if (!newColors[`${actualMessageName}/${field}`]) {
83-
newColors[`${actualMessageName}/${field}`] =
84-
colorPalette[Object.keys(newColors).length % colorPalette.length]
90+
// Assign a color
91+
const fieldKey = `${actualMessageName}/${field}`
92+
// First priority: use color from preset if it exists
93+
if (!newColors[fieldKey]) {
94+
// Second priority: use color from persistent map if it exists
95+
if (persistentColorMap[fieldKey]) {
96+
newColors[fieldKey] = persistentColorMap[fieldKey]
97+
} else {
98+
// Fall back to palette color
99+
const assignedColor =
100+
colorPalette[nextColorIndex % colorPalette.length]
101+
newColors[fieldKey] = assignedColor
102+
nextColorIndex = (nextColorIndex + 1) % colorPalette.length
103+
dispatch(
104+
setPersistentColorMapEntry({
105+
key: fieldKey,
106+
color: assignedColor,
107+
}),
108+
)
109+
}
85110
}
86111
})
87112
} else {
@@ -91,7 +116,7 @@ export default function PresetAccordionItem({ category, deleteCustomPreset }) {
91116
}
92117
})
93118

94-
dispatch(setColorIndex(Object.keys(newColors).length % colorPalette.length)) // limited by palette length
119+
dispatch(setColorIndex(nextColorIndex)) // limited by palette length
95120
dispatch(setCustomColors(newColors))
96121
dispatch(setMessageFilters(newFilters))
97122
// Don't allow saving if we just selected an existing preset

gcs/src/components/fla/savePresetModal.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "../../helpers/notification.js"
1111
import {
1212
selectAircraftType,
13+
selectCustomColors,
1314
selectLogType,
1415
selectMessageFilters,
1516
setCanSavePreset,
@@ -25,6 +26,7 @@ export default function SavePresetModal({
2526
const dispatch = useDispatch()
2627
const logType = useSelector(selectLogType)
2728
const messageFilters = useSelector(selectMessageFilters)
29+
const customColors = useSelector(selectCustomColors)
2830
const aircraftType = useSelector(selectAircraftType)
2931

3032
const [presetName, setPresetName] = useState("")
@@ -51,6 +53,7 @@ export default function SavePresetModal({
5153
const newPreset = {
5254
name: presetName,
5355
filters: currentFilters,
56+
customColors: customColors,
5457
aircraftType: aircraftType ? [aircraftType] : undefined, // Only save the aircraft type if it exists
5558
}
5659

gcs/src/fla.jsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
selectBaseChartData,
2323
selectCustomColors,
2424
selectMessageFilters,
25+
selectPersistentColorMap,
2526
// Selectors
2627
setAircraftType,
2728
setBaseChartData,
@@ -47,6 +48,7 @@ export default function FLA() {
4748
const dispatch = useDispatch()
4849
const messageFilters = useSelector(selectMessageFilters)
4950
const customColors = useSelector(selectCustomColors)
51+
const persistentColorMap = useSelector(selectPersistentColorMap)
5052
const baseChartData = useSelector(selectBaseChartData)
5153

5254
/**
@@ -160,6 +162,40 @@ export default function FLA() {
160162
}
161163
}, [requestedLabels, baseChartData])
162164

165+
// Restore visible colors from persistent map when messageFilters change
166+
useEffect(() => {
167+
if (!messageFilters) return
168+
169+
// Build colors for active filters only.
170+
// Prefer persisted colors when present, otherwise keep existing custom color.
171+
const mergedVisibleColors = Object.keys(messageFilters).reduce(
172+
(acc, category) => {
173+
Object.keys(messageFilters[category]).forEach((field) => {
174+
if (!messageFilters[category][field]) return
175+
176+
const key = `${category}/${field}`
177+
const resolvedColor = persistentColorMap[key] ?? customColors[key]
178+
if (resolvedColor) {
179+
acc[key] = resolvedColor
180+
}
181+
})
182+
return acc
183+
},
184+
{},
185+
)
186+
187+
const isSameAsCurrentColors =
188+
Object.keys(mergedVisibleColors).length ===
189+
Object.keys(customColors).length &&
190+
Object.entries(mergedVisibleColors).every(
191+
([key, value]) => customColors[key] === value,
192+
)
193+
194+
if (Object.keys(mergedVisibleColors).length > 0 && !isSameAsCurrentColors) {
195+
dispatch(setCustomColors(mergedVisibleColors))
196+
}
197+
}, [messageFilters, persistentColorMap, customColors, dispatch])
198+
163199
// Step 3: Memoize the final chart data.
164200
// This filters the master cache. Colors are set to white by default.
165201
// The actual display colors come from customColors and are applied

gcs/src/redux/slices/logAnalyserSlice.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const logAnalyserSlice = createSlice({
1414
messageMeans: {},
1515
baseChartData: [],
1616
customColors: {},
17+
persistentColorMap: {},
1718
colorIndex: 0,
1819
aircraftType: null,
1920
canSavePreset: false,
@@ -64,6 +65,15 @@ const logAnalyserSlice = createSlice({
6465
setCustomColors: (state, action) => {
6566
state.customColors = action.payload
6667
},
68+
setPersistentColorMap: (state, action) => {
69+
state.persistentColorMap = action.payload
70+
},
71+
setPersistentColorMapEntry: (state, action) => {
72+
const { key, color } = action.payload || {}
73+
if (!key || !color) return
74+
if (state.persistentColorMap[key] === color) return
75+
state.persistentColorMap[key] = color
76+
},
6777
setColorIndex: (state, action) => {
6878
state.colorIndex = action.payload
6979
},
@@ -109,6 +119,7 @@ const logAnalyserSlice = createSlice({
109119
selectMessageMeans: (state) => state.messageMeans,
110120
selectBaseChartData: (state) => state.baseChartData,
111121
selectCustomColors: (state) => state.customColors,
122+
selectPersistentColorMap: (state) => state.persistentColorMap,
112123
selectColorIndex: (state) => state.colorIndex,
113124
selectAircraftType: (state) => state.aircraftType,
114125
selectCanSavePreset: (state) => state.canSavePreset,
@@ -133,6 +144,8 @@ export const {
133144
setMessageMeans,
134145
setBaseChartData,
135146
setCustomColors,
147+
setPersistentColorMap,
148+
setPersistentColorMapEntry,
136149
setColorIndex,
137150
setAircraftType,
138151
setCanSavePreset,
@@ -157,6 +170,7 @@ export const {
157170
selectMessageMeans,
158171
selectBaseChartData,
159172
selectCustomColors,
173+
selectPersistentColorMap,
160174
selectColorIndex,
161175
selectAircraftType,
162176
selectCanSavePreset,

gcs/src/redux/store.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import droneInfoSlice, {
2323
setSelectedDisplayTelemetry,
2424
} from "./slices/droneInfoSlice"
2525
import ftpSlice from "./slices/ftpSlice"
26-
import logAnalyserSlice from "./slices/logAnalyserSlice"
26+
import logAnalyserSlice, {
27+
setPersistentColorMap,
28+
} from "./slices/logAnalyserSlice"
2729
import missionInfoSlice, { setPlannedHomePosition } from "./slices/missionSlice"
2830
import paramsSlice from "./slices/paramsSlice"
2931
import simulationParamsSlice from "./slices/simulationParamsSlice"
@@ -211,6 +213,19 @@ if (plannedHomePosition !== null) {
211213
}
212214
}
213215

216+
const persistentColorMap = localStorage.getItem("flaPersistentColorMap")
217+
if (persistentColorMap !== null) {
218+
try {
219+
const parsedColorMap = JSON.parse(persistentColorMap)
220+
if (parsedColorMap && typeof parsedColorMap === "object") {
221+
store.dispatch(setPersistentColorMap(parsedColorMap))
222+
}
223+
} catch {
224+
console.log("Failed to parse persistent color map from localStorage")
225+
store.dispatch(setPersistentColorMap({}))
226+
}
227+
}
228+
214229
const updateLocalStorageIfChanged = (key, newValue) => {
215230
if (newValue !== null && newValue !== undefined) {
216231
const currentValue = localStorage.getItem(key)
@@ -284,6 +299,8 @@ function mergeSelectedDisplayTelemetryConfigWithDefaults(persistedConfig) {
284299
})
285300
}
286301

302+
let prevPersistentColorMap = store.getState().logAnalyser.persistentColorMap
303+
287304
// Update states when a new message comes in
288305
store.subscribe(() => {
289306
const store_mut = store.getState()
@@ -397,4 +414,17 @@ store.subscribe(() => {
397414
store_mut.missionInfo.plannedHomePosition,
398415
)
399416
}
417+
418+
const currentPersistentColorMap = store_mut.logAnalyser.persistentColorMap
419+
if (
420+
currentPersistentColorMap !== prevPersistentColorMap &&
421+
currentPersistentColorMap &&
422+
typeof currentPersistentColorMap === "object"
423+
) {
424+
updateJSONLocalStorageIfChanged(
425+
"flaPersistentColorMap",
426+
currentPersistentColorMap,
427+
)
428+
prevPersistentColorMap = currentPersistentColorMap
429+
}
400430
})

0 commit comments

Comments
 (0)